mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 22:53:25 +00:00
feat: add user token management
This commit is contained in:
32
app/user/controllers/api_token_controller.ts
Normal file
32
app/user/controllers/api_token_controller.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ApiTokenService } from '#user/services/api_token_service';
|
||||||
|
import { createApiTokenValidator } from '#user/validators/token/create_api_token';
|
||||||
|
import { deleteApiTokenValidator } from '#user/validators/token/delete_api_token';
|
||||||
|
import { inject } from '@adonisjs/core';
|
||||||
|
import { HttpContext } from '@adonisjs/core/http';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
@inject()
|
||||||
|
export default class ApiTokenController {
|
||||||
|
constructor(private apiTokenService: ApiTokenService) {}
|
||||||
|
|
||||||
|
async store({ request, response, auth }: HttpContext) {
|
||||||
|
const { name, expiresAt } = await request.validateUsing(
|
||||||
|
createApiTokenValidator
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.apiTokenService.createToken({
|
||||||
|
user: auth.user!,
|
||||||
|
name,
|
||||||
|
expiresAt: expiresAt ? DateTime.fromJSDate(expiresAt) : undefined,
|
||||||
|
});
|
||||||
|
return response.redirect().withQs().back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy({ request, response, auth }: HttpContext) {
|
||||||
|
const { params } = await request.validateUsing(deleteApiTokenValidator);
|
||||||
|
const tokenId = params.tokenId;
|
||||||
|
|
||||||
|
await this.apiTokenService.revokeToken(tokenId, auth.user!.id);
|
||||||
|
return response.redirect().withQs().back();
|
||||||
|
}
|
||||||
|
}
|
||||||
52
app/user/models/api_token.ts
Normal file
52
app/user/models/api_token.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import AppBaseModel from '#core/models/app_base_model';
|
||||||
|
import User from '#user/models/user';
|
||||||
|
import { beforeSave, belongsTo, column } from '@adonisjs/lucid/orm';
|
||||||
|
import type { BelongsTo } from '@adonisjs/lucid/types/relations';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { randomBytes } from 'node:crypto';
|
||||||
|
|
||||||
|
export default class ApiToken extends AppBaseModel {
|
||||||
|
@column()
|
||||||
|
declare userId: number;
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare name: string;
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare token: string;
|
||||||
|
|
||||||
|
@column.dateTime()
|
||||||
|
declare lastUsedAt: DateTime | null;
|
||||||
|
|
||||||
|
@column.dateTime()
|
||||||
|
declare expiresAt: DateTime | null;
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare isActive: boolean;
|
||||||
|
|
||||||
|
@belongsTo(() => User, {
|
||||||
|
foreignKey: 'userId',
|
||||||
|
})
|
||||||
|
declare user: BelongsTo<typeof User>;
|
||||||
|
|
||||||
|
isExpired(): boolean {
|
||||||
|
if (!this.expiresAt) return false;
|
||||||
|
return DateTime.now() > this.expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid(): boolean {
|
||||||
|
return this.isActive && !this.isExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAsUsed(): Promise<void> {
|
||||||
|
this.lastUsedAt = DateTime.now();
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
@beforeSave()
|
||||||
|
static async generateToken(token: ApiToken) {
|
||||||
|
if (!token.token) {
|
||||||
|
token.token = randomBytes(32).toString('hex');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import AppBaseModel from '#core/models/app_base_model';
|
|||||||
import Link from '#links/models/link';
|
import Link from '#links/models/link';
|
||||||
import { type DisplayPreferences } from '#shared/types/index';
|
import { type DisplayPreferences } from '#shared/types/index';
|
||||||
import { ensureDisplayPreferences } from '#user/lib/index';
|
import { ensureDisplayPreferences } from '#user/lib/index';
|
||||||
|
import ApiToken from '#user/models/api_token';
|
||||||
import type { GoogleToken } from '@adonisjs/ally/types';
|
import type { GoogleToken } from '@adonisjs/ally/types';
|
||||||
import { column, computed, hasMany } from '@adonisjs/lucid/orm';
|
import { column, computed, hasMany } from '@adonisjs/lucid/orm';
|
||||||
import type { HasMany } from '@adonisjs/lucid/types/relations';
|
import type { HasMany } from '@adonisjs/lucid/types/relations';
|
||||||
@@ -43,6 +44,11 @@ export default class User extends AppBaseModel {
|
|||||||
})
|
})
|
||||||
declare links: HasMany<typeof Link>;
|
declare links: HasMany<typeof Link>;
|
||||||
|
|
||||||
|
@hasMany(() => ApiToken, {
|
||||||
|
foreignKey: 'userId',
|
||||||
|
})
|
||||||
|
declare apiTokens: HasMany<typeof ApiToken>;
|
||||||
|
|
||||||
@computed()
|
@computed()
|
||||||
get fullname() {
|
get fullname() {
|
||||||
return this.nickName || this.name;
|
return this.nickName || this.name;
|
||||||
|
|||||||
15
app/user/routes/api_token_routes.ts
Normal file
15
app/user/routes/api_token_routes.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { middleware } from '#start/kernel';
|
||||||
|
import router from '@adonisjs/core/services/router';
|
||||||
|
|
||||||
|
const ApiTokenController = () =>
|
||||||
|
import('#user/controllers/api_token_controller');
|
||||||
|
|
||||||
|
router
|
||||||
|
.group(() => {
|
||||||
|
router.post('/', [ApiTokenController, 'store']).as('user.api-tokens.store');
|
||||||
|
router
|
||||||
|
.delete('/:tokenId', [ApiTokenController, 'destroy'])
|
||||||
|
.as('user.api-tokens.destroy');
|
||||||
|
})
|
||||||
|
.prefix('/user/api-tokens')
|
||||||
|
.middleware([middleware.auth()]);
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
|
import './api_token_routes.js';
|
||||||
import './user_display_preferences_route.js';
|
import './user_display_preferences_route.js';
|
||||||
import './user_theme_route.js';
|
import './user_theme_route.js';
|
||||||
|
|||||||
54
app/user/services/api_token_service.ts
Normal file
54
app/user/services/api_token_service.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import ApiToken from '#user/models/api_token';
|
||||||
|
import User from '#user/models/user';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
type CreateApiTokenPayload = {
|
||||||
|
user: User;
|
||||||
|
name: string;
|
||||||
|
expiresAt?: DateTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ApiTokenService {
|
||||||
|
async createToken({
|
||||||
|
user,
|
||||||
|
name,
|
||||||
|
expiresAt,
|
||||||
|
}: CreateApiTokenPayload): Promise<ApiToken> {
|
||||||
|
return await ApiToken.create({
|
||||||
|
userId: user.id,
|
||||||
|
name,
|
||||||
|
expiresAt,
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserTokens(userId: number): Promise<ApiToken[]> {
|
||||||
|
return await ApiToken.query()
|
||||||
|
.where('userId', userId)
|
||||||
|
.orderBy('created_at', 'desc');
|
||||||
|
}
|
||||||
|
|
||||||
|
async revokeToken(tokenId: number, userId: number): Promise<void> {
|
||||||
|
const token = await ApiToken.query()
|
||||||
|
.where('id', tokenId)
|
||||||
|
.where('userId', userId)
|
||||||
|
.firstOrFail();
|
||||||
|
|
||||||
|
token.isActive = false;
|
||||||
|
await token.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateToken(tokenString: string): Promise<ApiToken | null> {
|
||||||
|
const token = await ApiToken.query()
|
||||||
|
.where('token', tokenString)
|
||||||
|
.where('isActive', true)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!token || !token.isValid()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await token.markAsUsed();
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
app/user/validators/token/create_api_token.ts
Normal file
8
app/user/validators/token/create_api_token.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import vine from '@vinejs/vine';
|
||||||
|
|
||||||
|
export const createApiTokenValidator = vine.compile(
|
||||||
|
vine.object({
|
||||||
|
name: vine.string().trim().minLength(1).maxLength(255),
|
||||||
|
expiresAt: vine.date().optional(),
|
||||||
|
})
|
||||||
|
);
|
||||||
9
app/user/validators/token/delete_api_token.ts
Normal file
9
app/user/validators/token/delete_api_token.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import vine from '@vinejs/vine';
|
||||||
|
|
||||||
|
export const deleteApiTokenValidator = vine.compile(
|
||||||
|
vine.object({
|
||||||
|
params: vine.object({
|
||||||
|
tokenId: vine.number().positive(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
|
import { ApiTokenService } from '#user/services/api_token_service';
|
||||||
|
import { inject } from '@adonisjs/core';
|
||||||
import { HttpContext } from '@adonisjs/core/http';
|
import { HttpContext } from '@adonisjs/core/http';
|
||||||
|
|
||||||
|
@inject()
|
||||||
export default class ShowUserSettingsController {
|
export default class ShowUserSettingsController {
|
||||||
|
constructor(private apiTokenService: ApiTokenService) {}
|
||||||
|
|
||||||
public async render({ auth, inertia }: HttpContext) {
|
public async render({ auth, inertia }: HttpContext) {
|
||||||
const user = await auth.authenticate();
|
const user = await auth.authenticate();
|
||||||
|
const tokens = await this.apiTokenService.getUserTokens(user.id);
|
||||||
return inertia.render('user_settings/show', {
|
return inertia.render('user_settings/show', {
|
||||||
user,
|
user,
|
||||||
|
tokens,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
database/migrations/1755640100587_create_api_tokens_table.ts
Normal file
35
database/migrations/1755640100587_create_api_tokens_table.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { defaultTableFields } from '#database/default_table_fields';
|
||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema';
|
||||||
|
|
||||||
|
export default class CreateApiTokensTable extends BaseSchema {
|
||||||
|
static tableName = 'api_tokens';
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
const exists = await this.schema.hasTable(CreateApiTokensTable.tableName);
|
||||||
|
if (exists) {
|
||||||
|
return console.warn(
|
||||||
|
`Table ${CreateApiTokensTable.tableName} already exists.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.schema.createTable(CreateApiTokensTable.tableName, (table) => {
|
||||||
|
table
|
||||||
|
.integer('user_id')
|
||||||
|
.unsigned()
|
||||||
|
.references('id')
|
||||||
|
.inTable('users')
|
||||||
|
.onDelete('CASCADE');
|
||||||
|
table.string('name', 255).notNullable();
|
||||||
|
table.string('token', 255).notNullable().unique();
|
||||||
|
table.timestamp('last_used_at').nullable();
|
||||||
|
table.timestamp('expires_at').nullable();
|
||||||
|
table.boolean('is_active').defaultTo(true).notNullable();
|
||||||
|
|
||||||
|
defaultTableFields(table);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
this.schema.dropTable(CreateApiTokensTable.tableName);
|
||||||
|
}
|
||||||
|
}
|
||||||
159
inertia/components/common/api_tokens/api_tokens.tsx
Normal file
159
inertia/components/common/api_tokens/api_tokens.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CopyButton,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { modals } from '@mantine/modals';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { TbCheck, TbCopy, TbPlus, TbTrash } from 'react-icons/tb';
|
||||||
|
import { useApiTokens } from '~/hooks/use_api_tokens';
|
||||||
|
import { CreateTokenModal } from './create_token_modal';
|
||||||
|
|
||||||
|
export function ApiTokens() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { tokens, createToken, revokeToken } = useApiTokens();
|
||||||
|
|
||||||
|
const handleCreateTokenModal = () => {
|
||||||
|
modals.open({
|
||||||
|
title: t('api-tokens.create-new'),
|
||||||
|
children: (
|
||||||
|
<CreateTokenModal
|
||||||
|
onCreate={(name) => createToken(name)}
|
||||||
|
onClose={() => modals.closeAll()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRevokeToken = async (tokenId: number, tokenName: string) => {
|
||||||
|
modals.openConfirmModal({
|
||||||
|
title: t('api-tokens.revoke'),
|
||||||
|
children: (
|
||||||
|
<Text size="sm">
|
||||||
|
{t('api-tokens.confirm-revoke')} <strong>{tokenName}</strong>?
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
labels: {
|
||||||
|
confirm: t('api-tokens.revoke'),
|
||||||
|
cancel: t('common.cancel'),
|
||||||
|
},
|
||||||
|
confirmProps: { color: 'red' },
|
||||||
|
onConfirm: () => revokeToken(tokenId),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string | null) => {
|
||||||
|
if (!dateString) return t('api-tokens.never');
|
||||||
|
return DateTime.fromISO(dateString).toRelative();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isExpired = (expiresAt: string | null) => {
|
||||||
|
if (!expiresAt) return false;
|
||||||
|
return DateTime.fromISO(expiresAt) < DateTime.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card withBorder>
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Text fw={500}>{t('api-tokens.title')}</Text>
|
||||||
|
<Button
|
||||||
|
leftSection={<TbPlus size={16} />}
|
||||||
|
onClick={handleCreateTokenModal}
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
>
|
||||||
|
{t('api-tokens.create')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack gap="sm">
|
||||||
|
{tokens.length === 0 ? (
|
||||||
|
<Text c="dimmed" ta="center" py="xl">
|
||||||
|
{t('api-tokens.no-tokens')}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
tokens.map((token) => (
|
||||||
|
<Card
|
||||||
|
key={token.id}
|
||||||
|
withBorder
|
||||||
|
p="sm"
|
||||||
|
opacity={token.isActive ? 1 : 0.5}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="flex-start">
|
||||||
|
<Stack gap="xs" style={{ flex: 1 }}>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text fw={500}>{token.name}</Text>
|
||||||
|
{isExpired(token.expiresAt) && (
|
||||||
|
<Badge color="red" variant="light" size="xs">
|
||||||
|
{t('api-tokens.expired')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{!token.isActive && (
|
||||||
|
<Badge color="gray" variant="light" size="xs">
|
||||||
|
{t('api-tokens.revoked')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t('api-tokens.created')}: {formatDate(token.createdAt)}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t('api-tokens.last-used')}: {formatDate(token.lastUsedAt)}
|
||||||
|
</Text>
|
||||||
|
{token.expiresAt && (
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t('api-tokens.expires')}: {formatDate(token.expiresAt)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
{token.isActive && (
|
||||||
|
<Group gap="xs">
|
||||||
|
<CopyButton value={token.token} timeout={2000}>
|
||||||
|
{({ copied, copy }) => (
|
||||||
|
<Tooltip
|
||||||
|
label={
|
||||||
|
copied
|
||||||
|
? t('api-tokens.copied')
|
||||||
|
: t('api-tokens.copy')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
color={copied ? 'teal' : 'blue'}
|
||||||
|
onClick={copy}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<TbCheck size={16} />
|
||||||
|
) : (
|
||||||
|
<TbCopy size={16} />
|
||||||
|
)}
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</CopyButton>
|
||||||
|
<Tooltip label={t('api-tokens.revoke')}>
|
||||||
|
<ActionIcon
|
||||||
|
color="red"
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => handleRevokeToken(token.id, token.name)}
|
||||||
|
>
|
||||||
|
<TbTrash size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
inertia/components/common/api_tokens/create_token_modal.tsx
Normal file
53
inertia/components/common/api_tokens/create_token_modal.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Button, Group, Stack, Text, TextInput } from '@mantine/core';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface CreateTokenModalProps {
|
||||||
|
onCreate: (name: string) => Promise<void>;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateTokenModal({ onCreate, onClose }: CreateTokenModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [tokenName, setTokenName] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
if (!tokenName.trim()) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await onCreate(tokenName);
|
||||||
|
onClose();
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('api-tokens.create-description')}
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
label={t('api-tokens.name')}
|
||||||
|
placeholder={t('api-tokens.name-placeholder')}
|
||||||
|
value={tokenName}
|
||||||
|
onChange={(e) => setTokenName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Button variant="subtle" onClick={onClose}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreate}
|
||||||
|
disabled={!tokenName.trim() || isLoading}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
{t('api-tokens.create')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,6 +32,9 @@ export function UserDropdown() {
|
|||||||
alt={auth.user?.fullname}
|
alt={auth.user?.fullname}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
size={20}
|
size={20}
|
||||||
|
imageProps={{
|
||||||
|
referrerPolicy: 'no-referrer',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Text fw={500} size="sm" lh={1} mr={3}>
|
<Text fw={500} size="sm" lh={1} mr={3}>
|
||||||
{auth.user?.fullname}
|
{auth.user?.fullname}
|
||||||
|
|||||||
33
inertia/hooks/use_api_tokens.ts
Normal file
33
inertia/hooks/use_api_tokens.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { router, usePage } from '@inertiajs/react';
|
||||||
|
|
||||||
|
interface ApiToken {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
token: string;
|
||||||
|
lastUsedAt: string | null;
|
||||||
|
expiresAt: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApiTokens() {
|
||||||
|
const {
|
||||||
|
props: { tokens },
|
||||||
|
} = usePage<{
|
||||||
|
tokens: ApiToken[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const createToken = async (name: string, expiresAt?: Date) => {
|
||||||
|
return router.post('/user/api-tokens', { name, expiresAt });
|
||||||
|
};
|
||||||
|
|
||||||
|
const revokeToken = async (tokenId: number) => {
|
||||||
|
return router.delete(`/user/api-tokens/${tokenId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
tokens,
|
||||||
|
createToken,
|
||||||
|
revokeToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -94,5 +94,26 @@
|
|||||||
"list": "List",
|
"list": "List",
|
||||||
"grid": "Grid"
|
"grid": "Grid"
|
||||||
},
|
},
|
||||||
"coming-soon": "Under development"
|
"coming-soon": "Under development",
|
||||||
|
"api-tokens": {
|
||||||
|
"title": "API Tokens",
|
||||||
|
"create": "Create token",
|
||||||
|
"create-new": "Create new token",
|
||||||
|
"create-description": "This token will be used for browser extension authentication.",
|
||||||
|
"name": "Token name",
|
||||||
|
"name-placeholder": "Ex: Chrome Extension",
|
||||||
|
"token-created": "Token created successfully",
|
||||||
|
"token-created-description": "Copy this token and keep it safe. It will not be displayed again after closing this window.",
|
||||||
|
"revoke": "Revoke",
|
||||||
|
"confirm-revoke": "Are you sure you want to revoke this token?",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copied": "Copied",
|
||||||
|
"no-tokens": "No tokens created",
|
||||||
|
"expired": "Expired",
|
||||||
|
"revoked": "Revoked",
|
||||||
|
"created": "Created",
|
||||||
|
"last-used": "Last used",
|
||||||
|
"expires": "Expires",
|
||||||
|
"never": "Never"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,5 +94,26 @@
|
|||||||
"list": "Liste",
|
"list": "Liste",
|
||||||
"grid": "Grille"
|
"grid": "Grille"
|
||||||
},
|
},
|
||||||
"coming-soon": "En cours de développement"
|
"coming-soon": "En cours de développement",
|
||||||
|
"api-tokens": {
|
||||||
|
"title": "Tokens API",
|
||||||
|
"create": "Créer un token",
|
||||||
|
"create-new": "Créer un nouveau token",
|
||||||
|
"create-description": "Ce token sera utilisé pour l'authentification de votre extension navigateur.",
|
||||||
|
"name": "Nom du token",
|
||||||
|
"name-placeholder": "Ex: Extension Chrome",
|
||||||
|
"token-created": "Token créé avec succès",
|
||||||
|
"token-created-description": "Copiez ce token et conservez-le en lieu sûr. Il ne sera plus affiché après fermeture de cette fenêtre.",
|
||||||
|
"revoke": "Révoquer",
|
||||||
|
"confirm-revoke": "Êtes-vous sûr de vouloir révoquer ce token ?",
|
||||||
|
"copy": "Copier",
|
||||||
|
"copied": "Copié",
|
||||||
|
"no-tokens": "Aucun token créé",
|
||||||
|
"expired": "Expiré",
|
||||||
|
"revoked": "Révoqué",
|
||||||
|
"created": "Créé",
|
||||||
|
"last-used": "Dernière utilisation",
|
||||||
|
"expires": "Expire",
|
||||||
|
"never": "Jamais"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ApiTokens } from '~/components/common/api_tokens/api_tokens';
|
||||||
import {
|
import {
|
||||||
FloatingTab,
|
FloatingTab,
|
||||||
FloatingTabs,
|
FloatingTabs,
|
||||||
@@ -14,6 +15,11 @@ function UserSettingsShow() {
|
|||||||
value: 'preferences',
|
value: 'preferences',
|
||||||
content: <UserPreferences />,
|
content: <UserPreferences />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t('api-tokens.title'),
|
||||||
|
value: 'api-tokens',
|
||||||
|
content: <ApiTokens />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
return <FloatingTabs tabs={tabs} />;
|
return <FloatingTabs tabs={tabs} />;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user