chore: sync all remaining changes
Release Build / build-docker (push) Successful in 52s
Release Build / build-android-and-release (push) Failing after 1m1s

- Remove android build directory (moved to CI)
- Update package dependencies
- Misc server and client improvements
This commit is contained in:
2026-04-18 22:40:25 +02:00
parent 64368b1f29
commit c1651550d1
94 changed files with 361 additions and 1287 deletions
+22
View File
@@ -10,6 +10,7 @@
"dependencies": {
"@fastify/cors": "^11.2.0",
"@fastify/jwt": "^10.0.0",
"@fastify/rate-limit": "^10.3.0",
"cors": "^2.8.5",
"fastify": "^5.6.2",
"fastify-cors": "^6.0.3",
@@ -677,6 +678,27 @@
"ipaddr.js": "^2.1.0"
}
},
"node_modules/@fastify/rate-limit": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-10.3.0.tgz",
"integrity": "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"@lukeed/ms": "^2.0.2",
"fastify-plugin": "^5.0.0",
"toad-cache": "^3.7.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+1
View File
@@ -12,6 +12,7 @@
"dependencies": {
"@fastify/cors": "^11.2.0",
"@fastify/jwt": "^10.0.0",
"@fastify/rate-limit": "^10.3.0",
"cors": "^2.8.5",
"fastify": "^5.6.2",
"fastify-cors": "^6.0.3",
+7 -4
View File
@@ -1,7 +1,8 @@
import Fastify from 'fastify';
import cors from '@fastify/cors';
import jwt from '@fastify/jwt';
import { PORT, JWT_SECRET } from './src/config/constants.js';
import rateLimit from '@fastify/rate-limit';
import { PORT } from './src/config/constants.js';
import { connectDB } from './src/config/db.js';
import routes from './src/routes/v1.js';
import { User } from './src/models/User.js';
@@ -39,6 +40,11 @@ await fastify.register(jwt, {
secret: process.env.JWT_SECRET
});
await fastify.register(rateLimit, {
max: 100,
timeWindow: '1 minute'
});
fastify.decorate('authenticate', async function (req, reply) {
try {
const payload = await req.jwtVerify();
@@ -54,9 +60,6 @@ fastify.decorate('authenticate', async function (req, reply) {
reply.code(401).send({ message: 'Session invalid', code: 'INVALID_SESSION' });
return;
}
if (payload.version !== user.tokenVersion) {
throw new Error('Session invalid');
}
req.user = user;
+1
View File
@@ -11,6 +11,7 @@ const studyItemSchema = new mongoose.Schema({
onyomi: { type: [String], default: [] },
kunyomi: { type: [String], default: [] },
nanori: { type: [String], default: [] },
mnemonic: { type: String, default: '' },
radicals: [{
meaning: String,
char: String,
+3 -1
View File
@@ -4,7 +4,9 @@ import { submitReview, submitLesson } from '../controllers/review.controller.js'
import { getStats, getQueue, getLessonQueue, getCollection, updateSettings } from '../controllers/collection.controller.js';
async function routes(fastify, options) {
fastify.post('/api/auth/login', login);
fastify.post('/api/auth/login', {
config: { rateLimit: { max: 5, timeWindow: '1 minute' } }
}, login);
fastify.register(async (privateParams) => {
privateParams.addHook('onRequest', fastify.authenticate);
+3 -3
View File
@@ -9,14 +9,14 @@ export const getUserStats = async (user) => {
{ $match: { userId: userId } },
{ $group: { _id: "$srsLevel", count: { $sum: 1 } } }
]);
const dist = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 };
const dist = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
srsCounts.forEach(g => { if (dist[g._id] !== undefined) dist[g._id] = g.count; });
const now = new Date();
const next24h = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const upcoming = await StudyItem.find({
userId: userId,
srsLevel: { $lt: 6, $gt: 0 },
srsLevel: { $lt: 10, $gt: 0 },
nextReview: { $lte: next24h }
}).select('nextReview');
@@ -29,7 +29,7 @@ export const getUserStats = async (user) => {
const queueItems = await StudyItem.find({
userId: userId,
srsLevel: { $lt: 6, $gt: 0 },
srsLevel: { $lt: 10, $gt: 0 },
nextReview: { $lte: now }
}).select('srsLevel');
const queueCount = queueItems.length;
+4
View File
@@ -81,11 +81,15 @@ export const syncWithWaniKani = async (user) => {
.map(rid => radicalMap.get(rid))
.filter(Boolean);
const rawMnemonic = d.data.meaning_mnemonic || '';
const mnemonic = rawMnemonic.replace(/<[^>]*>/g, '');
return {
userId: user._id,
wkSubjectId: d.id,
char: d.data.characters,
meaning: d.data.meanings.find(m => m.primary)?.meaning || 'Unknown',
mnemonic,
level: d.data.level,
srsLevel: 0,
nextReview: Date.now(),
+1 -1
View File
@@ -71,7 +71,7 @@ describe('Stats Service', () => {
expect(stats.ghosts[1].accuracy).toBe(60);
expect(stats.distribution[1]).toBe(5);
expect(stats.distribution[8]).toBeUndefined();
expect(stats.distribution[8]).toBe(2);
expect(stats.heatmap['2023-01-09']).toBe(5);
});
+18 -14
View File
@@ -24,22 +24,27 @@ describe('Date Utils', () => {
vi.useRealTimers();
});
it('should add correct hours for levels 1-6', () => {
const base = new Date('2023-01-01T12:00:00Z');
// Helper: getSRSDate adds hours then zeroes out minutes/seconds
const expectedDate = (hoursToAdd) => {
const d = new Date('2023-01-01T12:00:00Z');
d.setUTCHours(d.getUTCHours() + hoursToAdd);
d.setUTCMinutes(0, 0, 0);
return d.toISOString();
};
expect(getSRSDate(1).toISOString()).toBe(new Date(base.getTime() + 4 * 3600000).toISOString());
expect(getSRSDate(2).toISOString()).toBe(new Date(base.getTime() + 8 * 3600000).toISOString());
expect(getSRSDate(3).toISOString()).toBe(new Date(base.getTime() + 24 * 3600000).toISOString());
expect(getSRSDate(4).toISOString()).toBe(new Date(base.getTime() + 48 * 3600000).toISOString());
expect(getSRSDate(5).toISOString()).toBe(new Date(base.getTime() + 7 * 24 * 3600000).toISOString());
expect(getSRSDate(6).toISOString()).toBe(new Date(base.getTime() + 14 * 24 * 3600000).toISOString());
it('should add correct hours for levels 1-6', () => {
expect(getSRSDate(1).toISOString()).toBe(expectedDate(4)); // 4 hours
expect(getSRSDate(2).toISOString()).toBe(expectedDate(8)); // 8 hours
expect(getSRSDate(3).toISOString()).toBe(expectedDate(24)); // 1 day
expect(getSRSDate(4).toISOString()).toBe(expectedDate(48)); // 2 days
expect(getSRSDate(5).toISOString()).toBe(expectedDate(7 * 24)); // 1 week
expect(getSRSDate(6).toISOString()).toBe(expectedDate(14 * 24)); // 2 weeks
});
it('should add correct hours for levels 7-9', () => {
const base = new Date('2023-01-01T12:00:00Z');
expect(getSRSDate(7).toISOString()).toBe(new Date(base.getTime() + 7 * 24 * 3600000).toISOString());
expect(getSRSDate(8).toISOString()).toBe(new Date(base.getTime() + 30 * 24 * 3600000).toISOString());
expect(getSRSDate(9).toISOString()).toBe(new Date(base.getTime() + 90 * 24 * 3600000).toISOString());
expect(getSRSDate(7).toISOString()).toBe(expectedDate(30 * 24)); // 30 days
expect(getSRSDate(8).toISOString()).toBe(expectedDate(90 * 24)); // 90 days
expect(getSRSDate(9).toISOString()).toBe(expectedDate(180 * 24)); // 180 days
});
it('should return null for level 10 (burned)', () => {
@@ -47,8 +52,7 @@ describe('Date Utils', () => {
});
it('should default to 4 hours for unknown levels', () => {
const base = new Date('2023-01-01T12:00:00Z');
expect(getSRSDate(99).toISOString()).toBe(new Date(base.getTime() + 4 * 3600000).toISOString());
expect(getSRSDate(99).toISOString()).toBe(expectedDate(4));
});
});
});