diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..b2095be --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3a77f1d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "editor.tabSize": 2, + "[vue]": { + "editor.defaultFormatter": "octref.vetur", + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + } +} diff --git a/src/api/index.ts b/src/api/index.ts index 57ff78f..c3aec14 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -11,6 +11,7 @@ import fastifySwagger from 'fastify-swagger' import { db, isDev } from '../shared' import { searchRouter } from './search' +import { userRouter } from './user' const apiRouter: FastifyPluginAsync = async (f) => { f.register(fCookie) @@ -129,36 +130,39 @@ const apiRouter: FastifyPluginAsync = async (f) => { f.register(searchRouter, { prefix: '/search', }) + + f.register(userRouter, { + prefix: '/user', + }) } export default apiRouter -async function main() { - const app = fastify({ - logger: { - prettyPrint: isDev, - serializers: { - req(req) { - if (!req.url) { - return { method: req.method } - } +if (require.main === module) { + async function main() { + const app = fastify({ + logger: { + prettyPrint: isDev, + serializers: { + req(req) { + if (!req.url) { + return { method: req.method } + } - const [url = '', q] = req.url.split(/\?(.+)$/) - const query = q ? qs.parse(q) : undefined + const [url = '', q] = req.url.split(/\?(.+)$/) + const query = q ? qs.parse(q) : undefined - return { method: req.method, url, query } + return { method: req.method, url, query } + }, }, }, - }, - }) + }) - app.register(apiRouter, { - prefix: '/api', - }) + app.register(apiRouter, { + prefix: '/api', + }) - return app.listen(process.env['PORT']!, '0.0.0.0') -} - -if (require.main === module) { + return app.listen(process.env['PORT']!, '0.0.0.0') + } main() } diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..eca468a --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,32 @@ +import S from 'jsonschema-definer' + +export const sTypeLevel = S.string().enum('character', 'vocabulary') +export const sType = S.anyOf(sTypeLevel, S.string().enum('sentence')) + +export const sLevelBrowserByType = S.shape({ + whatToShow: S.list(S.string()), +}) + +const oLevelBrowser: Record< + typeof sTypeLevel.type, + typeof sLevelBrowserByType +> = { + character: sLevelBrowserByType, + vocabulary: sLevelBrowserByType, +} + +export const sLevelBrowser = S.shape(oLevelBrowser) + +const oUserLocalSettings = { + level: S.integer(), + levelMin: S.integer().optional(), + level_browser: sLevelBrowser.optional(), +} + +const oUserSettings = { + ...oUserLocalSettings, + identifier: S.string(), +} + +export const sUserLocalSettings = S.shape(oUserLocalSettings) +export const sUserSettings = S.shape(oUserSettings) diff --git a/src/api/user.ts b/src/api/user.ts new file mode 100644 index 0000000..d2499d3 --- /dev/null +++ b/src/api/user.ts @@ -0,0 +1,126 @@ +import { SQLQuery, sql } from '@databases/pg' +import { FastifyPluginAsync } from 'fastify' +import S from 'jsonschema-definer' + +import { db } from '../shared' +import { sUserSettings } from './types' + +export const userRouter: FastifyPluginAsync = async (f) => { + { + const sQuery = S.shape({ + select: S.string(), + }) + + const sResponse = sUserSettings.partial() + + f.get<{ + Querystring: typeof sQuery.type + }>( + '/', + { + schema: { + operationId: 'userGetSettings', + querystring: sQuery.valueOf(), + response: { + 200: sResponse.valueOf(), + }, + }, + }, + async (req): Promise => { + const select = req.query.select.split( + ',' + ) as (keyof typeof sUserSettings.type)[] + + const userId: string = req.session['userId'] + if (!userId) { + throw { statusCode: 403 } + } + + const selMap: Record = { + identifier: sql`"identifier"`, + level: sql`"level"`, + levelMin: sql`"levelMin"`, + level_browser: sql`"level_browser"`, + } + + const sel = select.map((s) => selMap[s]).filter((s) => s) + + if (!sel.length) { + throw { statusCode: 400, message: 'not enough select' } + } + + const [r] = await db.query( + sql` + SELECT ${sql.join(sel, ',')} FROM "user" WHERE "id" = ${userId} + ` + ) + if (!r) { + throw { statusCode: 403 } + } + + return { + identifier: r.identifier || undefined, + level: r.level || undefined, + levelMin: r.levelMin || undefined, + level_browser: r.levelBrowser || undefined, + } + } + ) + } + + { + const sBody = sUserSettings.partial() + + const sResult = S.shape({ + result: S.string(), + }) + + f.patch<{ + Body: Partial + }>( + '/', + { + schema: { + operationId: 'userUpdateSettings', + body: sBody.valueOf(), + response: { + 201: sResult.valueOf(), + }, + }, + }, + async (req, reply): Promise => { + const userId: string = req.session['userId'] + if (!userId) { + throw { statusCode: 403 } + } + + if (!Object.keys(req.body).length) { + throw { statusCode: 400, message: 'not enough update' } + } + + const uMap: Partial> = + { + level: sql`"level" = ${req.body.level}`, + levelMin: sql`"levelMin" = ${req.body.levelMin}`, + level_browser: sql`"level_browser" = ${req.body.level_browser}`, + } + + await db.tx(async (db) => { + await db.query(sql` + UPDATE "user" + SET ${sql.join( + Object.keys(req.body).map((k) => (uMap as any)[k]), + ',' + )} + WHERE "id" = ${userId} + `) + }) + + reply.status(201) + return { + result: 'updated', + } + } + ) + } +}