feat: 新增我的授权历史组件
This commit is contained in:
parent
c28bca6385
commit
6b29b03750
|
|
@ -0,0 +1,3 @@
|
|||
import { EntityDict } from "../../../oak-app-domain";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "oauthUserAuthorization", true, {}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import assert from "assert";
|
||||
import { generateNewId } from "oak-domain/lib/utils/uuid";
|
||||
export default OakComponent({
|
||||
entity: 'oauthUserAuthorization',
|
||||
isList: true,
|
||||
projection: {
|
||||
userId: 1,
|
||||
applicationId: 1,
|
||||
authorizedAt: 1,
|
||||
codeId: 1,
|
||||
tokenId: 1,
|
||||
usageState: 1,
|
||||
$$createAt$$: 1,
|
||||
application: {
|
||||
logo: 1,
|
||||
name: 1,
|
||||
description: 1,
|
||||
isConfidential: 1,
|
||||
},
|
||||
code: {
|
||||
scope: 1,
|
||||
},
|
||||
token: {
|
||||
accessExpiresAt: 1,
|
||||
refreshExpiresAt: 1,
|
||||
lastUsedAt: 1,
|
||||
revokedAt: 1,
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
pageSize: 5,
|
||||
currentPage: 1,
|
||||
},
|
||||
filters: [{
|
||||
filter() {
|
||||
const userId = this.features.token.getUserId();
|
||||
const systemId = this.features.application.getApplication()?.systemId;
|
||||
return {
|
||||
userId,
|
||||
application: {
|
||||
systemId: systemId
|
||||
},
|
||||
usageState: {
|
||||
$in: ['granted', 'denied', 'revoked']
|
||||
}
|
||||
};
|
||||
},
|
||||
}],
|
||||
formData({ data }) {
|
||||
return {
|
||||
list: data,
|
||||
};
|
||||
},
|
||||
properties: {},
|
||||
methods: {
|
||||
async revoke(item) {
|
||||
assert(item.id, 'No id found for this authorization record');
|
||||
await this.features.cache.operate("oauthUserAuthorization", {
|
||||
action: "revoke",
|
||||
id: generateNewId(),
|
||||
data: {},
|
||||
filter: {
|
||||
id: item.id
|
||||
}
|
||||
});
|
||||
console.log('Revoking authorization for:', item.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"page_title": "My Authorization Records",
|
||||
"page_description": "Manage access permissions you've granted to third-party applications",
|
||||
"no_records": "No authorization records",
|
||||
"unknown_app": "Unknown Application",
|
||||
"revoke": "Revoke",
|
||||
"revoke_confirm_title": "Confirm Revoke Authorization",
|
||||
"revoke_confirm_content": "Are you sure you want to revoke authorization for '%{appName}'? The application will no longer be able to access your data.",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"status_active": "Active",
|
||||
"status_revoked": "Revoked",
|
||||
"status_denied": "Denied",
|
||||
"status_expired": "Expired",
|
||||
"status_unknown": "Unknown",
|
||||
"authorized_at": "Authorized At",
|
||||
"scope": "Scope",
|
||||
"full_access": "Full Access",
|
||||
"last_used_at": "Last Used",
|
||||
"access_expires_at": "Access Expires At",
|
||||
"revoked_at": "Revoked At",
|
||||
"pagination_total": "Total %{total} items"
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"page_title": "我的授权记录",
|
||||
"page_description": "管理您授权给第三方应用的访问权限",
|
||||
"no_records": "暂无授权记录",
|
||||
"unknown_app": "未知应用",
|
||||
"revoke": "撤销",
|
||||
"revoke_confirm_title": "确认撤销授权",
|
||||
"revoke_confirm_content": "确定要撤销对「%{appName}」的授权吗?撤销后该应用将无法访问您的数据。",
|
||||
"confirm": "确定",
|
||||
"cancel": "取消",
|
||||
"status_active": "已授权",
|
||||
"status_revoked": "已撤销",
|
||||
"status_denied": "已拒绝",
|
||||
"status_expired": "已过期",
|
||||
"status_unknown": "未知状态",
|
||||
"authorized_at": "授权时间",
|
||||
"scope": "授权范围",
|
||||
"full_access": "完全访问",
|
||||
"last_used_at": "最后使用",
|
||||
"access_expires_at": "访问过期时间",
|
||||
"revoked_at": "撤销时间",
|
||||
"pagination_total": "共 %{total} 条"
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 32px;
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.listContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.item {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.appInfo {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.appLogo {
|
||||
flex-shrink: 0;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d1d5db;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.appNameWrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.appName {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.appDescription {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 0 0 16px 60px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.statusActive,
|
||||
.statusRevoked,
|
||||
.statusDenied,
|
||||
.statusExpired,
|
||||
.statusUnknown {
|
||||
font-size: 12px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.statusActive {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.statusRevoked {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.statusDenied {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.statusExpired {
|
||||
background: #e5e7eb;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.statusUnknown {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.revokeButton {
|
||||
padding: 6px 16px;
|
||||
font-size: 14px;
|
||||
color: #dc2626;
|
||||
background: #fff;
|
||||
border: 1px solid #dc2626;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.itemDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
margin-left: 64px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.detailRow {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #6b7280;
|
||||
min-width: 120px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #1f2937;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty p {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
declare const Records: (props: WebComponentProps<EntityDict, "oauthUserAuthorization", true, {
|
||||
list: RowWithActions<EntityDict, "oauthUserAuthorization">[];
|
||||
}, {
|
||||
revoke: (item: RowWithActions<EntityDict, "oauthUserAuthorization">) => void;
|
||||
}>) => React.JSX.Element;
|
||||
export default Records;
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import React from 'react';
|
||||
import { Modal, Avatar, Pagination } from 'antd';
|
||||
import { AppstoreOutlined } from '@ant-design/icons';
|
||||
import Styles from './styles.module.less';
|
||||
const Records = (props) => {
|
||||
const { list, oakPagination } = props.data;
|
||||
const { pageSize, currentPage, total } = oakPagination || {};
|
||||
const { t, revoke, setCurrentPage, setPageSize } = props.methods;
|
||||
const handleRevoke = (item) => {
|
||||
Modal.confirm({
|
||||
title: t('revoke_confirm_title'),
|
||||
content: t('revoke_confirm_content', { appName: item.application?.name || t('unknown_app') }),
|
||||
okText: t('confirm'),
|
||||
cancelText: t('cancel'),
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => {
|
||||
revoke(item);
|
||||
},
|
||||
});
|
||||
};
|
||||
const formatDate = (date) => {
|
||||
if (!date)
|
||||
return '-';
|
||||
return new Date(date).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
function formateScope(scope) {
|
||||
if (!scope || scope.length === 0 || scope.includes('')) {
|
||||
return t('full_access');
|
||||
}
|
||||
return scope.join(', ');
|
||||
}
|
||||
const getStatusTag = (item) => {
|
||||
if (item.token?.revokedAt) {
|
||||
return <span className={Styles.statusRevoked}>{t('status_revoked')}</span>;
|
||||
}
|
||||
if (item.usageState === 'denied') {
|
||||
return <span className={Styles.statusDenied}>{t('status_denied')}</span>;
|
||||
}
|
||||
if (item.token?.refreshExpiresAt && new Date(item.token.refreshExpiresAt) < new Date()) {
|
||||
return <span className={Styles.statusExpired}>{t('status_expired')}</span>;
|
||||
}
|
||||
if (item.usageState === 'granted') {
|
||||
return <span className={Styles.statusActive}>{t('status_active')}</span>;
|
||||
}
|
||||
if (item.usageState === 'revoked') {
|
||||
return <span className={Styles.statusRevoked}>{t('status_revoked')}</span>;
|
||||
}
|
||||
return <span className={Styles.statusUnknown}>{t('status_unknown')}</span>;
|
||||
};
|
||||
const isRevokable = (item) => {
|
||||
return item.usageState === 'granted' && !item.token?.revokedAt;
|
||||
};
|
||||
return (<div className={Styles.container}>
|
||||
<div className={Styles.header}>
|
||||
<h2>{t('page_title')}</h2>
|
||||
<p className={Styles.description}>{t('page_description')}</p>
|
||||
</div>
|
||||
|
||||
{!list || list.length === 0 ? (<div className={Styles.empty}>
|
||||
<div className={Styles.emptyIcon}>🔐</div>
|
||||
<p>{t('no_records')}</p>
|
||||
</div>) : (<>
|
||||
<div className={Styles.listContainer}>
|
||||
{list.map((item) => {
|
||||
return (<div key={item.id} className={Styles.item}>
|
||||
<div className={Styles.itemHeader}>
|
||||
<div className={Styles.appInfo}>
|
||||
<Avatar src={item.application?.logo} icon={!item.application?.logo && <AppstoreOutlined />} size={48} shape="square" className={Styles.appLogo}/>
|
||||
<div className={Styles.appNameWrapper}>
|
||||
<h3 className={Styles.appName}>
|
||||
{item.application?.name || t('unknown_app')}
|
||||
</h3>
|
||||
{getStatusTag(item)}
|
||||
</div>
|
||||
</div>
|
||||
{isRevokable(item) && (<button className={Styles.revokeButton} onClick={() => handleRevoke(item)}>
|
||||
{t('revoke')}
|
||||
</button>)}
|
||||
</div>
|
||||
|
||||
{item.application?.description && (<p className={Styles.appDescription}>
|
||||
{item.application?.description}
|
||||
</p>)}
|
||||
|
||||
<div className={Styles.itemDetails}>
|
||||
<div className={Styles.detailRow}>
|
||||
<span className={Styles.label}>{t('authorized_at')}:</span>
|
||||
<span className={Styles.value}>{formatDate(item.authorizedAt)}</span>
|
||||
</div>
|
||||
|
||||
<div className={Styles.detailRow}>
|
||||
<span className={Styles.label}>{t('scope')}:</span>
|
||||
<span className={Styles.value}>
|
||||
{formateScope(item.code?.scope)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{item.token?.lastUsedAt && (<div className={Styles.detailRow}>
|
||||
<span className={Styles.label}>{t('last_used_at')}:</span>
|
||||
<span className={Styles.value}>{formatDate(item.token.lastUsedAt)}</span>
|
||||
</div>)}
|
||||
|
||||
{/* {item.token?.accessExpiresAt && (
|
||||
<div className={Styles.detailRow}>
|
||||
<span className={Styles.label}>{t('access_expires_at')}:</span>
|
||||
<span className={Styles.value}>{formatDate(item.token.accessExpiresAt)}</span>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{item.token?.revokedAt && (<div className={Styles.detailRow}>
|
||||
<span className={Styles.label}>{t('revoked_at')}:</span>
|
||||
<span className={Styles.value}>{formatDate(item.token.revokedAt)}</span>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{total && total > 0 && (<div className={Styles.paginationWrapper}>
|
||||
<Pagination current={currentPage} pageSize={pageSize} total={total} onChange={(page) => setCurrentPage(page)} onShowSizeChange={(current, size) => {
|
||||
setPageSize(size);
|
||||
setCurrentPage(1);
|
||||
}} showSizeChanger showQuickJumper showTotal={(total) => t('pagination_total', { total })} pageSizeOptions={['5', '10', '20', '50', '100']} className={Styles.pagination}/>
|
||||
</div>)}
|
||||
</>)}
|
||||
</div>);
|
||||
};
|
||||
export default Records;
|
||||
|
|
@ -444,6 +444,36 @@ const i18ns = [
|
|||
"refreshEndpointPlaceholder": "请输入刷新端点URL"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "0b72f69f7fe7160fb6871917d674c12b",
|
||||
namespace: "oak-general-business-c-oauth-records",
|
||||
language: "zh-CN",
|
||||
module: "oak-general-business",
|
||||
position: "src/components/oauth/records",
|
||||
data: {
|
||||
"page_title": "我的授权记录",
|
||||
"page_description": "管理您授权给第三方应用的访问权限",
|
||||
"no_records": "暂无授权记录",
|
||||
"unknown_app": "未知应用",
|
||||
"revoke": "撤销",
|
||||
"revoke_confirm_title": "确认撤销授权",
|
||||
"revoke_confirm_content": "确定要撤销对「%{appName}」的授权吗?撤销后该应用将无法访问您的数据。",
|
||||
"confirm": "确定",
|
||||
"cancel": "取消",
|
||||
"status_active": "已授权",
|
||||
"status_revoked": "已撤销",
|
||||
"status_denied": "已拒绝",
|
||||
"status_expired": "已过期",
|
||||
"status_unknown": "未知状态",
|
||||
"authorized_at": "授权时间",
|
||||
"scope": "授权范围",
|
||||
"full_access": "完全访问",
|
||||
"last_used_at": "最后使用",
|
||||
"access_expires_at": "访问过期时间",
|
||||
"revoked_at": "撤销时间",
|
||||
"pagination_total": "共 %{total} 条"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "9bdbb9993789ecd48e7b0e10b7117620",
|
||||
namespace: "oak-general-business-c-passport",
|
||||
|
|
|
|||
|
|
@ -9,52 +9,107 @@ const triggers = [
|
|||
fn: async ({ operation }, context) => {
|
||||
const { filter } = operation;
|
||||
assert(filter, 'No filter found in revoke operation');
|
||||
const datas = await context.select("oauthUserAuthorization", {
|
||||
data: {
|
||||
userId: 1,
|
||||
applicationId: 1,
|
||||
tokenId: 1,
|
||||
codeId: 1,
|
||||
usageState: 1,
|
||||
},
|
||||
filter: { ...filter },
|
||||
}, {});
|
||||
let res = 0;
|
||||
// 如果是unused并且code的usedAt是空的,则把code的usedAt全部设置为当前时间
|
||||
const opRes0 = await context.operate("oauthAuthorizationCode", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: "update",
|
||||
data: {
|
||||
usedAt: new Date()
|
||||
},
|
||||
filter: {
|
||||
usedAt: {
|
||||
$exists: false
|
||||
for (const data of datas) {
|
||||
// 如果是unused并且code的usedAt是空的,则把code的usedAt全部设置为当前时间
|
||||
const opRes0 = await context.operate("oauthAuthorizationCode", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: "update",
|
||||
data: {
|
||||
usedAt: new Date()
|
||||
},
|
||||
oauthUserAuthorization$code: {
|
||||
...filter,
|
||||
// 未被使用肯定就没有tokenId
|
||||
usageState: 'unused',
|
||||
filter: {
|
||||
usedAt: {
|
||||
$exists: false
|
||||
},
|
||||
// 某一个用户对某一个应用的授权记录
|
||||
oauthAppId: data.applicationId,
|
||||
userId: data.userId,
|
||||
}
|
||||
}, {});
|
||||
res += opRes0.oauthAuthorizationCode?.update || 0;
|
||||
// // 如果没有token,可以直接删除oauthUserAuthorization (可能是复用的之前的token, 也可能是未被使用的授权记录)
|
||||
if (data.usageState === 'unused' && !data.tokenId) {
|
||||
const opRes = await context.operate("oauthUserAuthorization", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: "remove",
|
||||
data: {},
|
||||
filter: {
|
||||
id: data.id,
|
||||
}
|
||||
}, {});
|
||||
res += opRes.oauthApplication?.remove || 0;
|
||||
}
|
||||
}, {});
|
||||
res += opRes0.oauthAuthorizationCode?.update || 0;
|
||||
// 如果没有token,可以直接删除oauthUserAuthorization (可能是复用的之前的token, 也可能是未被使用的授权记录)
|
||||
const opRes = await context.operate("oauthUserAuthorization", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: "remove",
|
||||
data: {},
|
||||
filter: {
|
||||
...filter,
|
||||
// 未被使用肯定就没有tokenId
|
||||
usageState: 'unused',
|
||||
// 如果有token,则将token的revokedAt设置为当前时间
|
||||
if (data.tokenId) {
|
||||
const opRes2 = await context.operate("oauthToken", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: "update",
|
||||
data: {
|
||||
revokedAt: new Date()
|
||||
},
|
||||
filter: {
|
||||
id: data.tokenId
|
||||
}
|
||||
}, {});
|
||||
res += opRes2.oauthToken?.update || 0;
|
||||
}
|
||||
}, {});
|
||||
res += opRes.oauthApplication?.remove || 0;
|
||||
// 如果有token,则将token的revokedAt设置为当前时间
|
||||
const opRes2 = await context.operate("oauthToken", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: "update",
|
||||
data: {
|
||||
revokedAt: new Date()
|
||||
},
|
||||
filter: {
|
||||
oauthUserAuthorization$token: {
|
||||
...filter
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
res += opRes2.oauthToken?.update || 0;
|
||||
}
|
||||
// // 如果是unused并且code的usedAt是空的,则把code的usedAt全部设置为当前时间
|
||||
// const opRes0 = await context.operate("oauthAuthorizationCode", {
|
||||
// id: await generateNewIdAsync(),
|
||||
// action: "update",
|
||||
// data: {
|
||||
// usedAt: new Date()
|
||||
// },
|
||||
// filter: {
|
||||
// usedAt: {
|
||||
// $exists: false
|
||||
// },
|
||||
// oauthUserAuthorization$code: {
|
||||
// ...filter,
|
||||
// // 未被使用肯定就没有tokenId
|
||||
// usageState: 'unused',
|
||||
// }
|
||||
// }
|
||||
// }, {});
|
||||
// res += opRes0.oauthAuthorizationCode?.update || 0;
|
||||
// // 如果没有token,可以直接删除oauthUserAuthorization (可能是复用的之前的token, 也可能是未被使用的授权记录)
|
||||
// const opRes = await context.operate("oauthUserAuthorization", {
|
||||
// id: await generateNewIdAsync(),
|
||||
// action: "remove",
|
||||
// data: {},
|
||||
// filter: {
|
||||
// ...filter,
|
||||
// // 未被使用肯定就没有tokenId
|
||||
// usageState: 'unused',
|
||||
// }
|
||||
// }, {});
|
||||
// res += opRes.oauthApplication?.remove || 0;
|
||||
// // 如果有token,则将token的revokedAt设置为当前时间
|
||||
// const opRes2 = await context.operate("oauthToken", {
|
||||
// id: await generateNewIdAsync(),
|
||||
// action: "update",
|
||||
// data: {
|
||||
// revokedAt: new Date()
|
||||
// },
|
||||
// filter: {
|
||||
// oauthUserAuthorization$token: {
|
||||
// ...filter
|
||||
// }
|
||||
// }
|
||||
// }, {});
|
||||
// res += opRes2.oauthToken?.update || 0;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -446,6 +446,36 @@ const i18ns = [
|
|||
"refreshEndpointPlaceholder": "请输入刷新端点URL"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "0b72f69f7fe7160fb6871917d674c12b",
|
||||
namespace: "oak-general-business-c-oauth-records",
|
||||
language: "zh-CN",
|
||||
module: "oak-general-business",
|
||||
position: "src/components/oauth/records",
|
||||
data: {
|
||||
"page_title": "我的授权记录",
|
||||
"page_description": "管理您授权给第三方应用的访问权限",
|
||||
"no_records": "暂无授权记录",
|
||||
"unknown_app": "未知应用",
|
||||
"revoke": "撤销",
|
||||
"revoke_confirm_title": "确认撤销授权",
|
||||
"revoke_confirm_content": "确定要撤销对「%{appName}」的授权吗?撤销后该应用将无法访问您的数据。",
|
||||
"confirm": "确定",
|
||||
"cancel": "取消",
|
||||
"status_active": "已授权",
|
||||
"status_revoked": "已撤销",
|
||||
"status_denied": "已拒绝",
|
||||
"status_expired": "已过期",
|
||||
"status_unknown": "未知状态",
|
||||
"authorized_at": "授权时间",
|
||||
"scope": "授权范围",
|
||||
"full_access": "完全访问",
|
||||
"last_used_at": "最后使用",
|
||||
"access_expires_at": "访问过期时间",
|
||||
"revoked_at": "撤销时间",
|
||||
"pagination_total": "共 %{total} 条"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "9bdbb9993789ecd48e7b0e10b7117620",
|
||||
namespace: "oak-general-business-c-passport",
|
||||
|
|
|
|||
|
|
@ -12,52 +12,107 @@ const triggers = [
|
|||
fn: async ({ operation }, context) => {
|
||||
const { filter } = operation;
|
||||
(0, assert_1.default)(filter, 'No filter found in revoke operation');
|
||||
const datas = await context.select("oauthUserAuthorization", {
|
||||
data: {
|
||||
userId: 1,
|
||||
applicationId: 1,
|
||||
tokenId: 1,
|
||||
codeId: 1,
|
||||
usageState: 1,
|
||||
},
|
||||
filter: { ...filter },
|
||||
}, {});
|
||||
let res = 0;
|
||||
// 如果是unused并且code的usedAt是空的,则把code的usedAt全部设置为当前时间
|
||||
const opRes0 = await context.operate("oauthAuthorizationCode", {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: "update",
|
||||
data: {
|
||||
usedAt: new Date()
|
||||
},
|
||||
filter: {
|
||||
usedAt: {
|
||||
$exists: false
|
||||
for (const data of datas) {
|
||||
// 如果是unused并且code的usedAt是空的,则把code的usedAt全部设置为当前时间
|
||||
const opRes0 = await context.operate("oauthAuthorizationCode", {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: "update",
|
||||
data: {
|
||||
usedAt: new Date()
|
||||
},
|
||||
oauthUserAuthorization$code: {
|
||||
...filter,
|
||||
// 未被使用肯定就没有tokenId
|
||||
usageState: 'unused',
|
||||
filter: {
|
||||
usedAt: {
|
||||
$exists: false
|
||||
},
|
||||
// 某一个用户对某一个应用的授权记录
|
||||
oauthAppId: data.applicationId,
|
||||
userId: data.userId,
|
||||
}
|
||||
}, {});
|
||||
res += opRes0.oauthAuthorizationCode?.update || 0;
|
||||
// // 如果没有token,可以直接删除oauthUserAuthorization (可能是复用的之前的token, 也可能是未被使用的授权记录)
|
||||
if (data.usageState === 'unused' && !data.tokenId) {
|
||||
const opRes = await context.operate("oauthUserAuthorization", {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: "remove",
|
||||
data: {},
|
||||
filter: {
|
||||
id: data.id,
|
||||
}
|
||||
}, {});
|
||||
res += opRes.oauthApplication?.remove || 0;
|
||||
}
|
||||
}, {});
|
||||
res += opRes0.oauthAuthorizationCode?.update || 0;
|
||||
// 如果没有token,可以直接删除oauthUserAuthorization (可能是复用的之前的token, 也可能是未被使用的授权记录)
|
||||
const opRes = await context.operate("oauthUserAuthorization", {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: "remove",
|
||||
data: {},
|
||||
filter: {
|
||||
...filter,
|
||||
// 未被使用肯定就没有tokenId
|
||||
usageState: 'unused',
|
||||
// 如果有token,则将token的revokedAt设置为当前时间
|
||||
if (data.tokenId) {
|
||||
const opRes2 = await context.operate("oauthToken", {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: "update",
|
||||
data: {
|
||||
revokedAt: new Date()
|
||||
},
|
||||
filter: {
|
||||
id: data.tokenId
|
||||
}
|
||||
}, {});
|
||||
res += opRes2.oauthToken?.update || 0;
|
||||
}
|
||||
}, {});
|
||||
res += opRes.oauthApplication?.remove || 0;
|
||||
// 如果有token,则将token的revokedAt设置为当前时间
|
||||
const opRes2 = await context.operate("oauthToken", {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: "update",
|
||||
data: {
|
||||
revokedAt: new Date()
|
||||
},
|
||||
filter: {
|
||||
oauthUserAuthorization$token: {
|
||||
...filter
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
res += opRes2.oauthToken?.update || 0;
|
||||
}
|
||||
// // 如果是unused并且code的usedAt是空的,则把code的usedAt全部设置为当前时间
|
||||
// const opRes0 = await context.operate("oauthAuthorizationCode", {
|
||||
// id: await generateNewIdAsync(),
|
||||
// action: "update",
|
||||
// data: {
|
||||
// usedAt: new Date()
|
||||
// },
|
||||
// filter: {
|
||||
// usedAt: {
|
||||
// $exists: false
|
||||
// },
|
||||
// oauthUserAuthorization$code: {
|
||||
// ...filter,
|
||||
// // 未被使用肯定就没有tokenId
|
||||
// usageState: 'unused',
|
||||
// }
|
||||
// }
|
||||
// }, {});
|
||||
// res += opRes0.oauthAuthorizationCode?.update || 0;
|
||||
// // 如果没有token,可以直接删除oauthUserAuthorization (可能是复用的之前的token, 也可能是未被使用的授权记录)
|
||||
// const opRes = await context.operate("oauthUserAuthorization", {
|
||||
// id: await generateNewIdAsync(),
|
||||
// action: "remove",
|
||||
// data: {},
|
||||
// filter: {
|
||||
// ...filter,
|
||||
// // 未被使用肯定就没有tokenId
|
||||
// usageState: 'unused',
|
||||
// }
|
||||
// }, {});
|
||||
// res += opRes.oauthApplication?.remove || 0;
|
||||
// // 如果有token,则将token的revokedAt设置为当前时间
|
||||
// const opRes2 = await context.operate("oauthToken", {
|
||||
// id: await generateNewIdAsync(),
|
||||
// action: "update",
|
||||
// data: {
|
||||
// revokedAt: new Date()
|
||||
// },
|
||||
// filter: {
|
||||
// oauthUserAuthorization$token: {
|
||||
// ...filter
|
||||
// }
|
||||
// }
|
||||
// }, {});
|
||||
// res += opRes2.oauthToken?.update || 0;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import assert from "assert";
|
||||
import { generateNewId } from "oak-domain/lib/utils/uuid";
|
||||
import { RowWithActions } from "oak-frontend-base";
|
||||
import { EntityDict } from "../../../oak-app-domain";
|
||||
|
||||
export default OakComponent({
|
||||
entity: 'oauthUserAuthorization',
|
||||
isList: true,
|
||||
projection: {
|
||||
userId: 1,
|
||||
applicationId: 1,
|
||||
authorizedAt: 1,
|
||||
codeId: 1,
|
||||
tokenId: 1,
|
||||
usageState: 1,
|
||||
$$createAt$$: 1,
|
||||
application: {
|
||||
logo: 1,
|
||||
name: 1,
|
||||
description: 1,
|
||||
isConfidential: 1,
|
||||
},
|
||||
code: {
|
||||
scope: 1,
|
||||
},
|
||||
token: {
|
||||
accessExpiresAt: 1,
|
||||
refreshExpiresAt: 1,
|
||||
lastUsedAt: 1,
|
||||
revokedAt: 1,
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
pageSize: 5,
|
||||
currentPage: 1,
|
||||
},
|
||||
filters: [{
|
||||
filter() {
|
||||
const userId = this.features.token.getUserId();
|
||||
const systemId = this.features.application.getApplication()?.systemId!
|
||||
return {
|
||||
userId,
|
||||
application: {
|
||||
systemId: systemId
|
||||
},
|
||||
usageState: {
|
||||
$in: ['granted', 'denied', 'revoked']
|
||||
}
|
||||
}
|
||||
},
|
||||
}],
|
||||
formData({ data }) {
|
||||
return {
|
||||
list: data,
|
||||
};
|
||||
},
|
||||
properties: {},
|
||||
methods: {
|
||||
async revoke(item: RowWithActions<EntityDict, 'oauthUserAuthorization'>) {
|
||||
assert(item.id, 'No id found for this authorization record');
|
||||
|
||||
await this.features.cache.operate("oauthUserAuthorization", {
|
||||
action: "revoke",
|
||||
id: generateNewId(),
|
||||
data: {},
|
||||
filter: {
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Revoking authorization for:', item.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"page_title": "My Authorization Records",
|
||||
"page_description": "Manage access permissions you've granted to third-party applications",
|
||||
"no_records": "No authorization records",
|
||||
"unknown_app": "Unknown Application",
|
||||
"revoke": "Revoke",
|
||||
"revoke_confirm_title": "Confirm Revoke Authorization",
|
||||
"revoke_confirm_content": "Are you sure you want to revoke authorization for '%{appName}'? The application will no longer be able to access your data.",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"status_active": "Active",
|
||||
"status_revoked": "Revoked",
|
||||
"status_denied": "Denied",
|
||||
"status_expired": "Expired",
|
||||
"status_unknown": "Unknown",
|
||||
"authorized_at": "Authorized At",
|
||||
"scope": "Scope",
|
||||
"full_access": "Full Access",
|
||||
"last_used_at": "Last Used",
|
||||
"access_expires_at": "Access Expires At",
|
||||
"revoked_at": "Revoked At",
|
||||
"pagination_total": "Total %{total} items"
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"page_title": "我的授权记录",
|
||||
"page_description": "管理您授权给第三方应用的访问权限",
|
||||
"no_records": "暂无授权记录",
|
||||
"unknown_app": "未知应用",
|
||||
"revoke": "撤销",
|
||||
"revoke_confirm_title": "确认撤销授权",
|
||||
"revoke_confirm_content": "确定要撤销对「%{appName}」的授权吗?撤销后该应用将无法访问您的数据。",
|
||||
"confirm": "确定",
|
||||
"cancel": "取消",
|
||||
"status_active": "已授权",
|
||||
"status_revoked": "已撤销",
|
||||
"status_denied": "已拒绝",
|
||||
"status_expired": "已过期",
|
||||
"status_unknown": "未知状态",
|
||||
"authorized_at": "授权时间",
|
||||
"scope": "授权范围",
|
||||
"full_access": "完全访问",
|
||||
"last_used_at": "最后使用",
|
||||
"access_expires_at": "访问过期时间",
|
||||
"revoked_at": "撤销时间",
|
||||
"pagination_total": "共 %{total} 条"
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 32px;
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.listContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.item {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.appInfo {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.appLogo {
|
||||
flex-shrink: 0;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d1d5db;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.appNameWrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.appName {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.appDescription {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 0 0 16px 60px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.statusActive,
|
||||
.statusRevoked,
|
||||
.statusDenied,
|
||||
.statusExpired,
|
||||
.statusUnknown {
|
||||
font-size: 12px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.statusActive {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.statusRevoked {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.statusDenied {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.statusExpired {
|
||||
background: #e5e7eb;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.statusUnknown {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.revokeButton {
|
||||
padding: 6px 16px;
|
||||
font-size: 14px;
|
||||
color: #dc2626;
|
||||
background: #fff;
|
||||
border: 1px solid #dc2626;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.itemDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
margin-left: 64px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.detailRow {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #6b7280;
|
||||
min-width: 120px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #1f2937;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty p {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { Modal, Avatar, Pagination } from 'antd';
|
||||
import { AppstoreOutlined } from '@ant-design/icons';
|
||||
import Styles from './styles.module.less';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { StringListJson } from '../../../types/datatype';
|
||||
|
||||
const Records = (
|
||||
props: WebComponentProps<
|
||||
EntityDict,
|
||||
'oauthUserAuthorization',
|
||||
true,
|
||||
{
|
||||
list: RowWithActions<EntityDict, 'oauthUserAuthorization'>[];
|
||||
},
|
||||
{
|
||||
revoke: (item: RowWithActions<EntityDict, 'oauthUserAuthorization'>) => void;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
const { list, oakPagination } = props.data;
|
||||
const { pageSize, currentPage, total } = oakPagination || {};
|
||||
const { t, revoke, setCurrentPage, setPageSize } = props.methods;
|
||||
|
||||
const handleRevoke = (item: RowWithActions<EntityDict, 'oauthUserAuthorization'>) => {
|
||||
Modal.confirm({
|
||||
title: t('revoke_confirm_title'),
|
||||
content: t('revoke_confirm_content', { appName: item.application?.name || t('unknown_app') }),
|
||||
okText: t('confirm'),
|
||||
cancelText: t('cancel'),
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => {
|
||||
revoke(item);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const formatDate = (date: string | Date | number | undefined) => {
|
||||
if (!date) return '-';
|
||||
return new Date(date).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
function formateScope(scope: StringListJson | undefined): React.ReactNode {
|
||||
if (!scope || scope.length === 0 || scope.includes('')) {
|
||||
return t('full_access');
|
||||
}
|
||||
return scope.join(', ');
|
||||
}
|
||||
|
||||
const getStatusTag = (item: RowWithActions<EntityDict, 'oauthUserAuthorization'>) => {
|
||||
if (item.token?.revokedAt) {
|
||||
return <span className={Styles.statusRevoked}>{t('status_revoked')}</span>;
|
||||
}
|
||||
if (item.usageState === 'denied') {
|
||||
return <span className={Styles.statusDenied}>{t('status_denied')}</span>;
|
||||
}
|
||||
if (item.token?.refreshExpiresAt && new Date(item.token.refreshExpiresAt) < new Date()) {
|
||||
return <span className={Styles.statusExpired}>{t('status_expired')}</span>;
|
||||
}
|
||||
if (item.usageState === 'granted') {
|
||||
return <span className={Styles.statusActive}>{t('status_active')}</span>;
|
||||
}
|
||||
if (item.usageState === 'revoked') {
|
||||
return <span className={Styles.statusRevoked}>{t('status_revoked')}</span>;
|
||||
}
|
||||
return <span className={Styles.statusUnknown}>{t('status_unknown')}</span>;
|
||||
};
|
||||
|
||||
const isRevokable = (item: RowWithActions<EntityDict, 'oauthUserAuthorization'>) => {
|
||||
return item.usageState === 'granted' && !item.token?.revokedAt;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={Styles.container}>
|
||||
<div className={Styles.header}>
|
||||
<h2>{t('page_title')}</h2>
|
||||
<p className={Styles.description}>{t('page_description')}</p>
|
||||
</div>
|
||||
|
||||
{!list || list.length === 0 ? (
|
||||
<div className={Styles.empty}>
|
||||
<div className={Styles.emptyIcon}>🔐</div>
|
||||
<p>{t('no_records')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={Styles.listContainer}>
|
||||
{list.map((item) => {
|
||||
return (
|
||||
<div key={item.id} className={Styles.item}>
|
||||
<div className={Styles.itemHeader}>
|
||||
<div className={Styles.appInfo}>
|
||||
<Avatar
|
||||
src={item.application?.logo}
|
||||
icon={!item.application?.logo && <AppstoreOutlined />}
|
||||
size={48}
|
||||
shape="square"
|
||||
className={Styles.appLogo}
|
||||
/>
|
||||
<div className={Styles.appNameWrapper}>
|
||||
<h3 className={Styles.appName}>
|
||||
{item.application?.name || t('unknown_app')}
|
||||
</h3>
|
||||
{getStatusTag(item)}
|
||||
</div>
|
||||
</div>
|
||||
{isRevokable(item) && (
|
||||
<button
|
||||
className={Styles.revokeButton}
|
||||
onClick={() => handleRevoke(item)}
|
||||
>
|
||||
{t('revoke')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{item.application?.description && (
|
||||
<p className={Styles.appDescription}>
|
||||
{item.application?.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className={Styles.itemDetails}>
|
||||
<div className={Styles.detailRow}>
|
||||
<span className={Styles.label}>{t('authorized_at')}:</span>
|
||||
<span className={Styles.value}>{formatDate(item.authorizedAt)}</span>
|
||||
</div>
|
||||
|
||||
<div className={Styles.detailRow}>
|
||||
<span className={Styles.label}>{t('scope')}:</span>
|
||||
<span className={Styles.value}>
|
||||
{formateScope(item.code?.scope)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{item.token?.lastUsedAt && (
|
||||
<div className={Styles.detailRow}>
|
||||
<span className={Styles.label}>{t('last_used_at')}:</span>
|
||||
<span className={Styles.value}>{formatDate(item.token.lastUsedAt)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* {item.token?.accessExpiresAt && (
|
||||
<div className={Styles.detailRow}>
|
||||
<span className={Styles.label}>{t('access_expires_at')}:</span>
|
||||
<span className={Styles.value}>{formatDate(item.token.accessExpiresAt)}</span>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{item.token?.revokedAt && (
|
||||
<div className={Styles.detailRow}>
|
||||
<span className={Styles.label}>{t('revoked_at')}:</span>
|
||||
<span className={Styles.value}>{formatDate(item.token.revokedAt)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{total && total > 0 && (
|
||||
<div className={Styles.paginationWrapper}>
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
onChange={(page) => setCurrentPage(page)}
|
||||
onShowSizeChange={(current, size) => {
|
||||
setPageSize(size);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
showTotal={(total) => t('pagination_total', { total })}
|
||||
pageSizeOptions={['5' ,'10', '20', '50', '100']}
|
||||
className={Styles.pagination}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Records;
|
||||
|
|
@ -446,6 +446,36 @@ const i18ns: I18n[] = [
|
|||
"refreshEndpointPlaceholder": "请输入刷新端点URL"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "0b72f69f7fe7160fb6871917d674c12b",
|
||||
namespace: "oak-general-business-c-oauth-records",
|
||||
language: "zh-CN",
|
||||
module: "oak-general-business",
|
||||
position: "src/components/oauth/records",
|
||||
data: {
|
||||
"page_title": "我的授权记录",
|
||||
"page_description": "管理您授权给第三方应用的访问权限",
|
||||
"no_records": "暂无授权记录",
|
||||
"unknown_app": "未知应用",
|
||||
"revoke": "撤销",
|
||||
"revoke_confirm_title": "确认撤销授权",
|
||||
"revoke_confirm_content": "确定要撤销对「%{appName}」的授权吗?撤销后该应用将无法访问您的数据。",
|
||||
"confirm": "确定",
|
||||
"cancel": "取消",
|
||||
"status_active": "已授权",
|
||||
"status_revoked": "已撤销",
|
||||
"status_denied": "已拒绝",
|
||||
"status_expired": "已过期",
|
||||
"status_unknown": "未知状态",
|
||||
"authorized_at": "授权时间",
|
||||
"scope": "授权范围",
|
||||
"full_access": "完全访问",
|
||||
"last_used_at": "最后使用",
|
||||
"access_expires_at": "访问过期时间",
|
||||
"revoked_at": "撤销时间",
|
||||
"pagination_total": "共 %{total} 条"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "9bdbb9993789ecd48e7b0e10b7117620",
|
||||
namespace: "oak-general-business-c-passport",
|
||||
|
|
|
|||
|
|
@ -14,58 +14,116 @@ const triggers = [
|
|||
const { filter } = operation;
|
||||
assert(filter, 'No filter found in revoke operation');
|
||||
|
||||
const datas = await context.select("oauthUserAuthorization", {
|
||||
data: {
|
||||
userId: 1,
|
||||
applicationId: 1,
|
||||
tokenId: 1,
|
||||
codeId: 1,
|
||||
usageState: 1,
|
||||
},
|
||||
filter: { ...filter },
|
||||
}, {})
|
||||
|
||||
let res = 0;
|
||||
|
||||
// 如果是unused并且code的usedAt是空的,则把code的usedAt全部设置为当前时间
|
||||
const opRes0 = await context.operate("oauthAuthorizationCode", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: "update",
|
||||
data: {
|
||||
usedAt: new Date()
|
||||
},
|
||||
filter: {
|
||||
usedAt: {
|
||||
$exists: false
|
||||
for (const data of datas) {
|
||||
// 如果是unused并且code的usedAt是空的,则把code的usedAt全部设置为当前时间
|
||||
const opRes0 = await context.operate("oauthAuthorizationCode", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: "update",
|
||||
data: {
|
||||
usedAt: new Date()
|
||||
},
|
||||
oauthUserAuthorization$code: {
|
||||
...filter,
|
||||
// 未被使用肯定就没有tokenId
|
||||
usageState: 'unused',
|
||||
filter: {
|
||||
usedAt: {
|
||||
$exists: false
|
||||
},
|
||||
// 某一个用户对某一个应用的授权记录
|
||||
oauthAppId: data.applicationId,
|
||||
userId: data.userId,
|
||||
}
|
||||
}, {});
|
||||
res += opRes0.oauthAuthorizationCode?.update || 0;
|
||||
|
||||
// // 如果没有token,可以直接删除oauthUserAuthorization (可能是复用的之前的token, 也可能是未被使用的授权记录)
|
||||
if (data.usageState === 'unused' && !data.tokenId) {
|
||||
const opRes = await context.operate("oauthUserAuthorization", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: "remove",
|
||||
data: {},
|
||||
filter: {
|
||||
id: data.id,
|
||||
}
|
||||
}, {});
|
||||
res += opRes.oauthApplication?.remove || 0;
|
||||
}
|
||||
}, {});
|
||||
|
||||
res += opRes0.oauthAuthorizationCode?.update || 0;
|
||||
|
||||
// 如果没有token,可以直接删除oauthUserAuthorization (可能是复用的之前的token, 也可能是未被使用的授权记录)
|
||||
const opRes = await context.operate("oauthUserAuthorization", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: "remove",
|
||||
data: {},
|
||||
filter: {
|
||||
...filter,
|
||||
// 未被使用肯定就没有tokenId
|
||||
usageState: 'unused',
|
||||
// 如果有token,则将token的revokedAt设置为当前时间
|
||||
if (data.tokenId) {
|
||||
const opRes2 = await context.operate("oauthToken", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: "update",
|
||||
data: {
|
||||
revokedAt: new Date()
|
||||
},
|
||||
filter: {
|
||||
id: data.tokenId
|
||||
}
|
||||
}, {});
|
||||
res += opRes2.oauthToken?.update || 0;
|
||||
}
|
||||
}, {});
|
||||
}
|
||||
|
||||
res += opRes.oauthApplication?.remove || 0;
|
||||
// // 如果是unused并且code的usedAt是空的,则把code的usedAt全部设置为当前时间
|
||||
// const opRes0 = await context.operate("oauthAuthorizationCode", {
|
||||
// id: await generateNewIdAsync(),
|
||||
// action: "update",
|
||||
// data: {
|
||||
// usedAt: new Date()
|
||||
// },
|
||||
// filter: {
|
||||
// usedAt: {
|
||||
// $exists: false
|
||||
// },
|
||||
// oauthUserAuthorization$code: {
|
||||
// ...filter,
|
||||
// // 未被使用肯定就没有tokenId
|
||||
// usageState: 'unused',
|
||||
// }
|
||||
// }
|
||||
// }, {});
|
||||
|
||||
// 如果有token,则将token的revokedAt设置为当前时间
|
||||
const opRes2 = await context.operate("oauthToken", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: "update",
|
||||
data: {
|
||||
revokedAt: new Date()
|
||||
},
|
||||
filter: {
|
||||
oauthUserAuthorization$token: {
|
||||
...filter
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
// res += opRes0.oauthAuthorizationCode?.update || 0;
|
||||
|
||||
res += opRes2.oauthToken?.update || 0;
|
||||
// // 如果没有token,可以直接删除oauthUserAuthorization (可能是复用的之前的token, 也可能是未被使用的授权记录)
|
||||
// const opRes = await context.operate("oauthUserAuthorization", {
|
||||
// id: await generateNewIdAsync(),
|
||||
// action: "remove",
|
||||
// data: {},
|
||||
// filter: {
|
||||
// ...filter,
|
||||
// // 未被使用肯定就没有tokenId
|
||||
// usageState: 'unused',
|
||||
// }
|
||||
// }, {});
|
||||
|
||||
// res += opRes.oauthApplication?.remove || 0;
|
||||
|
||||
// // 如果有token,则将token的revokedAt设置为当前时间
|
||||
// const opRes2 = await context.operate("oauthToken", {
|
||||
// id: await generateNewIdAsync(),
|
||||
// action: "update",
|
||||
// data: {
|
||||
// revokedAt: new Date()
|
||||
// },
|
||||
// filter: {
|
||||
// oauthUserAuthorization$token: {
|
||||
// ...filter
|
||||
// }
|
||||
// }
|
||||
// }, {});
|
||||
|
||||
// res += opRes2.oauthToken?.update || 0;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue