mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-05 00:46:00 +08:00
Persist learn progress per user
This commit is contained in:
21
app/middleware/require-auth.ts
Normal file
21
app/middleware/require-auth.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineNuxtRouteMiddleware, navigateTo } from '#app'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
const auth = useAuthStore()
|
||||
|
||||
if (!auth.accessToken) {
|
||||
await auth.tryRefresh().catch(() => false)
|
||||
}
|
||||
|
||||
if (!auth.initialized) {
|
||||
await auth.fetchUser().catch(() => null)
|
||||
} else if (!auth.user) {
|
||||
await auth.fetchUser().catch(() => null)
|
||||
}
|
||||
|
||||
if (!auth.user) {
|
||||
const redirect = encodeURIComponent(to.fullPath || '/')
|
||||
return navigateTo(`/login?redirect=${redirect}`)
|
||||
}
|
||||
})
|
||||
@@ -8,7 +8,7 @@ import {normalize, TTS_MODEL, normalizeATC} from "../../utils/normalize";
|
||||
import { getServerRuntimeConfig } from "../../utils/runtimeConfig";
|
||||
import {request} from "node:http";
|
||||
import { TransmissionLog } from "../../models/TransmissionLog";
|
||||
import { getUserFromEvent } from "../../utils/auth";
|
||||
import { requireUserSession } from "../../utils/auth";
|
||||
|
||||
|
||||
function outDir() {
|
||||
@@ -125,6 +125,8 @@ export default defineEventHandler(async (event) => {
|
||||
format?: AudioFmt | "smallest";
|
||||
}>(event);
|
||||
|
||||
const user = await requireUserSession(event);
|
||||
|
||||
const raw = (body?.text || "").trim();
|
||||
if (!raw) throw createError({ statusCode: 400, statusMessage: "text required" });
|
||||
|
||||
@@ -212,9 +214,8 @@ export default defineEventHandler(async (event) => {
|
||||
// await writeFile(fileJson, JSON.stringify(meta, null, 2), "utf-8");
|
||||
|
||||
try {
|
||||
const user = await getUserFromEvent(event)
|
||||
await TransmissionLog.create({
|
||||
user: user?._id,
|
||||
user: user._id,
|
||||
role: "atc",
|
||||
channel: "say",
|
||||
direction: "outgoing",
|
||||
|
||||
24
server/api/learn/state.get.ts
Normal file
24
server/api/learn/state.get.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { requireUserSession } from '../../utils/auth'
|
||||
import { LearnProfile } from '../../models/LearnProfile'
|
||||
import { createDefaultLearnConfig, createDefaultLearnState } from '~~/shared/learn/config'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = await requireUserSession(event)
|
||||
const profile = await LearnProfile.findOne({ user: user._id })
|
||||
|
||||
if (!profile) {
|
||||
return createDefaultLearnState()
|
||||
}
|
||||
|
||||
const config = { ...createDefaultLearnConfig(), ...(profile.config || {}) }
|
||||
const progress =
|
||||
profile.progress && typeof profile.progress === 'object'
|
||||
? JSON.parse(JSON.stringify(profile.progress))
|
||||
: {}
|
||||
|
||||
return {
|
||||
xp: typeof profile.xp === 'number' ? profile.xp : 0,
|
||||
progress,
|
||||
config,
|
||||
}
|
||||
})
|
||||
110
server/api/learn/state.put.ts
Normal file
110
server/api/learn/state.put.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { readBody } from 'h3'
|
||||
import { requireUserSession } from '../../utils/auth'
|
||||
import { LearnProfile } from '../../models/LearnProfile'
|
||||
import type { LearnConfig, LearnProgress, LessonProgress } from '~~/shared/learn/config'
|
||||
import { createDefaultLearnConfig } from '~~/shared/learn/config'
|
||||
|
||||
interface LearnStateUpdateBody {
|
||||
xp?: number
|
||||
progress?: LearnProgress
|
||||
config?: Partial<LearnConfig>
|
||||
}
|
||||
|
||||
function sanitizeProgress(input: LearnProgress | undefined): LearnProgress | undefined {
|
||||
if (!input || typeof input !== 'object') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const sanitized: LearnProgress = {}
|
||||
|
||||
for (const [moduleId, lessons] of Object.entries(input)) {
|
||||
if (!lessons || typeof lessons !== 'object') {
|
||||
continue
|
||||
}
|
||||
|
||||
const lessonProgress: Record<string, LessonProgress> = {}
|
||||
|
||||
for (const [lessonId, value] of Object.entries(lessons as Record<string, any>)) {
|
||||
if (!value || typeof value !== 'object') {
|
||||
continue
|
||||
}
|
||||
|
||||
const bestRaw = (value as any).best
|
||||
const doneRaw = (value as any).done
|
||||
|
||||
const best = typeof bestRaw === 'number' && Number.isFinite(bestRaw) ? Math.max(0, Math.min(100, Math.round(bestRaw))) : 0
|
||||
const done = Boolean(doneRaw)
|
||||
|
||||
lessonProgress[lessonId] = { best, done }
|
||||
}
|
||||
|
||||
sanitized[moduleId] = lessonProgress
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
function sanitizeConfig(input: Partial<LearnConfig> | undefined): LearnConfig | undefined {
|
||||
if (!input || typeof input !== 'object') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const config = createDefaultLearnConfig()
|
||||
|
||||
if (typeof input.tts === 'boolean') {
|
||||
config.tts = input.tts
|
||||
}
|
||||
|
||||
if (typeof input.audioChallenge === 'boolean') {
|
||||
config.audioChallenge = input.audioChallenge
|
||||
}
|
||||
|
||||
if (typeof input.radioLevel === 'number' && Number.isFinite(input.radioLevel)) {
|
||||
const level = Math.round(input.radioLevel)
|
||||
config.radioLevel = Math.min(5, Math.max(1, level))
|
||||
}
|
||||
|
||||
if (typeof input.voice === 'string') {
|
||||
config.voice = input.voice.slice(0, 120)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = await requireUserSession(event)
|
||||
const body = await readBody<LearnStateUpdateBody>(event)
|
||||
|
||||
let profile = await LearnProfile.findOne({ user: user._id })
|
||||
if (!profile) {
|
||||
profile = new LearnProfile({ user: user._id })
|
||||
}
|
||||
|
||||
if (typeof body.xp === 'number' && Number.isFinite(body.xp)) {
|
||||
profile.xp = Math.max(0, Math.round(body.xp))
|
||||
}
|
||||
|
||||
const progress = sanitizeProgress(body.progress)
|
||||
if (progress !== undefined) {
|
||||
profile.progress = progress
|
||||
}
|
||||
|
||||
const config = sanitizeConfig(body.config)
|
||||
if (config !== undefined) {
|
||||
profile.config = config
|
||||
}
|
||||
|
||||
await profile.save()
|
||||
|
||||
const responseConfig = { ...createDefaultLearnConfig(), ...(profile.config || {}) }
|
||||
const responseProgress =
|
||||
profile.progress && typeof profile.progress === 'object'
|
||||
? JSON.parse(JSON.stringify(profile.progress))
|
||||
: {}
|
||||
|
||||
return {
|
||||
xp: typeof profile.xp === 'number' ? profile.xp : 0,
|
||||
progress: responseProgress,
|
||||
config: responseConfig,
|
||||
}
|
||||
})
|
||||
34
server/models/LearnProfile.ts
Normal file
34
server/models/LearnProfile.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import mongoose from 'mongoose'
|
||||
import type { LearnConfig, LearnProgress } from '~~/shared/learn/config'
|
||||
|
||||
export interface LearnProfileDocument extends mongoose.Document {
|
||||
user: mongoose.Types.ObjectId
|
||||
xp: number
|
||||
progress: LearnProgress
|
||||
config: LearnConfig
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
const learnProfileSchema = new mongoose.Schema<LearnProfileDocument>(
|
||||
{
|
||||
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, unique: true },
|
||||
xp: { type: Number, default: 0 },
|
||||
progress: { type: mongoose.Schema.Types.Mixed, default: () => ({}) },
|
||||
config: {
|
||||
tts: { type: Boolean, default: false },
|
||||
radioLevel: { type: Number, default: 4, min: 1, max: 5 },
|
||||
voice: { type: String, default: '', trim: true },
|
||||
audioChallenge: { type: Boolean, default: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
)
|
||||
|
||||
learnProfileSchema.index({ user: 1 }, { unique: true })
|
||||
|
||||
export const LearnProfile =
|
||||
(mongoose.models.LearnProfile as mongoose.Model<LearnProfileDocument> | undefined) ||
|
||||
mongoose.model<LearnProfileDocument>('LearnProfile', learnProfileSchema)
|
||||
38
shared/learn/config.ts
Normal file
38
shared/learn/config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export interface LessonProgress {
|
||||
best: number
|
||||
done: boolean
|
||||
}
|
||||
|
||||
export type LearnProgress = Record<string, Record<string, LessonProgress>>
|
||||
|
||||
export interface LearnConfig {
|
||||
tts: boolean
|
||||
radioLevel: number
|
||||
voice: string
|
||||
audioChallenge: boolean
|
||||
}
|
||||
|
||||
export interface LearnState {
|
||||
xp: number
|
||||
progress: LearnProgress
|
||||
config: LearnConfig
|
||||
}
|
||||
|
||||
export const LEARN_CONFIG_DEFAULTS: LearnConfig = {
|
||||
tts: false,
|
||||
radioLevel: 4,
|
||||
voice: '',
|
||||
audioChallenge: false,
|
||||
}
|
||||
|
||||
export function createDefaultLearnConfig(): LearnConfig {
|
||||
return { ...LEARN_CONFIG_DEFAULTS }
|
||||
}
|
||||
|
||||
export function createDefaultLearnState(): LearnState {
|
||||
return {
|
||||
xp: 0,
|
||||
progress: {} as LearnProgress,
|
||||
config: createDefaultLearnConfig(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user