Compare commits

...

3 commits

Author SHA1 Message Date
Nathan Lamy
06fc137057 feat: add attachments 2025-08-19 20:07:38 +02:00
Nathan Lamy
12241d52b9 feat: add health 2025-08-19 19:32:12 +02:00
Nathan Lamy
b2d23dd6d8 feat: add preferences 2025-08-19 17:04:03 +02:00
11 changed files with 224 additions and 48 deletions

View file

@ -3,6 +3,7 @@ import { ColleService } from '#services/colle_service'
import { createColleValidator, createUpcomingCollesValidator } from '#validators/colle'
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import redis from '@adonisjs/redis/services/main'
import { DateTime } from 'luxon'
@inject()
@ -42,6 +43,7 @@ export default class CollesController {
.preload('examiner')
.preload('subject')
.preload('room')
.preload('attachments')
.first()
if (!colle) {
return response.notFound({ message: 'Colle not found' })
@ -57,6 +59,39 @@ export default class CollesController {
}
}
async refresh({ params, response, auth }: HttpContext) {
const colleId = parseInt(params.colleId)
if (isNaN(colleId)) {
return response.badRequest({ message: 'Invalid colle ID' })
}
// Retrieve the colle by ID
const colle = await Colle.query()
.where('id', colleId)
.whereHas('student', (query) => {
query.where('className', auth.user!.className)
})
.first()
if (!colle) {
return response.notFound({ message: 'Colle not found' })
}
// Post Redis message to refresh the colle
await redis.publish(
'jobs_queue',
JSON.stringify({
type: 0, // Refresh colle
colle_id: colle.bjid,
colle_secret: colle.bjsecret,
class_name: auth.user!.className,
})
)
return response.ok({
success: true,
message: `Colle ${colleId} refresh request sent`,
})
}
async create({ request, response }: HttpContext) {
const { colle: payload, className } = await request.validateUsing(createColleValidator)
@ -93,19 +128,54 @@ export default class CollesController {
}
// Check if the colle already exists
const colle = await Colle.query()
const existing = await Colle.query()
.where('studentId', student.id)
.where('subjectId', subject.id)
.where('date', date.toISO())
.first()
// If it exists, update the existing colle
if (colle) {
Object.assign(colle, colleData)
return colle.save()
if (existing) {
// Merge the new data with the existing colle
Object.assign(existing, colleData)
// Handle attachments if any
if (payload.attachments && payload.attachments.length > 0) {
// Retrieve existing attachments
const existingAttachments = await existing.related('attachments').query()
// Remove attachments that are not in the new payload
const existingAttachmentUrls = new Set(existingAttachments.map((a) => a.path))
for (const attachment of payload.attachments) {
if (!existingAttachmentUrls.has(attachment.url)) {
await existing.related('attachments').create({
name: attachment.name,
path: attachment.url,
})
}
}
// Remove attachments that are not in the new payload
for (const attachment of existingAttachments) {
if (!payload.attachments.some((a) => a.url === attachment.path)) {
await attachment.delete()
}
}
}
return existing.save()
}
// Create the colle
return Colle.create(colleData)
const colle = await Colle.create(colleData)
// Handle attachments if any
if (payload.attachments && payload.attachments.length > 0) {
for (const attachment of payload.attachments) {
await colle.related('attachments').create({
path: attachment.url,
name: attachment.name,
})
}
}
return colle
}
async createUpcoming({ request }: HttpContext) {

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,28 +1,57 @@
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 {
success: true,
data: auth.user,
data: {
...auth.user?.serialize(),
email: auth.user!.email || '',
},
}
}
// 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

@ -4,6 +4,9 @@ export default class ColleAttachment extends BaseModel {
@column({ isPrimary: true })
declare id: number
@column()
declare colleId: number
@column()
declare name: string

View file

@ -11,10 +11,10 @@ export default class User extends BaseModel {
@column()
declare className: string
@column({ serializeAs: null})
@column({ serializeAs: null })
declare firstName: string
@column({ serializeAs: null})
@column({ serializeAs: null })
declare lastName: string
@computed()
@ -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

@ -3,8 +3,19 @@ import Examiner from '#models/examiner'
import Room from '#models/room'
import Subject from '#models/subject'
import User from '#models/user'
import redis from '@adonisjs/redis/services/main'
export class ColleService {
async getHealthyUntil(className: string) {
const healtyUntil = await redis.get(`healthy_until_${className}`)
return new Date(healtyUntil || '')
}
async getLastSync(className: string) {
const lastSync = await redis.get(`last_sync_${className}`)
return new Date(lastSync || '')
}
async getStudent(studentName: string, className: string) {
// Find or create a student by name
const { firstName, lastName } = this.splitNames(studentName)
@ -64,6 +75,7 @@ export class ColleService {
.preload('examiner')
.preload('subject')
.preload('room')
.preload('attachments')
.where('date', '>=', startDate)
.where('date', '<=', endDate)
.whereHas('student', (query) => {
@ -74,10 +86,11 @@ export class ColleService {
.orderBy('date', 'asc')
const studentColles = await Colle.query()
.preload('student')
.preload('examiner')
.preload('subject')
.preload('room')
.preload('student')
.preload('attachments')
.where('date', '>=', startDate)
.where('date', '<=', endDate)
.where('studentId', student.id)
@ -90,6 +103,8 @@ export class ColleService {
classColles,
studentColles,
favoriteColles,
healthyUntil: await this.getHealthyUntil(student.className),
lastSync: await this.getLastSync(student.className),
}
}

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

@ -11,7 +11,12 @@ const colle = vine.object({
date: vine.date({ formats: ['iso8601'] }),
bjsecret: vine.string().optional(),
bjid: vine.string().optional(),
// TODO: Add attachments validation
attachments: vine.array(
vine.object({
url: vine.string(),
name: vine.string().maxLength(255),
})
).optional(),
})
const className = vine.string()

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,30 +30,19 @@ 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())
// TEST ROUTE
import redis from '@adonisjs/redis/services/main'
router.get('/', async () => {
await redis.publish("jobs_queue", JSON.stringify({
type: 1,
date: "09/12/2024"
}))
return { message: 'Hello, world!' }
})
// END TEST ROUTE
const SubjectsController = () => import('#controllers/subjects_controller')
router.get('/subjects', [SubjectsController, 'index']).use(middleware.auth())
const CollesController = () => import('#controllers/colles_controller')
router.group(() => {
// TODO: PRIVATE ROUTES
router.post('/', [CollesController, 'create'])
router.post('/upcoming', [CollesController, 'createUpcoming'])
router.post('/:colleId/refresh', [CollesController, 'refresh']).use(middleware.auth())
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')