add credential module

4.0
vapao 2023-02-22 16:50:04 +08:00
parent 85fa6d6e9f
commit 65b5e806b9
8 changed files with 348 additions and 0 deletions

View File

@ -0,0 +1,3 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.

View File

@ -0,0 +1,31 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# 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',)

View File

@ -0,0 +1,11 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# 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),
]

View File

@ -0,0 +1,60 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# 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)

View File

@ -0,0 +1,75 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* 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 (
<Modal
visible
width={700}
maskClosable={false}
title={store.record.id ? '编辑凭证' : '新建凭证'}
onCancel={() => store.formVisible = false}
confirmLoading={loading}
onOk={handleSubmit}>
<Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 14}}>
<Form.Item required name="type" label="凭证类型" initialValue="pw">
<Select placeholder="请选择">
<Select.Option value="pw">密码</Select.Option>
<Select.Option value="pk">密钥</Select.Option>
</Select>
</Form.Item>
<Form.Item required name="name" label="凭证名称">
<Input placeholder="请输入凭证名称"/>
</Form.Item>
<Form.Item required name="username" label="用户名">
<Input placeholder="请输入用户名"/>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{({getFieldValue}) =>
getFieldValue('type') === 'pw' ? (
<Form.Item required name="secret" label="密码">
<Input placeholder="请输入密码"/>
</Form.Item>
) : (
<React.Fragment>
<Form.Item required name="secret" label="私钥">
<Input.TextArea placeholder="请输入私钥内容"/>
</Form.Item>
<Form.Item name="extra" label="私钥密码">
<Input placeholder="请输入私钥密码"/>
</Form.Item>
</React.Fragment>
)
}
</Form.Item>
<Form.Item name="is_public" valuePropName="checked" label="共享凭据" tooltip="启用后凭据可以被其它用户使用。"
initialValue={false}>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
</Form>
</Modal>
)
})

View File

@ -0,0 +1,92 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* 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 ? <Tag color="green">开启</Tag> : <Tag></Tag>
}, {
title: '操作',
render: info => (
<Action>
<Action.Button onClick={() => store.showForm(info)}>编辑</Action.Button>
<Action.Button danger onClick={() => this.handleDelete(info)}>删除</Action.Button>
</Action>
)
}];
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 (
<TableCard
tKey="sc"
rowKey="id"
title="凭据列表"
loading={store.isFetching}
dataSource={store.dataSource}
onReload={store.fetchRecords}
actions={[
<Button type="primary" icon={<PlusOutlined/>} onClick={() => store.showForm()}>新建</Button>,
<Radio.Group value={store.f_status} onChange={e => store.f_status = e.target.value}>
<Radio.Button value="">全部</Radio.Button>
<Radio.Button value="true">正常</Radio.Button>
<Radio.Button value="false">禁用</Radio.Button>
</Radio.Group>
]}
pagination={{
showSizeChanger: true,
showLessItems: true,
showTotal: total => `${total}`,
pageSizeOptions: ['10', '20', '50', '100']
}}
columns={this.columns}/>
)
}
}
export default ComTable

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* 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 (
<AuthDiv auth="system.account.view">
<Breadcrumb>
<Breadcrumb.Item>首页</Breadcrumb.Item>
<Breadcrumb.Item>系统管理</Breadcrumb.Item>
<Breadcrumb.Item>凭据管理</Breadcrumb.Item>
</Breadcrumb>
<SearchForm>
<SearchForm.Item span={8} title="凭据名称">
<Input allowClear value={store.f_name} onChange={e => store.f_name = e.target.value} placeholder="请输入"/>
</SearchForm.Item>
<SearchForm.Item span={8} title="可共享">
<Select allowClear value={store.f_is_public} onChange={v => store.f_is_public = v} placeholder="请选择">
<Select.Option value={true}>开启</Select.Option>
<Select.Option value={false}>关闭</Select.Option>
</Select>
</SearchForm.Item>
</SearchForm>
<ComTable/>
{store.formVisible && <ComForm/>}
</AuthDiv>
)
})

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* 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()