finish srs system
This commit is contained in:
@@ -27,7 +27,7 @@ function createRefreshToken(user: any) {
|
||||
}
|
||||
|
||||
router.post('/login', async (req: Request, res: Response) => {
|
||||
const { email, username, password } = req.body
|
||||
const { username, password, remember } = req.body
|
||||
if (!username || !password) return res.status(400).json({ error: 'Missing credentials' })
|
||||
|
||||
try {
|
||||
@@ -40,7 +40,7 @@ router.post('/login', async (req: Request, res: Response) => {
|
||||
user = await UserModel.create({
|
||||
username: ldapUser.user.cn,
|
||||
email: ldapUser.user.dn,
|
||||
refresh_token: '',
|
||||
refreshToken: '',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,10 +51,12 @@ router.post('/login', async (req: Request, res: Response) => {
|
||||
await user.save()
|
||||
|
||||
res.cookie('access_token', accessToken, {
|
||||
httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV !== 'dev', maxAge: 15 * 60 * 1000,
|
||||
httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV !== 'dev', maxAge: 7 * 24 * 60 * 60 * 1000,
|
||||
})
|
||||
res.cookie('refresh_token', refreshToken, {
|
||||
httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV !== 'dev', maxAge: 7 * 24 * 3600 * 1000,
|
||||
const refreshMaxAge = remember > 7 ? 365 * 24 * 60 * 60 * 1000 : 7 * 24 * 60 * 60 * 1000
|
||||
|
||||
res.cookie('refreshToken', refreshToken, {
|
||||
httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV !== 'dev', maxAge: refreshMaxAge,
|
||||
})
|
||||
|
||||
res.json({
|
||||
@@ -72,12 +74,14 @@ router.post('/login', async (req: Request, res: Response) => {
|
||||
})
|
||||
|
||||
router.post('/refresh', async (req: Request, res: Response) => {
|
||||
const token = req.cookies.refresh_token
|
||||
const token = req.cookies.refreshToken
|
||||
if (!token) return res.status(401).json({ error: 'No refresh token' })
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(token, REFRESH_TOKEN_SECRET)
|
||||
const payload = jwt.verify(token, REFRESH_TOKEN_SECRET) as any
|
||||
const user = await UserModel.findById(payload.sub)
|
||||
if (!user || !user.refreshToken === token) return res.status(403).json({ error: 'Invalid refresh token' })
|
||||
if (!user || user.refreshToken !== token)
|
||||
return res.status(403).json({ error: 'Invalid refresh token' })
|
||||
|
||||
const newAccessToken = createAccessToken(user)
|
||||
const newRefreshToken = createRefreshToken(user)
|
||||
@@ -85,20 +89,33 @@ router.post('/refresh', async (req: Request, res: Response) => {
|
||||
user.refreshToken = newRefreshToken
|
||||
await user.save()
|
||||
|
||||
const existingRefreshCookie = req.cookies.refreshToken
|
||||
const decodedOld = jwt.decode(existingRefreshCookie) as any
|
||||
const remainingDays = (decodedOld.exp * 1000 - Date.now()) / (1000 * 60 * 60 * 24)
|
||||
|
||||
const refreshMaxAge = remainingDays > 7 ? 365 * 24 * 60 * 60 * 1000 : 7 * 24 * 60 * 60 * 1000
|
||||
|
||||
res.cookie('access_token', newAccessToken, {
|
||||
httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV !== 'dev', maxAge: 15 * 60 * 1000,
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: process.env.NODE_ENV !== 'dev',
|
||||
maxAge: 15 * 60 * 1000,
|
||||
})
|
||||
res.cookie('refresh_token', newRefreshToken, {
|
||||
httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV !== 'dev', maxAge: 7 * 24 * 3600 * 1000,
|
||||
res.cookie('refreshToken', newRefreshToken, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: process.env.NODE_ENV !== 'dev',
|
||||
maxAge: refreshMaxAge,
|
||||
})
|
||||
res.json({ ok: true })
|
||||
|
||||
return res.json({ ok: true })
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid refresh token' })
|
||||
return res.status(401).json({ error: 'Invalid refresh token' })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/logout', async (req: Request, res: Response) => {
|
||||
const token = req.cookies.refresh_token
|
||||
const token = req.cookies.refreshToken
|
||||
if (token) {
|
||||
try {
|
||||
const payload = jwt.verify(token, REFRESH_TOKEN_SECRET)
|
||||
@@ -110,7 +127,7 @@ router.post('/logout', async (req: Request, res: Response) => {
|
||||
} catch { }
|
||||
}
|
||||
res.clearCookie('access_token')
|
||||
res.clearCookie('refresh_token')
|
||||
res.clearCookie('refreshToken')
|
||||
res.json({ loggedOut: true })
|
||||
})
|
||||
|
||||
|
||||
@@ -27,15 +27,33 @@ router.post('/', verifyAccessToken, async (req: AuthRequest, res: Response) => {
|
||||
possibleModes.push({ key: 'writing_practice', type: 'writing_practice' })
|
||||
}
|
||||
|
||||
if (subjectOptions.direction === 'jp->en') {
|
||||
const modes = Array.isArray(subjectOptions.mode) ? subjectOptions.mode : [subjectOptions.mode]
|
||||
if (modes.includes('meaning')) possibleModes.push({ key: 'jp_en_meaning', type: 'jp_en_meaning' })
|
||||
if (modes.includes('reading')) possibleModes.push({ key: 'jp_en_reading', type: 'jp_en_reading' })
|
||||
}
|
||||
const directions = Array.isArray(subjectOptions.direction)
|
||||
? subjectOptions.direction
|
||||
: subjectOptions.direction === 'both'
|
||||
? ['jp->en', 'en->jp']
|
||||
: [subjectOptions.direction]
|
||||
|
||||
if (subjectOptions.direction === 'en->jp') {
|
||||
if (subjectOptions.mode === 'writing' || subjectOptions.writingMode) {
|
||||
possibleModes.push({ key: 'en_jp_writing', type: 'en_jp_writing' })
|
||||
for (const dir of directions) {
|
||||
if (dir === 'jp->en') {
|
||||
const modes = Array.isArray(subjectOptions.mode)
|
||||
? subjectOptions.mode
|
||||
: [subjectOptions.mode]
|
||||
if (modes.includes('meaning')) {
|
||||
possibleModes.push({ key: 'jp_en_meaning', type: 'jp_en_meaning' })
|
||||
}
|
||||
if (modes.includes('reading')) {
|
||||
possibleModes.push({ key: 'jp_en_reading', type: 'jp_en_reading' })
|
||||
}
|
||||
}
|
||||
|
||||
if (dir === 'en->jp') {
|
||||
const modes = Array.isArray(subjectOptions.mode)
|
||||
? subjectOptions.mode
|
||||
: [subjectOptions.mode]
|
||||
|
||||
if (!subjectOptions.meaning && (modes.includes('reading'))) {
|
||||
possibleModes.push({ key: 'en_jp_writing', type: 'en_jp_writing' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +107,7 @@ router.post('/correct', verifyAccessToken, async (req: AuthRequest, res: Respons
|
||||
if (req.body.currentSubjectOptions.writingPractice) {
|
||||
key = 'srs.writing_practice'
|
||||
} else if (req.body.currentSubjectOptions.direction === 'jp->en') {
|
||||
key = req.body.currentSubjectOptions.mode === 'reading' ? 'srs.jp_en_reading' : 'srs.jp_en_meaning'
|
||||
key = req.body.currentSubjectOptions.mode === 'jp_en_reading' ? 'srs.jp_en_reading' : 'srs.jp_en_meaning'
|
||||
} else {
|
||||
key = 'srs.en_jp_writing'
|
||||
}
|
||||
@@ -155,23 +173,42 @@ router.get('/stats', verifyAccessToken, async (req: AuthRequest, res: Response)
|
||||
|
||||
const now = new Date()
|
||||
|
||||
// Total counts
|
||||
const totalKanji = await ReviewItemModel.countDocuments({ userId, type: 'kanji' })
|
||||
const totalVocab = await ReviewItemModel.countDocuments({ userId, type: 'vocab' })
|
||||
|
||||
// Any of the SRS fields that are due
|
||||
const dueConditions = [
|
||||
{ 'srs.jp_en_meaning.next_review': { $lte: now } },
|
||||
{ 'srs.jp_en_reading.next_review': { $lte: now } },
|
||||
{ 'srs.en_jp_writing.next_review': { $lte: now } },
|
||||
{ 'srs.writing_practice.next_review': { $lte: now } },
|
||||
]
|
||||
|
||||
const dueItems = await ReviewItemModel.countDocuments({
|
||||
userId,
|
||||
srs_next_review: { $lte: now },
|
||||
$or: dueConditions,
|
||||
})
|
||||
|
||||
// Waiting items = have next_review set to the future in any field
|
||||
const waitingConditions = [
|
||||
{ 'srs.jp_en_meaning.next_review': { $gt: now } },
|
||||
{ 'srs.jp_en_reading.next_review': { $gt: now } },
|
||||
{ 'srs.en_jp_writing.next_review': { $gt: now } },
|
||||
{ 'srs.writing_practice.next_review': { $gt: now } },
|
||||
]
|
||||
|
||||
const waitingItems = await ReviewItemModel.countDocuments({
|
||||
userId,
|
||||
srs_next_review: { $gt: now },
|
||||
$or: waitingConditions,
|
||||
})
|
||||
|
||||
// Average SRS score per type (optional — kept from original)
|
||||
const kanjiAgg = await ReviewItemModel.aggregate([
|
||||
{ $match: { userId, type: 'kanji' } },
|
||||
{ $group: { _id: null, avgScore: { $avg: '$srs_score' } } },
|
||||
])
|
||||
|
||||
const vocabAgg = await ReviewItemModel.aggregate([
|
||||
{ $match: { userId, type: 'vocab' } },
|
||||
{ $group: { _id: null, avgScore: { $avg: '$srs_score' } } },
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import express, { type Router, type Request, type Response } from 'express'
|
||||
import { KanjiModel } from '../../../models/kanji.model.ts'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
|
||||
router.get('/', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const doc = await KanjiModel.find()
|
||||
res.json(doc)
|
||||
} catch (error) {
|
||||
console.error('Error fetching Kanji Subjects', error)
|
||||
res.status(500).json({ error: 'Failed to fetch Kanji Subjects' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router as Router
|
||||
@@ -1,18 +0,0 @@
|
||||
import express, { type Router, type Request, type Response } from 'express'
|
||||
import { VocabularyModel } from '../../../models/vocabulary.model.ts'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
|
||||
router.get('/', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const doc = await VocabularyModel.find()
|
||||
|
||||
res.json(doc)
|
||||
} catch (error) {
|
||||
console.error('Error fetching API key:', error)
|
||||
res.status(500).json({ error: 'Failed to fetch API key' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router as Router
|
||||
@@ -11,8 +11,6 @@ import { connectMongo } from './db/connect.ts'
|
||||
|
||||
import keyRouter from './api/v1/key/index.ts'
|
||||
import syncRouter from './api/v1/wanikani/sync.ts'
|
||||
import kanjiRouter from './api/v1/subject/kanji.ts'
|
||||
import vocabRouter from './api/v1/subject/vocab.ts'
|
||||
import authRouter from './api/v1/auth/index.ts'
|
||||
import userRoutes from './api/v1/user/index.ts'
|
||||
import subjectRoutes from './api/v1/subject/index.ts'
|
||||
@@ -56,11 +54,8 @@ if (process.env.NODE_ENV === 'production') {
|
||||
})
|
||||
)
|
||||
}
|
||||
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }))
|
||||
|
||||
app.use('/api/v1/key', verifyAccessToken, keyRouter)
|
||||
app.use('/api/v1/subject/kanji', verifyAccessToken, kanjiRouter)
|
||||
app.use('/api/v1/subject/vocab', verifyAccessToken, vocabRouter)
|
||||
app.use('/api/v1/user', verifyAccessToken, userRoutes)
|
||||
app.use('/api/v1/wanikani', verifyAccessToken, syncRouter)
|
||||
app.use('/api/v1/auth', authRouter)
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import mongoose from 'mongoose'
|
||||
import type { KanjiItem } from '../types/wanikani.ts'
|
||||
|
||||
const KanjiSchema = new mongoose.Schema<KanjiItem>({
|
||||
userId: { type: String, required: true },
|
||||
characters: String,
|
||||
meanings: Array,
|
||||
readings: Array,
|
||||
auxiliary_meanings: Array,
|
||||
level: Number,
|
||||
slug: String,
|
||||
srs_score: Number,
|
||||
})
|
||||
|
||||
export const KanjiModel = mongoose.model('Kanji', KanjiSchema)
|
||||
@@ -1,16 +0,0 @@
|
||||
import mongoose from 'mongoose'
|
||||
import type { VocabularyItem } from '../types/wanikani.ts'
|
||||
|
||||
const VocabSchema = new mongoose.Schema<VocabularyItem>({
|
||||
userId: { type: String, required: true },
|
||||
characters: String,
|
||||
meanings: Array,
|
||||
readings: Array,
|
||||
auxiliary_meanings: Array,
|
||||
pronunciation_audios: Array,
|
||||
level: Number,
|
||||
slug: String,
|
||||
srs_score: Number,
|
||||
})
|
||||
|
||||
export const VocabularyModel = mongoose.model('Vocabulary', VocabSchema)
|
||||
Reference in New Issue
Block a user