diff --git a/spug_api/apps/credential/__init__.py b/spug_api/apps/credential/__init__.py new file mode 100644 index 0000000..89f622a --- /dev/null +++ b/spug_api/apps/credential/__init__.py @@ -0,0 +1,3 @@ +# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug +# Copyright: (c) +# Released under the AGPL-3.0 License. diff --git a/spug_api/apps/credential/models.py b/spug_api/apps/credential/models.py new file mode 100644 index 0000000..617e935 --- /dev/null +++ b/spug_api/apps/credential/models.py @@ -0,0 +1,31 @@ +# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug +# Copyright: (c) +# Released under the AGPL-3.0 License. +from django.db import models +from libs.mixins import ModelMixin +from apps.account.models import User + + +class Credential(models.Model, ModelMixin): + TYPES = ( + ('pw', '密码'), + ('pk', '密钥'), + ) + name = models.CharField(max_length=64) + type = models.CharField(max_length=20, choices=TYPES) + username = models.CharField(max_length=64) + secret = models.TextField() + extra = models.CharField(max_length=255, null=True) + is_public = models.BooleanField(default=False) + created_by = models.ForeignKey(User, on_delete=models.PROTECT) + created_at = models.DateTimeField(auto_now_add=True) + + def to_view(self, user): + is_self = self.created_by_id == user.id + tmp = self.to_dict(excludes=None if is_self else ('secret', 'extra')) + tmp['type_alias'] = self.get_type_display() + return tmp + + class Meta: + db_table = 'credentials' + ordering = ('-id',) diff --git a/spug_api/apps/credential/urls.py b/spug_api/apps/credential/urls.py new file mode 100644 index 0000000..2977ed8 --- /dev/null +++ b/spug_api/apps/credential/urls.py @@ -0,0 +1,11 @@ +# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug +# Copyright: (c) +# Released under the AGPL-3.0 License. +from django.urls import path + +from apps.credential.views import CredView, handle_check + +urlpatterns = [ + path('', CredView.as_view()), + path('check/', handle_check), +] diff --git a/spug_api/apps/credential/views.py b/spug_api/apps/credential/views.py new file mode 100644 index 0000000..0abbcb7 --- /dev/null +++ b/spug_api/apps/credential/views.py @@ -0,0 +1,60 @@ +# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug +# Copyright: (c) +# Released under the AGPL-3.0 License. +from django.views.generic import View +from django.db.models import Q +from libs import JsonParser, Argument, json_response, auth +from libs.gitlib import RemoteGit +from apps.credential.models import Credential + + +class CredView(View): + def get(self, request): + credentials = Credential.objects.filter(Q(created_by=request.user) | Q(is_public=True)) + return json_response([x.to_view(request.user) for x in credentials]) + + @auth('deploy.app.add|deploy.app.edit|config.app.add|config.app.edit') + def post(self, request): + form, error = JsonParser( + Argument('id', type=int, required=False), + Argument('name', help='请输入凭据名称'), + Argument('username', help='请输入用户名'), + Argument('type', filter=lambda x: x in dict(Credential.TYPES), help='请选择凭据类型'), + Argument('is_public', type=bool, default=False), + Argument('secret', help='请输入密码/密钥'), + Argument('extra', required=False), + ).parse(request.body) + if error is None: + if form.id: + credential = Credential.objects.get(pk=form.id) + if credential.created_by_id != request.user.id: + return json_response(error='共享凭据无权修改') + credential.update_by_dict(form) + else: + Credential.objects.create(created_by=request.user, **form) + return json_response(error=error) + + @auth('deploy.app.del|config.app.del') + def delete(self, request): + form, error = JsonParser( + Argument('id', type=int, help='请指定操作对象') + ).parse(request.GET) + if error is None: + Credential.objects.filter(pk=form.id, created_by=request.user).delete() + return json_response(error=error) + + +def handle_check(request): + form, error = JsonParser( + Argument('id', type=int, required=False), + Argument('type', filter=lambda x: x in ('git',), help='参数错误'), + Argument('data', help='参数错误') + ).parse(request.body) + if error is None: + credential = None + if form.id: + credential = Credential.objects.get(pk=form.id) + if form.type == 'git': + is_pass, message = RemoteGit.check_auth(form.data, credential) + return json_response({'is_pass': is_pass, 'message': message}) + return json_response(error=error) diff --git a/spug_web/src/pages/system/credential/Form.js b/spug_web/src/pages/system/credential/Form.js new file mode 100644 index 0000000..e2cd1c5 --- /dev/null +++ b/spug_web/src/pages/system/credential/Form.js @@ -0,0 +1,75 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React, { useState } from 'react'; +import { observer } from 'mobx-react'; +import { Modal, Form, Select, Input, Switch, message } from 'antd'; +import http from 'libs/http'; +import store from './store'; + +export default observer(function () { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + + function handleSubmit() { + setLoading(true); + const formData = form.getFieldsValue(); + formData.id = store.record.id; + http.post('/api/credential/', formData) + .then(() => { + message.success('操作成功'); + store.formVisible = false; + store.fetchRecords() + }, () => setLoading(false)) + } + + return ( + store.formVisible = false} + confirmLoading={loading} + onOk={handleSubmit}> +
+ + + + + + + + + + + {({getFieldValue}) => + getFieldValue('type') === 'pw' ? ( + + + + ) : ( + + + + + + + + + ) + } + + + + +
+
+ ) +}) diff --git a/spug_web/src/pages/system/credential/Table.js b/spug_web/src/pages/system/credential/Table.js new file mode 100644 index 0000000..f6d35e9 --- /dev/null +++ b/spug_web/src/pages/system/credential/Table.js @@ -0,0 +1,92 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React from 'react'; +import { observer } from 'mobx-react'; +import { PlusOutlined } from '@ant-design/icons'; +import { Radio, Modal, Button, Tag, message } from 'antd'; +import { TableCard, Action } from 'components'; +import http from 'libs/http'; +import store from './store'; + +@observer +class ComTable extends React.Component { + constructor(props) { + super(props); + this.state = { + password: '' + } + } + + componentDidMount() { + store.fetchRecords() + } + + columns = [{ + title: '凭据类型', + dataIndex: 'type_alias', + }, { + title: '凭据名称', + dataIndex: 'name', + }, { + title: '用户名', + dataIndex: 'username', + }, { + title: '可共享', + dataIndex: 'is_public', + render: v => v ? 开启 : 关闭 + }, { + title: '操作', + render: info => ( + + store.showForm(info)}>编辑 + this.handleDelete(info)}>删除 + + ) + }]; + + handleDelete = (text) => { + Modal.confirm({ + title: '删除确认', + content: `确定要删除【${text.name}】?`, + onOk: () => { + return http.delete('/api/credential/', {params: {id: text.id}}) + .then(() => { + message.success('删除成功'); + store.fetchRecords() + }) + } + }) + }; + + render() { + return ( + } onClick={() => store.showForm()}>新建, + store.f_status = e.target.value}> + 全部 + 正常 + 禁用 + + ]} + pagination={{ + showSizeChanger: true, + showLessItems: true, + showTotal: total => `共 ${total} 条`, + pageSizeOptions: ['10', '20', '50', '100'] + }} + columns={this.columns}/> + ) + } +} + +export default ComTable diff --git a/spug_web/src/pages/system/credential/index.js b/spug_web/src/pages/system/credential/index.js new file mode 100644 index 0000000..4311e2c --- /dev/null +++ b/spug_web/src/pages/system/credential/index.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React from 'react'; +import { observer } from 'mobx-react'; +import { Input, Select } from 'antd'; +import { SearchForm, AuthDiv, Breadcrumb } from 'components'; +import ComTable from './Table'; +import ComForm from './Form'; +import store from './store'; + +export default observer(function () { + return ( + + + 首页 + 系统管理 + 凭据管理 + + + + store.f_name = e.target.value} placeholder="请输入"/> + + + + + + + {store.formVisible && } + + ) +}) diff --git a/spug_web/src/pages/system/credential/store.js b/spug_web/src/pages/system/credential/store.js new file mode 100644 index 0000000..59d8503 --- /dev/null +++ b/spug_web/src/pages/system/credential/store.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import { observable, computed } from 'mobx'; +import { http, includes } from 'libs'; +import lds from 'lodash'; + +class Store { + @observable records = []; + @observable record = {}; + @observable isFetching = true; + @observable formVisible = false; + + @observable f_name; + @observable f_is_public; + + @computed get dataSource() { + let records = this.records; + if (this.f_name) records = records.filter(x => includes(x.name, this.f_name)); + if (!lds.isNil(this.f_is_public)) records = records.filter(x => this.f_is_public === x.is_public); + return records + } + + fetchRecords = () => { + this.isFetching = true; + http.get('/api/credential/') + .then(res => this.records = res) + .finally(() => this.isFetching = false) + }; + + showForm = (info = {}) => { + this.formVisible = true; + this.record = info + } +} + +export default new Store()