feat: add preferences

This commit is contained in:
Nathan Lamy 2025-08-19 17:04:03 +02:00
parent 1082f29143
commit b2d23dd6d8
7 changed files with 121 additions and 29 deletions

View file

@ -0,0 +1,17 @@
import type { HttpContext } from '@adonisjs/core/http'
import { inject } from '@adonisjs/core'
import { SubjectService } from '#services/subject_service'
@inject()
export default class SubjectsController {
constructor(private subjectService: SubjectService) {}
// GET /subjects
async index({ auth }: HttpContext) {
const data = await this.subjectService.getAll(auth.user!.className)
return {
success: true,
data,
}
}
}

View file

@ -1,9 +1,12 @@
import User from '#models/user'
import { createUserValidator } from '#validators/user'
import { SubjectService } from '#services/subject_service'
import { updateUserValidator } from '#validators/user'
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import { cuid } from '@adonisjs/core/helpers'
@inject()
export default class UserController {
constructor(private subjectService: SubjectService) {}
// GET /users/@me
async me({ auth }: HttpContext) {
return {
@ -12,17 +15,40 @@ export default class UserController {
}
}
// POST /users
async create({ request }: HttpContext) {
const payload = await request.validateUsing(createUserValidator)
// Save avatar
const avatar = `avatars/${cuid()}.${payload.avatar.extname}`
await payload.avatar.moveToDisk(avatar)
// const avatar = await drive.use().getSignedUrl(key)
return User.create({
...payload,
// TODO: No avatar for now!!
// avatar,
// POST /users/@me
// Update user preferences (for subjects)
async update({ request, response, auth }: HttpContext) {
const user = auth.user!
const { preferences: data } = await request.validateUsing(updateUserValidator)
const preferences = user.extras?.preferences || []
// Validate subject names
const validSubjects = await this.subjectService.getAll(user.className)
for (const { name, emoji, color } of data) {
if (!validSubjects.includes(name)) {
return response.badRequest({
message: `Invalid subject name: ${name}`,
})
}
const existing = preferences.find((p: any) => p.name === name)
if (existing) {
// Update
existing.emoji = emoji
existing.color = color
} else {
// Create new preference
preferences.push({ name, emoji, color })
}
}
user.extras = {
...user.extras,
preferences,
}
await user.save()
return {
success: true,
data: user,
}
}
}

View file

@ -25,6 +25,14 @@ export default class User extends BaseModel {
@column({ serializeAs: null })
declare email: string
@column({ serializeAs: null })
declare extras: Record<string, any>
@computed()
get preferences(): { name: string; emoji: string; color: string }[] {
return this.extras?.preferences || []
}
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime

View file

@ -0,0 +1,18 @@
import Colle from '#models/colle'
import Subject from '#models/subject'
export class SubjectService {
async getAll(className: string): Promise<string[]> {
const subjectsIds = (
await Colle.query()
.distinct('subjectId')
.select('subjectId')
.whereHas('student', (query) => {
query.where('className', className)
})
).map((colle) => colle.subjectId)
const subjects = await Subject.query().whereIn('id', subjectsIds).select('name')
return subjects.map((subject) => subject.name)
}
}

View file

@ -1,10 +1,13 @@
import vine from '@vinejs/vine'
export const createUserValidator = vine.compile(
export const updateUserValidator = vine.compile(
vine.object({
firstName: vine.string().minLength(2).maxLength(50),
lastName: vine.string().minLength(2).maxLength(50),
className: vine.string().minLength(2).maxLength(10),
avatar: vine.file(),
preferences: vine.array(
vine.object({
name: vine.string(),
emoji: vine.string().maxLength(12),
color: vine.string().maxLength(12),
})
),
})
)

View file

@ -0,0 +1,18 @@
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'users'
async up() {
this.schema.alterTable(this.tableName, (table) => {
// Adding a JSON column for extras (can hold flexible data)
table.jsonb('extras').nullable().after('updated_at')
})
}
async down() {
this.schema.alterTable(this.tableName, (table) => {
table.dropColumn('extras')
})
}
}

View file

@ -8,12 +8,13 @@
*/
import router from '@adonisjs/core/services/router'
import transmit from '@adonisjs/transmit/services/main'
// import transmit from '@adonisjs/transmit/services/main'
import { authThrottle } from './limiters.js'
import { throttle } from './limiter.js'
import { middleware } from './kernel.js'
transmit.registerRoutes()
// TODO: Magic link login
// transmit.registerRoutes()
const AuthController = () => import('#controllers/auth_controller')
@ -29,8 +30,11 @@ router.group(() => {
})
const UserController = () => import('#controllers/user_controller')
router.get('/users/@me', [UserController, 'me']).use(middleware.auth())
router.post('/users/@me', [UserController, 'update']).use(middleware.auth())
const SubjectsController = () => import('#controllers/subjects_controller')
router.get('/subjects', [SubjectsController, 'index']).use(middleware.auth())
// TEST ROUTE
import redis from '@adonisjs/redis/services/main'
@ -38,7 +42,8 @@ import redis from '@adonisjs/redis/services/main'
router.get('/', async () => {
await redis.publish("jobs_queue", JSON.stringify({
type: 1,
date: "09/12/2024"
date: "20/09/2019",
class_name: "MPSI 2",
}))
return { message: 'Hello, world!' }
})
@ -51,8 +56,5 @@ router.group(() => {
router.post('/upcoming', [CollesController, 'createUpcoming'])
router.get('/', [CollesController, 'index']).use(middleware.auth())
router.get('/:colleId', [CollesController, 'show']).use(middleware.auth())
// router.get('/colles/:id', 'CollesController.show')
// router.put('/colles/:id', 'CollesController.update')
// router.delete('/colles/:id', 'CollesController.delete')
}
).prefix('/colles')