From 2996e8a8c1663d06b6277c866ce088ead4a961c8 Mon Sep 17 00:00:00 2001 From: Nikky Date: Sun, 2 Jan 2022 09:09:12 +0300 Subject: [PATCH] better types --- package-lock.json | 25 +++++++++++++++---------- package.json | 1 + src/config/email.config.js | 10 +++++----- src/config/server.config.js | 5 +++++ src/index.js | 19 +++++++++++++------ src/logic/cache.js | 2 +- src/logic/session.js | 7 ++++--- src/logic/utils.js | 6 ++++++ src/model/model.d.ts | 5 ++++- src/model/user.d.ts | 19 +++++++++++++++++++ src/model/user.model.js | 13 +++++++++---- src/route/auth.controller.js | 17 ++++++++++------- src/route/route.d.ts | 4 ++-- src/route/user.controller.js | 27 ++++++++++++++++++++++----- src/type/database.d.ts | 9 +++++---- src/type/webserver.d.ts | 10 +++++++--- 16 files changed, 128 insertions(+), 51 deletions(-) create mode 100644 src/config/server.config.js create mode 100644 src/model/user.d.ts diff --git a/package-lock.json b/package-lock.json index aca09c9..8c8d9bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -310,6 +310,11 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "optional": true }, + "chalk": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.0.tgz", + "integrity": "sha512-/duVOqst+luxCQRKEo4bNxinsOQtMP80ZYm7mMqzuh5PociNL0PvmHFvREJ9ueYL2TxlHjBcmLCdmocx9Vg+IQ==" + }, "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -998,9 +1003,9 @@ "optional": true }, "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "optional": true }, "json-schema-traverse": { @@ -1015,14 +1020,14 @@ "optional": true }, "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "optional": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" } }, @@ -2056,9 +2061,9 @@ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "validator": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.6.0.tgz", - "integrity": "sha512-gVgKbdbHgtxpRyR8K0O6oFZPhhB5tT1jeEHZR0Znr9Svg03U0+r9DXWMrnRAB+HtCStDQKlaIZm42tVsVjqtjg==" + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" }, "verror": { "version": "1.10.0", diff --git a/package.json b/package.json index 07ee53b..8f25304 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "license": "ISC", "dependencies": { "bcrypt": "^5.0.1", + "chalk": "^5.0.0", "fastify": "^3.22.0", "fastify-formbody": "^5.1.0", "fastify-multipart": "^5.0.2", diff --git a/src/config/email.config.js b/src/config/email.config.js index 382d38a..b7ee8de 100644 --- a/src/config/email.config.js +++ b/src/config/email.config.js @@ -1,11 +1,11 @@ export const MailgunAccount = { - apiKey: '0f5cb2ebb348c88c708f1b9f7e20c385-e5da0167-1b06a230', - publicApiKey: 'pubkey-933b975a7865e74f365bbd6e0bce889d', + apiKey: '6ba1ce11ad5f5cd9170b94176dfe07ef-e5da0167-acb94f76', + publicApiKey: 'pubkey-e3afb52f411587bd80aa2febd56b5f48', host: 'api.eu.mailgun.net', - domain: '7winds.nikky.dev' + domain: 'mail.headpat.network' }; export const EmailConfig = { - from: "Jun Kuroshio " -} + from: "Black Tide 🌊🖤 " +}; diff --git a/src/config/server.config.js b/src/config/server.config.js new file mode 100644 index 0000000..a382545 --- /dev/null +++ b/src/config/server.config.js @@ -0,0 +1,5 @@ + +export const ServerConfig = { + port: 3001, + host: '127.0.0.1' +}; diff --git a/src/index.js b/src/index.js index 802d9ee..412a397 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,10 @@ +import chalk from 'chalk'; import Fastify from 'fastify' import formBodyPlugin from 'fastify-formbody'; import fastifyMultipart from 'fastify-multipart'; import Sequelize from 'sequelize'; import { DatabaseConfig } from './config/database.config.js'; +import { ServerConfig } from './config/server.config.js'; import UserEntity from './model/user.model.js'; import AuthController from './route/auth.controller.js'; import UserController from './route/user.controller.js'; @@ -15,7 +17,7 @@ async function Database(){ const opts = {db}; UserEntity(opts); } - await db.authenticate(); //connect to the database of choice + await db.authenticate(); //connect to your database of choice await db.sync({alter: true}); //add missing tables and columns } @@ -28,17 +30,22 @@ async function WebApp(){ UserController(opts); } app.get('/', () => 'Hello. D:'); - const options = { port: 3000, host: '127.0.0.1' }; - const address = await app.listen(options); - return `Server is listening on ${address}.`; + const address = await app.listen(ServerConfig); + return chalk.magenta(`Server is listening on ${address}.`); } (async () => { //initialize const step = async (func, name) => { const start = Date.now(); - const msg = await func(); - console.log(`[${name}] (${(Date.now() - start)} ms) ${msg || 'Done.'}`); + const tag = chalk.yellow(name); + try{ + const msg = await func(); + console.log(`[${tag}] (${(Date.now() - start)} ms) ${msg || chalk.green('Done.')}`); + }catch(err){ + console.log(`[${tag}] ${chalk.red('Failure.')}`); + throw err; + } }; await step(Database, 'DB'); await step(WebApp, 'Fastify'); diff --git a/src/logic/cache.js b/src/logic/cache.js index 55a7dbe..5949d5e 100644 --- a/src/logic/cache.js +++ b/src/logic/cache.js @@ -40,7 +40,7 @@ export class SpamCache extends CacheTable{ /** * @param string key * @param {[[maxHits: number, durationMs: number]]} constraints - * @returns + * @returns {boolean} */ check(key, constraints){ const now = Date.now(); diff --git a/src/logic/session.js b/src/logic/session.js index e235b61..65d8f35 100644 --- a/src/logic/session.js +++ b/src/logic/session.js @@ -2,7 +2,10 @@ import { CacheTable, SpamCache } from "./cache.js"; import { ipAddress } from "./utils.js"; export class UserSession{ - + + /** + * @param {import('../model/user').UserEntity} user + */ constructor(user, request){ this.user = user; this.createdAt = Date.now(); @@ -19,8 +22,6 @@ export class UserSession{ return userCache.get(token); } static onUpdate(user){ - //todo: figure out where exactly - //this hook is necessary, if it is const session = find(user); if(session) session.user = user; } diff --git a/src/logic/utils.js b/src/logic/utils.js index 760d5f8..fbc31a1 100644 --- a/src/logic/utils.js +++ b/src/logic/utils.js @@ -16,6 +16,12 @@ export function ipAddress(request){ return request.ip; } +export function localeFromHeader(input){ + //not sure what to do with this yet. + //https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language + return checkStringParam(input, 1, 64) ? input : 'en'; +} + export function reverseString(str){ return str.split("").reverse().join(""); } diff --git a/src/model/model.d.ts b/src/model/model.d.ts index fc878c8..60d6b9a 100644 --- a/src/model/model.d.ts +++ b/src/model/model.d.ts @@ -1,5 +1,8 @@ import { database } from '../type/database'; +import { Model } from 'sequelize/types'; export interface props{ - db: database + db: database }; + +export type model = Model; diff --git a/src/model/user.d.ts b/src/model/user.d.ts new file mode 100644 index 0000000..6ce5f1d --- /dev/null +++ b/src/model/user.d.ts @@ -0,0 +1,19 @@ +import { model } from "./model" +import { UserRolesEnum } from "./user.model"; + +export type UserEntity = model & { + uuid: string, + token: string, + tokenExpiry: Date, + email: string, + emailConfirmed: boolean, + username: string, + passwd: string, + role: typeof UserRolesEnum, + firstIp: string, + firstLocale: string, + bannedUntil: Date, + banReason: string, + restoreCode: string, + restoreExpiry: Date +}; diff --git a/src/model/user.model.js b/src/model/user.model.js index b9b7a26..fc1aeb7 100644 --- a/src/model/user.model.js +++ b/src/model/user.model.js @@ -9,14 +9,15 @@ const UserEntity = ({db}) => ( uuid: {type: Sequelize.DataTypes.UUID, defaultValue: Sequelize.UUIDV4}, token: Sequelize.TEXT, tokenExpiry: Sequelize.DATE, - email: Sequelize.TEXT, + email: Sequelize.TEXT, + emailConfirmed: Sequelize.BOOLEAN, username: Sequelize.TEXT, paswd: Sequelize.TEXT, - role: Sequelize.TEXT, + role: {type: Sequelize.ENUM(UserRolesEnum), defaultValue: 'user'}, firstIp: Sequelize.TEXT, + firstLocale: Sequelize.TEXT, bannedUntil: Sequelize.DATE, banReason: Sequelize.TEXT, - restoreCode: Sequelize.TEXT, restoreExpiry: Sequelize.DATE }, { @@ -31,4 +32,8 @@ const UserEntity = ({db}) => ( }) ); -export default UserEntity; \ No newline at end of file +export const UserRolesEnum = [ + 'user', 'admin', 'moderator' +]; + +export default UserEntity; diff --git a/src/route/auth.controller.js b/src/route/auth.controller.js index e24ad40..decd2bc 100644 --- a/src/route/auth.controller.js +++ b/src/route/auth.controller.js @@ -1,4 +1,4 @@ -import { checkStringParam, errorOut, notYet, randomElement, ipAddress, reverseString, hours, minutes } from '../logic/utils.js'; +import { checkStringParam, errorOut, notYet, randomElement, ipAddress, reverseString, hours, minutes, localeFromHeader } from '../logic/utils.js'; import { Animals } from '../misc/animals.js'; import { reissueToken, generateToken, newTokenExpiry, hashPassword, doesPasswordMatch, isEndpointAllowedForBannedUsers, isEndpointProtected, generateRestoreCode, restoreValidity, restoreAttempts } from '../logic/security.js'; import { sendRestorationLink } from '../logic/email.js'; @@ -25,6 +25,7 @@ function AuthController({app, db}){ let session = UserSession.find(token); if(!session){ const userData = await Users.findOne({where: {token}}); + userData.role = '232'; if(userData){ session = new UserSession(userData, request); } @@ -94,15 +95,17 @@ function AuthController({app, db}){ if(countByIp >= 10) return errorOut(reply, 'error.suspicious'); } const newUser = { - email: fixedEmail, - username: fixedUsername, - token: generateToken(), + email: fixedEmail, + emailConfirmed: false, + username: fixedUsername, + token: generateToken(), tokenExpiry: newTokenExpiry(), - paswd: hashPassword(reverseString(paswd)), - role: 'user', - firstIp: ipAddress(request) + paswd: hashPassword(reverseString(paswd)), + firstIp: ipAddress(request), + firstLocale: localeFromHeader(request.headers['accept-language']) }; await Users.create(newUser); + //todo: email notify return {token: newUser.token}; }); diff --git a/src/route/route.d.ts b/src/route/route.d.ts index 90c0c16..eead615 100644 --- a/src/route/route.d.ts +++ b/src/route/route.d.ts @@ -2,6 +2,6 @@ import { webserver } from '../type/webserver'; import { database } from '../type/database'; export interface props{ - app: webserver, - db: database + app: webserver, + db: database }; diff --git a/src/route/user.controller.js b/src/route/user.controller.js index df08f1d..2d1dab7 100644 --- a/src/route/user.controller.js +++ b/src/route/user.controller.js @@ -6,11 +6,14 @@ import { reissueToken, tokenLifetime, tokenReissue } from '../logic/security.js' function UserController({app, db}){ const { Users } = db.models; - app.get('/users/me', async (request, reply) => { - const {session} = request; - const user = session.user; - const needsReissue = ((user.tokenExpiry - new Date()) < (tokenLifetime - tokenReissue)); - let newToken = needsReissue ? await reissueToken(user) : null; + app.get('/users/me', async (request) => { + const {user} = request.session; + + let newToken; + { // issue a new token if near expired + const needsReissue = ((user.tokenExpiry - new Date()) < (tokenLifetime - tokenReissue)); + if(needsReissue) newToken = await reissueToken(user); + } return { username: user.username, @@ -21,6 +24,20 @@ function UserController({app, db}){ }; }); + app.delete('/users/me', async (request, reply) => { + const {paswd} = request.body || {}; + const {user} = request.session; + + { //form validation + const paswdValid = isValidPassword(paswd); + const doesMatch = paswdValid && doesPasswordMatch(reverseString(paswd), user.paswd); + if(!doesMatch) return errorOut(reply, 'auth.bad_paswd'); + } + + await user.destroy(); + //todo: email notify + return '😔'; + }); } export default UserController; diff --git a/src/type/database.d.ts b/src/type/database.d.ts index ecf98a3..e8a9d75 100644 --- a/src/type/database.d.ts +++ b/src/type/database.d.ts @@ -1,7 +1,8 @@ -import { Sequelize } from 'sequelize/types'; +import { Model, ModelCtor, Sequelize } from 'sequelize/types'; +import { UserEntity } from '../model/user'; export type database = Sequelize & { - models: { - potato: string - } + models: { + Users: ModelCtor + } }; diff --git a/src/type/webserver.d.ts b/src/type/webserver.d.ts index 47dae4b..f75d9e7 100644 --- a/src/type/webserver.d.ts +++ b/src/type/webserver.d.ts @@ -1,6 +1,10 @@ import { FastifyInstance } from 'fastify/types/instance'; import { UserSession } from '../logic/session.js'; -export type webserver = FastifyInstance & { - session: UserSession -}; +declare module 'fastify' { + interface FastifyRequest { + session: UserSession + } +} + +export type webserver = FastifyInstance;