From 8a9916893ac8952c300ee16a851d61f0f066e735 Mon Sep 17 00:00:00 2001 From: "Navy.gif" Date: Mon, 17 Jul 2023 00:56:36 +0300 Subject: [PATCH] User client settings storage Edited some permissions --- src/server/components/UserDatabase.ts | 86 +++++++++++-------- src/server/endpoints/api/TestEndpoint.ts | 47 ---------- src/server/endpoints/api/management/Flags.ts | 2 +- src/server/endpoints/api/management/Roles.ts | 4 +- .../endpoints/api/management/Settings.ts | 24 +++++- src/server/endpoints/api/management/Users.ts | 8 +- .../interfaces/UserDatabaseInterface.ts | 6 +- src/server/structures/User.ts | 12 ++- 8 files changed, 95 insertions(+), 94 deletions(-) delete mode 100644 src/server/endpoints/api/TestEndpoint.ts diff --git a/src/server/components/UserDatabase.ts b/src/server/components/UserDatabase.ts index e4d5a7d..fc96567 100644 --- a/src/server/components/UserDatabase.ts +++ b/src/server/components/UserDatabase.ts @@ -1,4 +1,4 @@ -import { Collection } from '@discordjs/collection'; +import { Collection as ExtendedMap } from '@discordjs/collection'; import { LoggerClient } from '@navy.gif/logger'; import { MongoDB, ObjectId, Document } from '@navy.gif/wrappers'; @@ -6,7 +6,7 @@ import { MongoDB, ObjectId, Document } from '@navy.gif/wrappers'; import Server from '../Server.js'; import { Entity, UserDatabaseInterface } from '../interfaces/index.js'; import { Role, User, UserApplication } from '../structures/index.js'; -import { ApplicationData, ExternalProfile, RoleData, UserData } from '../../../@types/Server.js'; +import { ApplicationData, ExternalProfile, RoleData, UserClientSettings, UserData } from '../../../@types/Server.js'; import { Util } from '../../util/index.js'; import { SignupCode } from '../../../@types/Other.js'; import { CountResult, RoleQuery, UserQuery } from '../interfaces/UserDatabaseInterface.js'; @@ -15,6 +15,7 @@ type UserDBOptions = { userCollection?: string, appCollection?: string, roleCollection?: string, + settingsCollection?: string, disableCache?: boolean } @@ -32,12 +33,13 @@ class UserDatabase implements UserDatabaseInterface #logger: LoggerClient; #disableCache: boolean; - #cache: Collection; + #cache: ExtendedMap; // #userCollection: MongoCollection | null; - #_userCollection: string; - #_appCollection: string; - #_roleCollection: string; + #userCollection: string; + #appCollection: string; + #roleCollection: string; + #settingsCollection: string; #amount: CountResult; @@ -45,6 +47,7 @@ class UserDatabase implements UserDatabaseInterface userCollection = 'users', appCollection = 'applications', roleCollection = 'roles', + settingsCollection = 'userSettings', disableCache = false }: UserDBOptions = {}) { @@ -53,12 +56,13 @@ class UserDatabase implements UserDatabaseInterface this.#db = db; this.#server = server; this.#logger = server.createLogger(this); - this.#cache = new Collection(); + this.#cache = new ExtendedMap(); // this.#userCollection = null; - this.#_userCollection = userCollection; - this.#_appCollection = appCollection; - this.#_roleCollection = roleCollection; + this.#userCollection = userCollection; + this.#appCollection = appCollection; + this.#roleCollection = roleCollection; + this.#settingsCollection = settingsCollection; this.#disableCache = process.env.NODE_ENV === 'development' || disableCache; this.#amount = { @@ -71,8 +75,9 @@ class UserDatabase implements UserDatabaseInterface async init () { - for (const coll of [ this.#_userCollection, this.#_appCollection, this.#_roleCollection ]) + for (const coll of [ this.#userCollection, this.#appCollection, this.#roleCollection ]) await this.#db.ensureIndex(coll, [ 'name' ], { unique: true }); + await this.#db.ensureIndex(this.#settingsCollection, [ 'user' ], { unique: true }); await this.count(true); } @@ -81,9 +86,9 @@ class UserDatabase implements UserDatabaseInterface if (force) { const collections: string[] = [ - this.#_userCollection, - this.#_appCollection, - this.#_roleCollection + this.#userCollection, + this.#appCollection, + this.#roleCollection ]; this.#amount = {} as CountResult; @@ -94,6 +99,20 @@ class UserDatabase implements UserDatabaseInterface return this.#amount; } + async fetchSettings (id: string): Promise + { + const settings = await this.#db.collection(this.#settingsCollection).findOne({ user: id }) as {_id?: ObjectId}; + if (!settings) + return {}; + delete settings._id; + return settings; + } + + async updateSettings (id: string, settings: object): Promise + { + await this.#db.updateOne(this.#settingsCollection, { user: id }, settings, true); + } + async fetchUsers ({ ids, page, pageSize }: UserQuery = {}): Promise { @@ -114,7 +133,7 @@ class UserDatabase implements UserDatabaseInterface findOptions.skip = page * pageSize; } - const data = await this.#db.find(this.#_userCollection, query, findOptions); + const data = await this.#db.find(this.#userCollection, query, findOptions); const users = []; for (const user of data) { @@ -159,13 +178,12 @@ class UserDatabase implements UserDatabaseInterface data.roles = await this.fetchRoles(data.roles); return data as T; - } async fetchUser (id: string, force = false) { - const data = await this._fetch(id, this.#_userCollection, force); + const data = await this._fetch(id, this.#userCollection, force); if (!data) return null; if (data instanceof User) @@ -182,7 +200,7 @@ class UserDatabase implements UserDatabaseInterface async fetchApplication (id: string, force = false) { - const data = await this._fetch(id, this.#_appCollection, force); + const data = await this._fetch(id, this.#appCollection, force); if (!data) return null; if (data instanceof UserApplication) @@ -198,7 +216,7 @@ class UserDatabase implements UserDatabaseInterface async fetchRole (id: string, force = false) { - const data = await this._fetch(id, this.#_roleCollection, force); + const data = await this._fetch(id, this.#roleCollection, force); if (!data) return null; if (data instanceof Role) @@ -227,7 +245,7 @@ class UserDatabase implements UserDatabaseInterface findOptions.skip = page * pageSize; } - const data = await this.#db.find(this.#_roleCollection, query, findOptions); + const data = await this.#db.find(this.#roleCollection, query, findOptions); const roles = []; for (const role of data) { @@ -246,7 +264,7 @@ class UserDatabase implements UserDatabaseInterface // Retrieves the role with the lowest position, where 0 is highest async lowestRole () { - const data = await this.#db.findOne(this.#_roleCollection, {}, { sort: { position: 'desc' } }); + const data = await this.#db.findOne(this.#roleCollection, {}, { sort: { position: 'desc' } }); if (!data) return null; const role = this._createRole(data); @@ -260,7 +278,7 @@ class UserDatabase implements UserDatabaseInterface throw new Error('Missing user id'); id = id.toString(); - const data = await this.#db.find(this.#_appCollection, { ownerId: new ObjectId(id) }); + const data = await this.#db.find(this.#appCollection, { ownerId: new ObjectId(id) }); if (!data || !data.length) return []; @@ -286,7 +304,7 @@ class UserDatabase implements UserDatabaseInterface if (user) return Promise.resolve(user); - const data = await this.#db.findOne(this.#_userCollection, { name }, { collation: { locale: 'en', strength: 2 } }); + const data = await this.#db.findOne(this.#userCollection, { name }, { collation: { locale: 'en', strength: 2 } }); if (!data) return null; @@ -319,7 +337,7 @@ class UserDatabase implements UserDatabaseInterface if (app) return Promise.resolve(app); - const data = await this.#db.findOne(this.#_appCollection, { token }); + const data = await this.#db.findOne(this.#appCollection, { token }); if (!data) return null; @@ -350,7 +368,7 @@ class UserDatabase implements UserDatabaseInterface if (user) return Promise.resolve(user); - const data = await this.#db.findOne(this.#_userCollection, { 'externalProfiles.discord.id': profile.id }); + const data = await this.#db.findOne(this.#userCollection, { 'externalProfiles.discord.id': profile.id }); if (data) { user = await this._createUser(data); @@ -362,7 +380,7 @@ class UserDatabase implements UserDatabaseInterface return null; // If a user with the same username already exists, force the holder of the discord account to create a new account or log in to the other account and link them - const existing = await this.#db.findOne(this.#_userCollection, { name: profile.username }, { collation: { locale: 'en', strength: 2 } }); + const existing = await this.#db.findOne(this.#userCollection, { name: profile.username }, { collation: { locale: 'en', strength: 2 } }); if (existing) { const temp = await this._createUser({ temporary: true, displayName: profile.username }); @@ -402,7 +420,7 @@ class UserDatabase implements UserDatabaseInterface const json = user.jsonPrivate as {_id?: ObjectId, id?: string}; json._id = new ObjectId(json.id); delete json.id; - await this.#db.insertOne(this.#_userCollection, json); + await this.#db.insertOne(this.#userCollection, json); // await this.updateUser(user); if (!this.#disableCache) this.#cache.set(user.id, user); @@ -417,7 +435,7 @@ class UserDatabase implements UserDatabaseInterface const json = app.jsonPrivate as {_id?: ObjectId, id?: string}; json._id = new ObjectId(json.id); delete json.id; - await this.#db.insertOne(this.#_appCollection, json); + await this.#db.insertOne(this.#appCollection, json); if (!this.#disableCache) this.#cache.set(app.id, app); this.#amount.applications++; @@ -436,7 +454,7 @@ class UserDatabase implements UserDatabaseInterface } const role = this._createRole(data); - await this.#db.insertOne(this.#_roleCollection, role.jsonPrivate); + await this.#db.insertOne(this.#roleCollection, role.jsonPrivate); if (!this.#disableCache) this.#cache.set(role.id, role); this.#amount.roles++; @@ -445,12 +463,12 @@ class UserDatabase implements UserDatabaseInterface async deleteApplication (id: string) { - return void await this.#db.deleteOne(this.#_appCollection, { _id: new ObjectId(id) }); + return void await this.#db.deleteOne(this.#appCollection, { _id: new ObjectId(id) }); } deleteRole (id: string) { - return this.#db.deleteOne(this.#_roleCollection, { _id: new ObjectId(id) }); + return this.#db.deleteOne(this.#roleCollection, { _id: new ObjectId(id) }); } /** @@ -493,17 +511,17 @@ class UserDatabase implements UserDatabaseInterface */ updateUser (user: User) { - return this._updateEntity(user, this.#_userCollection); + return this._updateEntity(user, this.#userCollection); } updateApplication (app: UserApplication) { - return this._updateEntity(app, this.#_appCollection); + return this._updateEntity(app, this.#appCollection); } updateRole (role: Role) { - return this._updateEntity(role, this.#_roleCollection); + return this._updateEntity(role, this.#roleCollection); } diff --git a/src/server/endpoints/api/TestEndpoint.ts b/src/server/endpoints/api/TestEndpoint.ts deleted file mode 100644 index 9b66f4a..0000000 --- a/src/server/endpoints/api/TestEndpoint.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Request, Response } from '../../../../@types/Server.js'; -import { PermissionManager } from '../../../util/index.js'; -import Server from '../../Server.js'; -import VersionedEndpoint from '../../interfaces/VersionedEndpoint.js'; - -class TestEndpoint extends VersionedEndpoint -{ - - constructor (server: Server) - { - super(server, { - name: 'test', - path: '/test', - version: 2, - auth: true - }); - - this.methods = [ - [ 'get', this.get.bind(this) ] - ]; - - this.subpaths = [ - [ 'get', '/dingus/:thing', this.dingus.bind(this) ] - ]; - - this.middleware = [ server.auth.createAuthoriser('testing:bruh') ]; - - } - - get (_req: Request, res: Response) - { - res.send('duh!'); - } - - dingus (_req: Request, res: Response) - { - res.status(200).json({ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - routes: this.server.app._router.stack.filter((elem) => elem.route).map(elem => elem.route.path).sort(), - permissions: PermissionManager.DefaultPermissions - }); - } - -} - -export default TestEndpoint; \ No newline at end of file diff --git a/src/server/endpoints/api/management/Flags.ts b/src/server/endpoints/api/management/Flags.ts index bb63cf2..5289a56 100644 --- a/src/server/endpoints/api/management/Flags.ts +++ b/src/server/endpoints/api/management/Flags.ts @@ -133,7 +133,7 @@ class Flags extends ApiEndpoint flag.env = body.env; if (body.consumer) flag.consumer = body.consumer; - if (body.hierarchy) + if ('hierarchy' in body) flag.hierarchy = body.hierarchy; if (flag.value) try diff --git a/src/server/endpoints/api/management/Roles.ts b/src/server/endpoints/api/management/Roles.ts index a6d7bbf..fd63320 100644 --- a/src/server/endpoints/api/management/Roles.ts +++ b/src/server/endpoints/api/management/Roles.ts @@ -26,8 +26,8 @@ class Roles extends ApiEndpoint this.subpaths = [ [ 'delete', '/:roleid', this.deleteRole.bind(this), server.auth.createAuthoriser('administrator:roles:delete', 5) ], [ 'patch', '/:roleid', this.modifyRole.bind(this), server.auth.createAuthoriser('administrator:roles:edit', 5) ], - [ 'patch', '/:roleid/permissions', this.modifyPerms.bind(this), server.auth.createAuthoriser('administrator:roles:edit', 5) ], - [ 'patch', '/:roleid/ratelimits', this.modifyLimits.bind(this), server.auth.createAuthoriser('administrator:roles:edit', 5) ] + [ 'patch', '/:roleid/permissions', this.modifyPerms.bind(this), server.auth.createAuthoriser('administrator:roles:permissions', 5) ], + [ 'patch', '/:roleid/ratelimits', this.modifyLimits.bind(this), server.auth.createAuthoriser('administrator:roles:ratelimits', 5) ] ]; this.#users = server.users; diff --git a/src/server/endpoints/api/management/Settings.ts b/src/server/endpoints/api/management/Settings.ts index e47ab58..9bdc9b3 100644 --- a/src/server/endpoints/api/management/Settings.ts +++ b/src/server/endpoints/api/management/Settings.ts @@ -1,7 +1,8 @@ // const { ApiEndpoint } = require("../../../interfaces"); -import { Request, Response } from '../../../../../@types/Server.js'; +import { ClientSettings, Request, Response } from '../../../../../@types/Server.js'; import Server from '../../../Server.js'; import { ApiEndpoint } from '../../../interfaces/index.js'; +import User from '../../../structures/User.js'; class SettingsEndpoint extends ApiEndpoint { @@ -14,16 +15,31 @@ class SettingsEndpoint extends ApiEndpoint }); this.methods = [ - [ 'get', this.getSettings.bind(this) ] + [ 'get', this.getSettings.bind(this) ], + [ 'post', this.updateSettings.bind(this), [ server.auth.authenticate ]] ]; // this.middleware = [ server.auth.authenticate ]; } - getSettings (_req: Request, res: Response) + async getSettings (req: Request, res: Response) { - res.json(this.server.clientSettings); + const { user } = req; + const settings: ClientSettings = { global: this.server.clientSettings }; + if (user && user instanceof User) + settings.user = await user.clientSettings(); + + res.json(settings); + } + + async updateSettings (req: Request, res: Response): Promise + { + const { user, body } = req; + if (!(user instanceof User)) + return void res.status(403).send('Only user accounts can edit settings'); + await user.updateClientSettings(body); + res.end(); } } diff --git a/src/server/endpoints/api/management/Users.ts b/src/server/endpoints/api/management/Users.ts index 9cbd4e1..a25970a 100644 --- a/src/server/endpoints/api/management/Users.ts +++ b/src/server/endpoints/api/management/Users.ts @@ -17,12 +17,12 @@ class UsersEndpoint extends ApiEndpoint path: '/users', }); - this.methods.push([ 'get', this.getUsers.bind(this), [ server.auth.createAuthoriser('administrator', 5) ]]); + this.methods.push([ 'get', this.getUsers.bind(this), [ server.auth.createAuthoriser('administrator:users', 5) ]]); this.subpaths = [ - [ 'get', '/:userid', this.user.bind(this), [ server.auth.createAuthoriser('administrator', 5) ]], + [ 'get', '/:userid', this.user.bind(this), [ server.auth.createAuthoriser('administrator:users', 5) ]], [ 'get', '/:userid/avatar', this.avatar.bind(this) ], - [ 'get', '/:userid/applications', this.userApplications.bind(this), [ server.auth.createAuthoriser('administrator', 5) ]], - [ 'post', '/:userid/permissions', this.updatePermissions.bind(this), [ server.auth.createAuthoriser('administrator', 5) ]] + [ 'get', '/:userid/applications', this.userApplications.bind(this), [ server.auth.createAuthoriser('administrator:users:applications', 5) ]], + [ 'post', '/:userid/permissions', this.updatePermissions.bind(this), [ server.auth.createAuthoriser('administrator:users:permissions', 5) ]] ]; // this.middleware = [ server.auth.createAuthoriser('administrator', 10) ]; diff --git a/src/server/interfaces/UserDatabaseInterface.ts b/src/server/interfaces/UserDatabaseInterface.ts index 49c7534..a1c2703 100644 --- a/src/server/interfaces/UserDatabaseInterface.ts +++ b/src/server/interfaces/UserDatabaseInterface.ts @@ -1,6 +1,6 @@ import { DeleteResult } from '@navy.gif/wrappers'; import { SignupCode } from '../../../@types/Other.js'; -import { ApplicationData, ExternalProfile, RoleData, UserData } from '../../../@types/Server.js'; +import { ApplicationData, ExternalProfile, RoleData, UserClientSettings, UserData } from '../../../@types/Server.js'; import { Role, User, UserApplication } from '../structures/index.js'; export type Query = { @@ -43,6 +43,10 @@ interface UserDatabaseInterface { updateUser(user: User): Promise + fetchSettings(userId: string): Promise + + updateSettings(userId: string, settings: UserClientSettings): Promise + fetchUserApplications(id: string): Promise fetchApplication(id: string, force?: boolean): Promise diff --git a/src/server/structures/User.ts b/src/server/structures/User.ts index c404bcf..aca1f58 100644 --- a/src/server/structures/User.ts +++ b/src/server/structures/User.ts @@ -4,7 +4,7 @@ import Argon2 from 'argon2'; import { AbstractUser } from '../interfaces/index.js'; import Server from '../Server.js'; -import { ExternalProfile, UserData } from '../../../@types/Server.js'; +import { ExternalProfile, UserClientSettings, UserData } from '../../../@types/Server.js'; // Fields omitted in the json getter // Should be keys used in jsonPrivate, not the ones defined in the constructor @@ -39,6 +39,16 @@ class User extends AbstractUser } + clientSettings (): Promise + { + return this.db.fetchSettings(this.id); + } + + async updateClientSettings (settings: UserClientSettings): Promise + { + await this.db.updateSettings(this.id, settings); + } + override save () { return this.db.updateUser(this);