diff --git a/app/controllers/auth_controller.ts b/app/controllers/auth_controller.ts index e7ff2dc..e92ff95 100644 --- a/app/controllers/auth_controller.ts +++ b/app/controllers/auth_controller.ts @@ -1,5 +1,10 @@ import type { HttpContext } from '@adonisjs/core/http' -import { registerValidator, requestLoginValidator, verifyCodeValidator } from '#validators/auth' +import { + credentialsValidator, + registerValidator, + requestLoginValidator, + verifyCodeValidator, +} from '#validators/auth' import mail from '@adonisjs/mail/services/main' import { AuthService } from '#services/auth_service' import { inject } from '@adonisjs/core' @@ -7,6 +12,7 @@ import app from '@adonisjs/core/services/app' import env from '#start/env' import User from '#models/user' import { DateTime } from 'luxon' +import redis from '@adonisjs/redis/services/main' @inject() export default class AuthController { @@ -126,10 +132,7 @@ export default class AuthController { }) } - const user = await User.query() - .where('id', userId) - .where('className', className) - .first() + const user = await User.query().where('id', userId).where('className', className).first() if (!user) { return response.badRequest({ success: false, @@ -163,6 +166,29 @@ export default class AuthController { } } + // POST /auth/test + public async testCredentials({ request, auth }: HttpContext) { + const { username, password } = await request.validateUsing(credentialsValidator) + await redis.publish( + 'jobs_queue', + JSON.stringify({ + type: 5, // Test credentials + bj_username: username, + bj_password: password, + user_id: auth.user!.id, + class_name: env.get('DEFAULT_CLASS_NAME'), + }) + ) + + return { success: true, message: 'Testing credentials...' } + } + + // GET /auth/status + public async status({ auth }: HttpContext) { + const redisKey = `auth_success_${auth.user!.id}` + return { success: true, data: { authenticated: await redis.get(redisKey) } } + } + // TODO: Magic link login // magicLink({ }: HttpContext) { // // Validate signed url (adonis) diff --git a/app/controllers/internals_controller.ts b/app/controllers/internals_controller.ts index 4699b24..dcfc1e3 100644 --- a/app/controllers/internals_controller.ts +++ b/app/controllers/internals_controller.ts @@ -1,3 +1,4 @@ +import Meal from '#models/meal' import MealRegistration from '#models/meal_registration' import env from '#start/env' import { updateMealsRegistrationsValidator } from '#validators/meal' @@ -43,7 +44,7 @@ export default class InternalsController { // Remove all existing registrations for the user await MealRegistration.query().where('user_id', userId).delete() for (const mealData of meals) { - const meal = await MealRegistration.query() + const meal = await Meal.query() .where({ date: mealData.date, type: mealData.meal_type === 'lunch' ? 0 : 1, diff --git a/app/controllers/meals_controller.ts b/app/controllers/meals_controller.ts index a9d133f..19d1289 100644 --- a/app/controllers/meals_controller.ts +++ b/app/controllers/meals_controller.ts @@ -1,4 +1,6 @@ import Meal from '#models/meal' +import MealRegistration from '#models/meal_registration' +import { credentialsValidator } from '#validators/auth' import { createMealValidator } from '#validators/meal' import type { HttpContext } from '@adonisjs/core/http' import redis from '@adonisjs/redis/services/main' @@ -53,7 +55,7 @@ export default class MealsController { // Create the new meal const meal = await Meal.create({ - date: DateTime.fromJSDate(date), + date, type: mealType, }) await meal.related('courses').createMany(courses) @@ -61,13 +63,39 @@ export default class MealsController { } // GET /meals - public async index({}: HttpContext) { - // TODO: Check if the user is registered for each meal - return Meal.query().orderBy('date', 'asc').preload('courses') + public async index({ auth }: HttpContext) { + const meals = await Meal.query().orderBy('date', 'asc').preload('courses') + const data = meals.map(async (meal) => { + if (meal.submittable) { + const isRegistered = await MealRegistration.query() + .where('meal_id', meal.id) + .where('user_id', auth.user!.id) + .first() + // Remove temporary registrations that are older than 5 minutes + if (isRegistered?.temporary) { + const oneHourAgo = DateTime.now().minus({ minutes: 5 }) + if (isRegistered.createdAt < oneHourAgo) { + await isRegistered.delete() + return meal.serialize() + } + } + return { + ...meal.serialize(), + isRegistered: !!isRegistered, + } + } else { + return meal.serialize() + } + }) + + return Promise.all(data) } // POST /meals/:id - public async registerForMeal({ params, auth, response }: HttpContext) { + // TODO: Unregister from meal + public async registerForMeal({ params, auth, response, request }: HttpContext) { + const { username, password } = await request.validateUsing(credentialsValidator) + const meal = await Meal.find(params.id) if (!meal) { return response.notFound({ message: 'Meal not found' }) @@ -76,18 +104,28 @@ export default class MealsController { return response.badRequest({ message: 'Meal is not submittable' }) } + // Create a bullshit registration + // (to confirm to the user that the registration is in progress...) + // that will eventually be deleted if the registration fails (worker callback) + await MealRegistration.firstOrCreate({ + mealId: meal.id, + userId: auth.user!.id, + temporary: true, + }) + // Register the user for the meal await redis.publish( 'jobs_queue', JSON.stringify({ type: 4, // Submit meals meal: { - date: meal.date.toFormat('yyyy-MM-dd'), + date: DateTime.fromJSDate(meal.date).toISODate(), meal_type: meal.type === 0 ? 'lunch' : 'dinner', }, class_name: auth.user!.className, user_id: auth.user!.id, - // TODO: add user credentials + bj_username: username, + bj_password: password, }) ) diff --git a/app/models/meal.ts b/app/models/meal.ts index b0ccba9..a565b1e 100644 --- a/app/models/meal.ts +++ b/app/models/meal.ts @@ -1,4 +1,3 @@ -import { DateTime } from 'luxon' import { BaseModel, column, computed, hasMany } from '@adonisjs/lucid/orm' import Course from './course.js' import type { HasMany } from '@adonisjs/lucid/types/relations' @@ -8,7 +7,7 @@ export default class Meal extends BaseModel { declare id: number @column() - declare date: DateTime + declare date: Date @column() declare type: 0 | 1 // 0 = lunch, 1 = dinner diff --git a/app/models/meal_registration.ts b/app/models/meal_registration.ts index a97bd26..184aee9 100644 --- a/app/models/meal_registration.ts +++ b/app/models/meal_registration.ts @@ -1,4 +1,5 @@ import { BaseModel, column } from '@adonisjs/lucid/orm' +import { DateTime } from 'luxon' export default class MealRegistration extends BaseModel { @column({ isPrimary: true }) @@ -9,4 +10,10 @@ export default class MealRegistration extends BaseModel { @column() declare userId: number + + @column() + declare temporary: boolean + + @column.dateTime({ autoCreate: true }) + declare createdAt: DateTime } diff --git a/app/validators/auth.ts b/app/validators/auth.ts index 7dc8e29..0522ce8 100644 --- a/app/validators/auth.ts +++ b/app/validators/auth.ts @@ -27,6 +27,13 @@ export const registerValidator = vine.compile( }) ) +export const credentialsValidator = vine.compile( + vine.object({ + username: vine.string().minLength(1).maxLength(255), + password: vine.string().minLength(1).maxLength(255), + }) +) + // TODO: Magic link login // export const magicLinkValidator = vine.compile( // vine.object({ diff --git a/database/migrations/1756125738666_create_meals_registrations_table.ts b/database/migrations/1756125738666_create_meals_registrations_table.ts index 777ed0d..1c436b1 100644 --- a/database/migrations/1756125738666_create_meals_registrations_table.ts +++ b/database/migrations/1756125738666_create_meals_registrations_table.ts @@ -1,13 +1,15 @@ import { BaseSchema } from '@adonisjs/lucid/schema' export default class extends BaseSchema { - protected tableName = 'meals_registrations' + protected tableName = 'meal_registrations' async up() { this.schema.createTable(this.tableName, (table) => { table.increments('id') table.integer('meal_id').unsigned().references('id').inTable('meals').onDelete('CASCADE') table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE') + table.boolean('temporary').defaultTo(false) + table.timestamp('created_at').notNullable() }) } diff --git a/start/routes.ts b/start/routes.ts index fe9444c..6f39fca 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -18,16 +18,20 @@ import { middleware } from './kernel.js' const AuthController = () => import('#controllers/auth_controller') -router.group(() => { - router.post('/auth/request', [AuthController, 'requestLogin']).use(authThrottle) - router.post('/auth/verify', [AuthController, 'verifyCode']).use(throttle) - router.post('/auth/register', [AuthController, 'register']).use(throttle) - router.post('/auth/logout', [AuthController, 'logout']) - router.get('/auth/autocomplete', [AuthController, 'listNames']).use(throttle) - // TODO: Magic link login - // router.get('/auth/magic-link', 'AuthController.magicLink').use(throttle) - // router.get('/auth/listen', 'AuthController.listen') -}) +router + .group(() => { + router.post('/request', [AuthController, 'requestLogin']).use(authThrottle) + router.post('/verify', [AuthController, 'verifyCode']).use(throttle) + router.post('/register', [AuthController, 'register']).use(throttle) + router.post('/logout', [AuthController, 'logout']) + router.get('/autocomplete', [AuthController, 'listNames']).use(throttle) + router.post('/test', [AuthController, 'testCredentials']).use(throttle).use(middleware.auth()) + router.get('/status', [AuthController, 'status']).use(middleware.auth()) + // TODO: Magic link login + // router.get('/auth/magic-link', 'AuthController.magicLink').use(throttle) + // router.get('/auth/listen', 'AuthController.listen') + }) + .prefix('/auth') const UserController = () => import('#controllers/user_controller') router.get('/users/@me', [UserController, 'me']).use(middleware.auth())