feat: add api controllers and routes for browser extension

This commit is contained in:
Sonny
2025-08-28 16:44:31 +02:00
parent 9aa71dad30
commit 208f2c631f
28 changed files with 353 additions and 5 deletions

View File

@@ -0,0 +1,20 @@
import { CollectionService } from '#collections/services/collection_service';
import { createCollectionValidator } from '#collections/validators/create_collection_validator';
import { inject } from '@adonisjs/core';
import { type HttpContext } from '@adonisjs/core/http';
@inject()
export default class CreateCollectionController {
constructor(private collectionService: CollectionService) {}
async execute({ request, response }: HttpContext) {
console.log('avant');
const payload = await request.validateUsing(createCollectionValidator);
const collection = await this.collectionService.createCollection(payload);
console.log('après', collection);
return response.json({
message: 'Collection created successfully',
collection: collection.serialize(),
});
}
}

View File

@@ -0,0 +1,17 @@
import { CollectionService } from '#collections/services/collection_service';
import { deleteCollectionValidator } from '#collections/validators/delete_collection_validator';
import { inject } from '@adonisjs/core';
import { HttpContext } from '@adonisjs/core/http';
@inject()
export default class DeleteCollectionController {
constructor(private collectionService: CollectionService) {}
async execute({ request, response }: HttpContext) {
const { params } = await request.validateUsing(deleteCollectionValidator);
await this.collectionService.deleteCollection(params.id);
return response.json({
message: 'Collection deleted successfully',
});
}
}

View File

@@ -0,0 +1,16 @@
import { CollectionService } from '#collections/services/collection_service';
import { inject } from '@adonisjs/core';
import { HttpContext } from '@adonisjs/core/http';
@inject()
export default class GetCollectionsController {
constructor(private collectionService: CollectionService) {}
async show({ response }: HttpContext) {
const collections =
await this.collectionService.getCollectionsForAuthenticatedUser();
return response.json({
collections: collections.map((collection) => collection.serialize()),
});
}
}

View File

@@ -0,0 +1,21 @@
import { CollectionService } from '#collections/services/collection_service';
import { updateCollectionValidator } from '#collections/validators/update_collection_validator';
import { inject } from '@adonisjs/core';
import { HttpContext } from '@adonisjs/core/http';
@inject()
export default class UpdateCollectionController {
constructor(private collectionService: CollectionService) {}
async execute({ request, response }: HttpContext) {
const {
params: { id: collectionId },
...payload
} = await request.validateUsing(updateCollectionValidator);
await this.collectionService.updateCollection(collectionId, payload);
return response.json({
message: 'Collection updated successfully',
});
}
}

View File

@@ -0,0 +1,14 @@
import { middleware } from '#start/kernel';
import router from '@adonisjs/core/services/router';
const CreateCollectionsController = () =>
import('#api/collections/controllers/create_collection_controller');
router
.group(() => {
router
.post('', [CreateCollectionsController, 'execute'])
.as('api-collections.create');
})
.prefix('/api/v1/collections')
.middleware([middleware.auth({ guards: ['api'] })]);

View File

@@ -0,0 +1,14 @@
import { middleware } from '#start/kernel';
import router from '@adonisjs/core/services/router';
const DeleteCollectionsController = () =>
import('#api/collections/controllers/delete_collection_controller');
router
.group(() => {
router
.delete('/:id', [DeleteCollectionsController, 'execute'])
.as('api-collections.delete');
})
.prefix('/api/v1/collections')
.middleware([middleware.auth({ guards: ['api'] })]);

View File

@@ -0,0 +1,14 @@
import { middleware } from '#start/kernel';
import router from '@adonisjs/core/services/router';
const GetCollectionsController = () =>
import('#api/collections/controllers/get_collections_controller');
router
.group(() => {
router
.get('', [GetCollectionsController, 'show'])
.as('api-collections.index');
})
.prefix('/api/v1/collections')
.middleware([middleware.auth({ guards: ['api'] })]);

View File

@@ -0,0 +1,14 @@
import { middleware } from '#start/kernel';
import router from '@adonisjs/core/services/router';
const UpdateCollectionsController = () =>
import('#api/collections/controllers/update_collection_controller');
router
.group(() => {
router
.put('/:id', [UpdateCollectionsController, 'execute'])
.as('api-collections.update');
})
.prefix('/api/v1/collections')
.middleware([middleware.auth({ guards: ['api'] })]);

View File

@@ -0,0 +1,4 @@
import '#api/collections/routes/api_create_collections_routes';
import '#api/collections/routes/api_delete_collections_routes';
import '#api/collections/routes/api_get_collections_routes';
import '#api/collections/routes/api_update_collections_routes';

View File

@@ -0,0 +1,23 @@
import { LinkService } from '#links/services/link_service';
import { createLinkValidator } from '#links/validators/create_link_validator';
import { inject } from '@adonisjs/core';
import { HttpContext } from '@adonisjs/core/http';
@inject()
export default class CreateLinkController {
constructor(private linkService: LinkService) {}
async execute({ request, response }: HttpContext) {
const { collectionId, ...payload } =
await request.validateUsing(createLinkValidator);
const link = await this.linkService.createLink({
...payload,
collectionId,
});
return response.json({
message: 'Link created successfully',
link: link.serialize(),
});
}
}

View File

@@ -0,0 +1,39 @@
import { CollectionService } from '#collections/services/collection_service';
import { LinkService } from '#links/services/link_service';
import { deleteLinkValidator } from '#links/validators/delete_link_validator';
import { inject } from '@adonisjs/core';
import { HttpContext } from '@adonisjs/core/http';
import db from '@adonisjs/lucid/services/db';
@inject()
export default class DeleteLinkController {
constructor(
protected collectionsService: CollectionService,
protected linkService: LinkService
) {}
async render({ auth, inertia, request }: HttpContext) {
const linkId = request.qs()?.linkId;
if (!linkId) {
return this.collectionsService.redirectToDashboard();
}
const link = await this.linkService.getLinkById(linkId, auth.user!.id);
await link.load('collection');
return inertia.render('links/delete', { link });
}
async execute({ request, auth }: HttpContext) {
const { params } = await request.validateUsing(deleteLinkValidator);
const link = await this.linkService.getLinkById(params.id, auth.user!.id);
await this.linkService.deleteLink(params.id);
return this.collectionsService.redirectToCollectionId(link.collectionId);
}
async getTotalLinksCount() {
const totalCount = await db.from('links').count('* as total');
return Number(totalCount[0].total);
}
}

View File

@@ -0,0 +1,13 @@
import { LinkService } from '#links/services/link_service';
import { inject } from '@adonisjs/core';
import { HttpContext } from '@adonisjs/core/http';
@inject()
export default class GetFavoriteLinksController {
constructor(private linkService: LinkService) {}
public async execute({ response }: HttpContext) {
const links = await this.linkService.getFavoriteLinksForAuthenticatedUser();
return response.json(links);
}
}

View File

@@ -0,0 +1,19 @@
import { LinkService } from '#links/services/link_service';
import { updateLinkValidator } from '#links/validators/update_link_validator';
import { inject } from '@adonisjs/core';
import { HttpContext } from '@adonisjs/core/http';
@inject()
export default class UpdateLinkController {
constructor(private linkService: LinkService) {}
async execute({ request, response }: HttpContext) {
const { params, ...payload } =
await request.validateUsing(updateLinkValidator);
await this.linkService.updateLink(params.id, payload);
return response.json({
message: 'Link updated successfully',
});
}
}

View File

@@ -0,0 +1,12 @@
import { middleware } from '#start/kernel';
import router from '@adonisjs/core/services/router';
const CreateLinkController = () =>
import('#api/links/controllers/create_link_controller');
router
.group(() => {
router.post('', [CreateLinkController, 'execute']).as('api-links.create');
})
.prefix('/api/v1/links')
.middleware([middleware.auth({ guards: ['api'] })]);

View File

@@ -0,0 +1,14 @@
import { middleware } from '#start/kernel';
import router from '@adonisjs/core/services/router';
const DeleteLinkController = () =>
import('#api/links/controllers/delete_link_controller');
router
.group(() => {
router
.delete('/:id', [DeleteLinkController, 'execute'])
.as('api-links.delete');
})
.prefix('/api/v1/links')
.middleware([middleware.auth({ guards: ['api'] })]);

View File

@@ -0,0 +1,14 @@
import { middleware } from '#start/kernel';
import router from '@adonisjs/core/services/router';
const GetFavoriteLinksController = () =>
import('#api/links/controllers/get_favorite_links_controller');
router
.group(() => {
router
.get('', [GetFavoriteLinksController, 'execute'])
.as('api-links.get-favorite-links');
})
.prefix('/api/v1/links/favorites')
.middleware([middleware.auth({ guards: ['api'] })]);

View File

@@ -0,0 +1,14 @@
import { middleware } from '#start/kernel';
import router from '@adonisjs/core/services/router';
const UpdateLinkController = () =>
import('#api/links/controllers/update_link_controller');
router
.group(() => {
router
.put('/:id', [UpdateLinkController, 'execute'])
.as('api-links.update');
})
.prefix('/api/v1/links')
.middleware([middleware.auth({ guards: ['api'] })]);

View File

@@ -0,0 +1,4 @@
import '#api/links/routes/api_create_link_routes';
import '#api/links/routes/api_delete_link_route';
import '#api/links/routes/api_get_favorite_links_routes';
import '#api/links/routes/api_update_link_route';

View File

@@ -0,0 +1,18 @@
import UnAuthorizedException from '#api/tokens/exceptions/un_authorized_exception';
import { getTokenFromHeader } from '#api/tokens/lib/index';
import { inject } from '@adonisjs/core';
import { HttpContext } from '@adonisjs/core/http';
@inject()
export default class ApiTokenController {
async index(ctx: HttpContext) {
const token = getTokenFromHeader(ctx);
if (!token) {
throw new UnAuthorizedException();
}
return ctx.response.json({
message: 'Token is valid',
});
}
}

View File

@@ -0,0 +1,7 @@
import { Exception } from '@adonisjs/core/exceptions';
export default class UnAuthorizedException extends Exception {
static status = 401;
message = 'Missing or invalid authorization header';
code = 'UNAUTHORIZED';
}

View File

@@ -0,0 +1,11 @@
import { HttpContext } from '@adonisjs/core/http';
export function getTokenFromHeader(ctx: HttpContext) {
const authHeader = ctx.request.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null;
}
return authHeader.substring(7);
}

View File

@@ -0,0 +1,12 @@
import { middleware } from '#start/kernel';
import router from '@adonisjs/core/services/router';
const ApiTokenController = () =>
import('#api/tokens/controllers/api_token_controller');
router
.group(() => {
router.get('/check', [ApiTokenController, 'index']).as('api-tokens.index');
})
.prefix('/api/v1/tokens')
.middleware([middleware.auth({ guards: ['api'] })]);

View File

@@ -0,0 +1 @@
import '#api/tokens/routes/api_token_routes';

View File

@@ -36,6 +36,13 @@ export default class HttpExceptionHandler extends ExceptionHandler {
* response to the client * response to the client
*/ */
async handle(error: unknown, ctx: HttpContext) { async handle(error: unknown, ctx: HttpContext) {
if (ctx.request.url()?.startsWith('/api/v1')) {
return ctx.response.status(400).json({
message: 'Bad Request',
errors: [error],
});
}
if (error instanceof errors.E_ROW_NOT_FOUND) { if (error instanceof errors.E_ROW_NOT_FOUND) {
return ctx.response.redirectToNamedRoute('dashboard'); return ctx.response.redirectToNamedRoute('dashboard');
} }

View File

@@ -1,5 +1,4 @@
import User from '#user/models/user'; import User from '#user/models/user';
import { AccessToken } from '@adonisjs/auth/access_tokens';
type CreateTokenParams = { type CreateTokenParams = {
name: string; name: string;
@@ -23,10 +22,6 @@ export class ApiTokenService {
return User.accessTokens.delete(user, identifier); return User.accessTokens.delete(user, identifier);
} }
validateToken(token: AccessToken) {
return User.accessTokens.verify(token.value!);
}
getTokenByValue(user: User, value: string) { getTokenByValue(user: User, value: string) {
return User.accessTokens.find(user, value); return User.accessTokens.find(user, value);
} }

View File

@@ -1,4 +1,5 @@
import { defineConfig } from '@adonisjs/auth'; import { defineConfig } from '@adonisjs/auth';
import { tokensGuard, tokensUserProvider } from '@adonisjs/auth/access_tokens';
import { sessionGuard, sessionUserProvider } from '@adonisjs/auth/session'; import { sessionGuard, sessionUserProvider } from '@adonisjs/auth/session';
import { Authenticators, InferAuthEvents } from '@adonisjs/auth/types'; import { Authenticators, InferAuthEvents } from '@adonisjs/auth/types';
@@ -11,6 +12,12 @@ const authConfig = defineConfig({
model: () => import('#user/models/user'), model: () => import('#user/models/user'),
}), }),
}), }),
api: tokensGuard({
provider: tokensUserProvider({
tokens: 'accessTokens',
model: () => import('#user/models/user'),
}),
}),
}, },
}); });

View File

@@ -21,6 +21,7 @@
"#admin/*": "./app/admin/*.js", "#admin/*": "./app/admin/*.js",
"#adonis/api": "./.adonisjs/api.ts", "#adonis/api": "./.adonisjs/api.ts",
"#auth/*": "./app/auth/*.js", "#auth/*": "./app/auth/*.js",
"#api/*": "./app/api/*.js",
"#collections/*": "./app/collections/*.js", "#collections/*": "./app/collections/*.js",
"#config/*": "./config/*.js", "#config/*": "./config/*.js",
"#core/*": "./app/core/*.js", "#core/*": "./app/core/*.js",

View File

@@ -1,4 +1,7 @@
import '#admin/routes/routes'; import '#admin/routes/routes';
import '#api/collections/routes/routes';
import '#api/links/routes/routes';
import '#api/tokens/routes/routes';
import '#auth/routes/routes'; import '#auth/routes/routes';
import '#collections/routes/routes'; import '#collections/routes/routes';
import '#favicons/routes/routes'; import '#favicons/routes/routes';