mirror of https://github.com/openspug/spug
add credential module
parent
85fa6d6e9f
commit
65b5e806b9
|
@ -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.
|
|
@ -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',)
|
|
@ -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),
|
||||
]
|
|
@ -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)
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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()
|
Loading…
Reference in New Issue