From 208f2c631f4720f56913974549b74d659dd680f2 Mon Sep 17 00:00:00 2001 From: Sonny Date: Thu, 28 Aug 2025 16:44:31 +0200 Subject: [PATCH] feat: add api controllers and routes for browser extension --- .../create_collection_controller.ts | 20 ++++++++++ .../delete_collection_controller.ts | 17 ++++++++ .../controllers/get_collections_controller.ts | 16 ++++++++ .../update_collection_controller.ts | 21 ++++++++++ .../routes/api_create_collections_routes.ts | 14 +++++++ .../routes/api_delete_collections_routes.ts | 14 +++++++ .../routes/api_get_collections_routes.ts | 14 +++++++ .../routes/api_update_collections_routes.ts | 14 +++++++ app/api/collections/routes/routes.ts | 4 ++ .../controllers/create_link_controller.ts | 23 +++++++++++ .../controllers/delete_link_controller.ts | 39 +++++++++++++++++++ .../get_favorite_links_controller.ts | 13 +++++++ .../controllers/update_link_controller.ts | 19 +++++++++ .../links/routes/api_create_link_routes.ts | 12 ++++++ app/api/links/routes/api_delete_link_route.ts | 14 +++++++ .../routes/api_get_favorite_links_routes.ts | 14 +++++++ app/api/links/routes/api_update_link_route.ts | 14 +++++++ app/api/links/routes/routes.ts | 4 ++ .../controllers/api_token_controller.ts | 18 +++++++++ .../exceptions/un_authorized_exception.ts | 7 ++++ app/api/tokens/lib/index.ts | 11 ++++++ app/api/tokens/routes/api_token_routes.ts | 12 ++++++ app/api/tokens/routes/routes.ts | 1 + app/core/exceptions/handler.ts | 7 ++++ app/user/services/api_token_service.ts | 5 --- config/auth.ts | 7 ++++ package.json | 1 + start/routes.ts | 3 ++ 28 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 app/api/collections/controllers/create_collection_controller.ts create mode 100644 app/api/collections/controllers/delete_collection_controller.ts create mode 100644 app/api/collections/controllers/get_collections_controller.ts create mode 100644 app/api/collections/controllers/update_collection_controller.ts create mode 100644 app/api/collections/routes/api_create_collections_routes.ts create mode 100644 app/api/collections/routes/api_delete_collections_routes.ts create mode 100644 app/api/collections/routes/api_get_collections_routes.ts create mode 100644 app/api/collections/routes/api_update_collections_routes.ts create mode 100644 app/api/collections/routes/routes.ts create mode 100644 app/api/links/controllers/create_link_controller.ts create mode 100644 app/api/links/controllers/delete_link_controller.ts create mode 100644 app/api/links/controllers/get_favorite_links_controller.ts create mode 100644 app/api/links/controllers/update_link_controller.ts create mode 100644 app/api/links/routes/api_create_link_routes.ts create mode 100644 app/api/links/routes/api_delete_link_route.ts create mode 100644 app/api/links/routes/api_get_favorite_links_routes.ts create mode 100644 app/api/links/routes/api_update_link_route.ts create mode 100644 app/api/links/routes/routes.ts create mode 100644 app/api/tokens/controllers/api_token_controller.ts create mode 100644 app/api/tokens/exceptions/un_authorized_exception.ts create mode 100644 app/api/tokens/lib/index.ts create mode 100644 app/api/tokens/routes/api_token_routes.ts create mode 100644 app/api/tokens/routes/routes.ts diff --git a/app/api/collections/controllers/create_collection_controller.ts b/app/api/collections/controllers/create_collection_controller.ts new file mode 100644 index 0000000..2fa8209 --- /dev/null +++ b/app/api/collections/controllers/create_collection_controller.ts @@ -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(), + }); + } +} diff --git a/app/api/collections/controllers/delete_collection_controller.ts b/app/api/collections/controllers/delete_collection_controller.ts new file mode 100644 index 0000000..4328346 --- /dev/null +++ b/app/api/collections/controllers/delete_collection_controller.ts @@ -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', + }); + } +} diff --git a/app/api/collections/controllers/get_collections_controller.ts b/app/api/collections/controllers/get_collections_controller.ts new file mode 100644 index 0000000..10f8446 --- /dev/null +++ b/app/api/collections/controllers/get_collections_controller.ts @@ -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()), + }); + } +} diff --git a/app/api/collections/controllers/update_collection_controller.ts b/app/api/collections/controllers/update_collection_controller.ts new file mode 100644 index 0000000..236e522 --- /dev/null +++ b/app/api/collections/controllers/update_collection_controller.ts @@ -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', + }); + } +} diff --git a/app/api/collections/routes/api_create_collections_routes.ts b/app/api/collections/routes/api_create_collections_routes.ts new file mode 100644 index 0000000..51b9bdc --- /dev/null +++ b/app/api/collections/routes/api_create_collections_routes.ts @@ -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'] })]); diff --git a/app/api/collections/routes/api_delete_collections_routes.ts b/app/api/collections/routes/api_delete_collections_routes.ts new file mode 100644 index 0000000..00b6c59 --- /dev/null +++ b/app/api/collections/routes/api_delete_collections_routes.ts @@ -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'] })]); diff --git a/app/api/collections/routes/api_get_collections_routes.ts b/app/api/collections/routes/api_get_collections_routes.ts new file mode 100644 index 0000000..e61c00f --- /dev/null +++ b/app/api/collections/routes/api_get_collections_routes.ts @@ -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'] })]); diff --git a/app/api/collections/routes/api_update_collections_routes.ts b/app/api/collections/routes/api_update_collections_routes.ts new file mode 100644 index 0000000..6471307 --- /dev/null +++ b/app/api/collections/routes/api_update_collections_routes.ts @@ -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'] })]); diff --git a/app/api/collections/routes/routes.ts b/app/api/collections/routes/routes.ts new file mode 100644 index 0000000..c49a3b7 --- /dev/null +++ b/app/api/collections/routes/routes.ts @@ -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'; diff --git a/app/api/links/controllers/create_link_controller.ts b/app/api/links/controllers/create_link_controller.ts new file mode 100644 index 0000000..abb4e5e --- /dev/null +++ b/app/api/links/controllers/create_link_controller.ts @@ -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(), + }); + } +} diff --git a/app/api/links/controllers/delete_link_controller.ts b/app/api/links/controllers/delete_link_controller.ts new file mode 100644 index 0000000..6cf9a7e --- /dev/null +++ b/app/api/links/controllers/delete_link_controller.ts @@ -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); + } +} diff --git a/app/api/links/controllers/get_favorite_links_controller.ts b/app/api/links/controllers/get_favorite_links_controller.ts new file mode 100644 index 0000000..7bf1e16 --- /dev/null +++ b/app/api/links/controllers/get_favorite_links_controller.ts @@ -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); + } +} diff --git a/app/api/links/controllers/update_link_controller.ts b/app/api/links/controllers/update_link_controller.ts new file mode 100644 index 0000000..513066d --- /dev/null +++ b/app/api/links/controllers/update_link_controller.ts @@ -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', + }); + } +} diff --git a/app/api/links/routes/api_create_link_routes.ts b/app/api/links/routes/api_create_link_routes.ts new file mode 100644 index 0000000..4d9bd6d --- /dev/null +++ b/app/api/links/routes/api_create_link_routes.ts @@ -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'] })]); diff --git a/app/api/links/routes/api_delete_link_route.ts b/app/api/links/routes/api_delete_link_route.ts new file mode 100644 index 0000000..e19fc40 --- /dev/null +++ b/app/api/links/routes/api_delete_link_route.ts @@ -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'] })]); diff --git a/app/api/links/routes/api_get_favorite_links_routes.ts b/app/api/links/routes/api_get_favorite_links_routes.ts new file mode 100644 index 0000000..2e30039 --- /dev/null +++ b/app/api/links/routes/api_get_favorite_links_routes.ts @@ -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'] })]); diff --git a/app/api/links/routes/api_update_link_route.ts b/app/api/links/routes/api_update_link_route.ts new file mode 100644 index 0000000..d1052f0 --- /dev/null +++ b/app/api/links/routes/api_update_link_route.ts @@ -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'] })]); diff --git a/app/api/links/routes/routes.ts b/app/api/links/routes/routes.ts new file mode 100644 index 0000000..13a2ce6 --- /dev/null +++ b/app/api/links/routes/routes.ts @@ -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'; diff --git a/app/api/tokens/controllers/api_token_controller.ts b/app/api/tokens/controllers/api_token_controller.ts new file mode 100644 index 0000000..1654634 --- /dev/null +++ b/app/api/tokens/controllers/api_token_controller.ts @@ -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', + }); + } +} diff --git a/app/api/tokens/exceptions/un_authorized_exception.ts b/app/api/tokens/exceptions/un_authorized_exception.ts new file mode 100644 index 0000000..6eb061f --- /dev/null +++ b/app/api/tokens/exceptions/un_authorized_exception.ts @@ -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'; +} diff --git a/app/api/tokens/lib/index.ts b/app/api/tokens/lib/index.ts new file mode 100644 index 0000000..52aadc9 --- /dev/null +++ b/app/api/tokens/lib/index.ts @@ -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); +} diff --git a/app/api/tokens/routes/api_token_routes.ts b/app/api/tokens/routes/api_token_routes.ts new file mode 100644 index 0000000..81a2eb8 --- /dev/null +++ b/app/api/tokens/routes/api_token_routes.ts @@ -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'] })]); diff --git a/app/api/tokens/routes/routes.ts b/app/api/tokens/routes/routes.ts new file mode 100644 index 0000000..a4a6067 --- /dev/null +++ b/app/api/tokens/routes/routes.ts @@ -0,0 +1 @@ +import '#api/tokens/routes/api_token_routes'; diff --git a/app/core/exceptions/handler.ts b/app/core/exceptions/handler.ts index 79ad3df..c58550c 100644 --- a/app/core/exceptions/handler.ts +++ b/app/core/exceptions/handler.ts @@ -36,6 +36,13 @@ export default class HttpExceptionHandler extends ExceptionHandler { * response to the client */ 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) { return ctx.response.redirectToNamedRoute('dashboard'); } diff --git a/app/user/services/api_token_service.ts b/app/user/services/api_token_service.ts index 9b9fb30..4f2965d 100644 --- a/app/user/services/api_token_service.ts +++ b/app/user/services/api_token_service.ts @@ -1,5 +1,4 @@ import User from '#user/models/user'; -import { AccessToken } from '@adonisjs/auth/access_tokens'; type CreateTokenParams = { name: string; @@ -23,10 +22,6 @@ export class ApiTokenService { return User.accessTokens.delete(user, identifier); } - validateToken(token: AccessToken) { - return User.accessTokens.verify(token.value!); - } - getTokenByValue(user: User, value: string) { return User.accessTokens.find(user, value); } diff --git a/config/auth.ts b/config/auth.ts index d0eef6d..25a93d2 100644 --- a/config/auth.ts +++ b/config/auth.ts @@ -1,4 +1,5 @@ import { defineConfig } from '@adonisjs/auth'; +import { tokensGuard, tokensUserProvider } from '@adonisjs/auth/access_tokens'; import { sessionGuard, sessionUserProvider } from '@adonisjs/auth/session'; import { Authenticators, InferAuthEvents } from '@adonisjs/auth/types'; @@ -11,6 +12,12 @@ const authConfig = defineConfig({ model: () => import('#user/models/user'), }), }), + api: tokensGuard({ + provider: tokensUserProvider({ + tokens: 'accessTokens', + model: () => import('#user/models/user'), + }), + }), }, }); diff --git a/package.json b/package.json index 7c81290..41a5a3b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "#admin/*": "./app/admin/*.js", "#adonis/api": "./.adonisjs/api.ts", "#auth/*": "./app/auth/*.js", + "#api/*": "./app/api/*.js", "#collections/*": "./app/collections/*.js", "#config/*": "./config/*.js", "#core/*": "./app/core/*.js", diff --git a/start/routes.ts b/start/routes.ts index e2b8ca2..d02a5eb 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -1,4 +1,7 @@ import '#admin/routes/routes'; +import '#api/collections/routes/routes'; +import '#api/links/routes/routes'; +import '#api/tokens/routes/routes'; import '#auth/routes/routes'; import '#collections/routes/routes'; import '#favicons/routes/routes';