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