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 { type DisplayPreferences } from '#shared/types/index';
|
||||
import { ensureDisplayPreferences } from '#user/lib/index';
|
||||
import ApiToken from '#user/models/api_token';
|
||||
import type { GoogleToken } from '@adonisjs/ally/types';
|
||||
import { column, computed, hasMany } from '@adonisjs/lucid/orm';
|
||||
import type { HasMany } from '@adonisjs/lucid/types/relations';
|
||||
@@ -43,6 +44,11 @@ export default class User extends AppBaseModel {
|
||||
})
|
||||
declare links: HasMany<typeof Link>;
|
||||
|
||||
@hasMany(() => ApiToken, {
|
||||
foreignKey: 'userId',
|
||||
})
|
||||
declare apiTokens: HasMany<typeof ApiToken>;
|
||||
|
||||
@computed()
|
||||
get fullname() {
|
||||
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_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';
|
||||
|
||||
@inject()
|
||||
export default class ShowUserSettingsController {
|
||||
constructor(private apiTokenService: ApiTokenService) {}
|
||||
|
||||
public async render({ auth, inertia }: HttpContext) {
|
||||
const user = await auth.authenticate();
|
||||
const tokens = await this.apiTokenService.getUserTokens(user.id);
|
||||
return inertia.render('user_settings/show', {
|
||||
user,
|
||||
tokens,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user