diff --git a/app/Controllers/Http/v1/Auth/AuthsController.ts b/app/Controllers/Http/v1/Auth/AuthsController.ts index 22911fa..e8317ba 100644 --- a/app/Controllers/Http/v1/Auth/AuthsController.ts +++ b/app/Controllers/Http/v1/Auth/AuthsController.ts @@ -16,6 +16,7 @@ import TwilioService from 'App/Services/TwilioService' import RegisterWithPasswordValidator from 'App/Validators/v1/Auth/RegisterWithPasswordValidator' import LoginWithPasswordValidator from 'App/Validators/v1/Auth/LoginWithPasswordValidator' import LoginWithOtpValidator from 'App/Validators/v1/Auth/LoginWithOtpValidator' +import LogoutValidator from 'App/Validators/v1/Auth/LogoutValidator' // Models import User from 'App/Models/User' @@ -204,7 +205,9 @@ export default class AuthsController { }) if (newSession.session && newSession.refreshToken) { - const userToken = this.jwt.generate({ user_id: user.id }).make() + const userToken = this.jwt + .generate({ user_id: user.id, session_id: newSession.session.id }) + .make() const expiresAt = DateTime.now().plus({ days: 7 }).toUnixInteger() return response.api( @@ -243,8 +246,6 @@ export default class AuthsController { return response.api({ message: 'Invalid credentials.' }, StatusCodes.UNAUTHORIZED) } - console.log(user.toJSON()) - if (payload.email && !user.emailConfirmedAt) { return response.api({ message: 'Please confirm your email.' }, StatusCodes.FORBIDDEN) } @@ -293,4 +294,50 @@ export default class AuthsController { return response.api({ message: `OTP Code has been sent to ${payload.phone}` }, StatusCodes.OK) } } + + public async signOut({ request, response }: HttpContextContract) { + const payload = await request.validate(LogoutValidator) + const userId = request.decoded!.user_id + const sessionId = request.decoded!.session_id + + const sessions = await Database.transaction(async (trx) => { + const currentSession = await Session.query({ client: trx }) + .where('user_id', userId) + .andWhere('id', sessionId) + .first() + + const allSession = await Session.query({ client: trx }).where('user_id', userId).exec() + + return { + currentSession, + allSession, + } + }) + + try { + if (!sessions.currentSession) { + return response.api({ message: 'Invalid session.' }, StatusCodes.UNAUTHORIZED) + } + + if (payload.scope === 'global') { + sessions.allSession.map(async (session) => { + await session.delete() + }) + } + + if (payload.scope === 'others') { + sessions.allSession.map(async (session) => { + if (session.id !== sessions.currentSession!.id) await session.delete() + }) + } + + if (payload.scope === 'local') { + await sessions.currentSession.delete() + } + + return response.api({ message: '' }, StatusCodes.NO_CONTENT) + } catch (e) { + return response.api({ message: `704: ${e}` }, StatusCodes.INTERNAL_SERVER_ERROR) + } + } } diff --git a/app/Controllers/Http/v1/Auth/VerifiesController.ts b/app/Controllers/Http/v1/Auth/VerifiesController.ts index daae24a..a86722b 100644 --- a/app/Controllers/Http/v1/Auth/VerifiesController.ts +++ b/app/Controllers/Http/v1/Auth/VerifiesController.ts @@ -133,7 +133,9 @@ export default class VerifiesController { }) if (newSession.session && newSession.refreshToken) { - const userToken = this.jwt.generate({ user_id: user.id }).make() + const userToken = this.jwt + .generate({ user_id: user.id, session_id: newSession.session.id }) + .make() const expiresAt = DateTime.now().plus({ days: 7 }).toUnixInteger() return response.api( @@ -230,7 +232,9 @@ export default class VerifiesController { }) if (newSession.session && newSession.refreshToken) { - const userToken = this.jwt.generate({ user_id: user.id }).make() + const userToken = this.jwt + .generate({ user_id: user.id, session_id: newSession.session.id }) + .make() const expiresAt = DateTime.now().plus({ days: 7 }).toUnixInteger() return response diff --git a/app/Middleware/SessionMiddleware.ts b/app/Middleware/UserSessionMiddleware.ts similarity index 89% rename from app/Middleware/SessionMiddleware.ts rename to app/Middleware/UserSessionMiddleware.ts index 2c60dcb..a1f5809 100644 --- a/app/Middleware/SessionMiddleware.ts +++ b/app/Middleware/UserSessionMiddleware.ts @@ -1,7 +1,7 @@ import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' import JwtService from 'App/Services/JwtService' -export default class SessionMiddleware { +export default class UserSessionMiddleware { public jwtService = new JwtService() public async handle({ request, response }: HttpContextContract, next: () => Promise) { @@ -20,6 +20,7 @@ export default class SessionMiddleware { request.decoded = { user_id: decoded['user_id'], + session_id: decoded['session_id'], } await next() diff --git a/app/Types/authentication.d.ts b/app/Types/authentication.d.ts index ce99371..3df22d2 100644 --- a/app/Types/authentication.d.ts +++ b/app/Types/authentication.d.ts @@ -1,5 +1,6 @@ interface Token { user_id: string + session_id: string } interface JwtGeneratePayload { diff --git a/app/Validators/v1/Auth/LogoutValidator.ts b/app/Validators/v1/Auth/LogoutValidator.ts new file mode 100644 index 0000000..31d8d52 --- /dev/null +++ b/app/Validators/v1/Auth/LogoutValidator.ts @@ -0,0 +1,12 @@ +import { schema, CustomMessages } from '@ioc:Adonis/Core/Validator' +import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +export default class LogoutValidator { + constructor(protected ctx: HttpContextContract) {} + + public schema = schema.create({ + scope: schema.enum(['global', 'local', 'others']), + }) + + public messages: CustomMessages = {} +} diff --git a/contracts/request.ts b/contracts/request.ts index a5717e3..a792f76 100644 --- a/contracts/request.ts +++ b/contracts/request.ts @@ -1,5 +1,6 @@ interface Token { - user_id: number + user_id: string + session_id: string } declare module '@ioc:Adonis/Core/Request' { diff --git a/database/migrations/1702736652259_identities.ts b/database/migrations/1702736652259_identities.ts index 5602d96..85d4414 100644 --- a/database/migrations/1702736652259_identities.ts +++ b/database/migrations/1702736652259_identities.ts @@ -8,7 +8,7 @@ export default class extends BaseSchema { table .uuid('id', { primaryKey: true }) .defaultTo(this.db.rawQuery('uuid_generate_v4()').knexQuery) - table.uuid('user_id').references('id').inTable('auth.users') + table.uuid('user_id').references('id').inTable('auth.users').onDelete('CASCADE') table.string('provider') table.jsonb('identity_data') table.timestamp('last_sign_in_at', { useTz: true }) diff --git a/database/migrations/1702737214080_sessions.ts b/database/migrations/1702737214080_sessions.ts index 0f31d0c..a6578a6 100644 --- a/database/migrations/1702737214080_sessions.ts +++ b/database/migrations/1702737214080_sessions.ts @@ -8,7 +8,7 @@ export default class extends BaseSchema { table .uuid('id', { primaryKey: true }) .defaultTo(this.db.rawQuery('uuid_generate_v4()').knexQuery) - table.uuid('user_id').references('id').inTable('auth.users') + table.uuid('user_id').references('id').inTable('auth.users').onDelete('CASCADE') table.timestamp('refreshed_at', { useTz: true }) table.text('user_agent') table.string('ip') diff --git a/database/migrations/1702737445458_refresh_tokens.ts b/database/migrations/1702737445458_refresh_tokens.ts index 3b4d475..459e963 100644 --- a/database/migrations/1702737445458_refresh_tokens.ts +++ b/database/migrations/1702737445458_refresh_tokens.ts @@ -6,8 +6,8 @@ export default class extends BaseSchema { public async up() { this.schema.createTable(this.tableName, (table) => { table.increments('id') - table.uuid('user_id').references('id').inTable('auth.users') - table.uuid('session_id').references('id').inTable('auth.sessions') + table.uuid('user_id').references('id').inTable('auth.users').onDelete('CASCADE') + table.uuid('session_id').references('id').inTable('auth.sessions').onDelete('CASCADE') table.string('token') table.boolean('revoked').defaultTo(false) table.string('parent').nullable() diff --git a/routes/auth/v1/index.ts b/routes/auth/v1/index.ts index 33bb493..016c3fd 100644 --- a/routes/auth/v1/index.ts +++ b/routes/auth/v1/index.ts @@ -4,6 +4,7 @@ Route.group(() => { Route.post('/register', 'AuthsController.signUpWithPassword') Route.post('/login/password', 'AuthsController.signInWithPassword') Route.post('/login/otp', 'AuthsController.signInWithOtp') + Route.delete('/logout', 'AuthsController.signOut').middleware('userSession') require('./verify') }) .prefix('/v1') diff --git a/start/kernel.ts b/start/kernel.ts index 045edd0..8b1c09a 100644 --- a/start/kernel.ts +++ b/start/kernel.ts @@ -42,5 +42,5 @@ Server.middleware.register([ | */ Server.middleware.registerNamed({ - session: () => import('App/Middleware/SessionMiddleware'), + userSession: () => import('App/Middleware/UserSessionMiddleware'), })