mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 22:53:25 +00:00
refactor: use adonis's access tokens instead of creating custom (and unsecured) logic
This commit is contained in:
@@ -3,21 +3,23 @@ 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) {
|
||||
async store({ request, response, auth, session }: HttpContext) {
|
||||
const { name, expiresAt } = await request.validateUsing(
|
||||
createApiTokenValidator
|
||||
);
|
||||
|
||||
await this.apiTokenService.createToken({
|
||||
user: auth.user!,
|
||||
const token = await this.apiTokenService.createToken(auth.user!, {
|
||||
name,
|
||||
expiresAt: expiresAt ? DateTime.fromJSDate(expiresAt) : undefined,
|
||||
expiresAt,
|
||||
});
|
||||
session.flash('token', {
|
||||
...token.toJSON(),
|
||||
token: token.value?.release(),
|
||||
identifier: token.identifier,
|
||||
});
|
||||
return response.redirect().withQs().back();
|
||||
}
|
||||
@@ -26,7 +28,18 @@ export default class ApiTokenController {
|
||||
const { params } = await request.validateUsing(deleteApiTokenValidator);
|
||||
const tokenId = params.tokenId;
|
||||
|
||||
await this.apiTokenService.revokeToken(tokenId, auth.user!.id);
|
||||
const token = await this.apiTokenService.getTokenByValue(
|
||||
auth.user!,
|
||||
tokenId
|
||||
);
|
||||
if (!token) {
|
||||
return response.notFound();
|
||||
}
|
||||
|
||||
await this.apiTokenService.revokeToken(
|
||||
auth.user!,
|
||||
Number(token.identifier)
|
||||
);
|
||||
return response.redirect().withQs().back();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
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,8 +3,8 @@ 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 { DbAccessTokensProvider } from '@adonisjs/auth/access_tokens';
|
||||
import { column, computed, hasMany } from '@adonisjs/lucid/orm';
|
||||
import type { HasMany } from '@adonisjs/lucid/types/relations';
|
||||
import { DateTime } from 'luxon';
|
||||
@@ -44,11 +44,6 @@ 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;
|
||||
@@ -70,4 +65,6 @@ export default class User extends AppBaseModel {
|
||||
prepare: (value) => JSON.stringify(value),
|
||||
})
|
||||
declare displayPreferences: DisplayPreferences;
|
||||
|
||||
static accessTokens = DbAccessTokensProvider.forModel(User);
|
||||
}
|
||||
|
||||
@@ -1,54 +1,33 @@
|
||||
import ApiToken from '#user/models/api_token';
|
||||
import User from '#user/models/user';
|
||||
import { DateTime } from 'luxon';
|
||||
import { AccessToken } from '@adonisjs/auth/access_tokens';
|
||||
|
||||
type CreateApiTokenPayload = {
|
||||
user: User;
|
||||
type CreateTokenParams = {
|
||||
name: string;
|
||||
expiresAt?: DateTime;
|
||||
expiresAt?: Date;
|
||||
};
|
||||
|
||||
export class ApiTokenService {
|
||||
async createToken({
|
||||
user,
|
||||
name,
|
||||
expiresAt,
|
||||
}: CreateApiTokenPayload): Promise<ApiToken> {
|
||||
return await ApiToken.create({
|
||||
userId: user.id,
|
||||
createToken(user: User, { name, expiresAt }: CreateTokenParams) {
|
||||
const expiresIn = expiresAt ? expiresAt.getTime() - Date.now() : undefined;
|
||||
return User.accessTokens.create(user, undefined, {
|
||||
name,
|
||||
expiresAt,
|
||||
isActive: true,
|
||||
expiresIn,
|
||||
});
|
||||
}
|
||||
|
||||
async getUserTokens(userId: number): Promise<ApiToken[]> {
|
||||
return await ApiToken.query()
|
||||
.where('userId', userId)
|
||||
.orderBy('created_at', 'desc');
|
||||
getTokens(user: User) {
|
||||
return User.accessTokens.all(user);
|
||||
}
|
||||
|
||||
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();
|
||||
revokeToken(user: User, identifier: number) {
|
||||
return User.accessTokens.delete(user, identifier);
|
||||
}
|
||||
|
||||
async validateToken(tokenString: string): Promise<ApiToken | null> {
|
||||
const token = await ApiToken.query()
|
||||
.where('token', tokenString)
|
||||
.where('isActive', true)
|
||||
.first();
|
||||
validateToken(token: AccessToken) {
|
||||
return User.accessTokens.verify(token.value!);
|
||||
}
|
||||
|
||||
if (!token || !token.isValid()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await token.markAsUsed();
|
||||
return token;
|
||||
getTokenByValue(user: User, value: string) {
|
||||
return User.accessTokens.find(user, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import vine from '@vinejs/vine';
|
||||
export const deleteApiTokenValidator = vine.compile(
|
||||
vine.object({
|
||||
params: vine.object({
|
||||
tokenId: vine.number().positive(),
|
||||
tokenId: vine.string().trim().minLength(1).maxLength(255),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -8,10 +8,15 @@ export default class ShowUserSettingsController {
|
||||
|
||||
public async render({ auth, inertia }: HttpContext) {
|
||||
const user = await auth.authenticate();
|
||||
const tokens = await this.apiTokenService.getUserTokens(user.id);
|
||||
const tokens = await this.apiTokenService.getTokens(user);
|
||||
return inertia.render('user_settings/show', {
|
||||
user,
|
||||
tokens,
|
||||
tokens: tokens.map((token) => {
|
||||
return {
|
||||
...token.toJSON(),
|
||||
identifier: token.identifier,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user