Compare commits

...

2 Commits

Author SHA1 Message Date
f908620c46
typings 2023-07-17 00:56:49 +03:00
8a9916893a
User client settings storage
Edited some permissions
2023-07-17 00:56:36 +03:00
9 changed files with 104 additions and 94 deletions

View File

@ -182,3 +182,12 @@ export type RegistrationInvite = {
validFor: number, validFor: number,
created: number created: number
} }
// Types for these don't matter for the server as they're only used by the client
export type GlobalClientSettings = object
export type UserClientSettings = object
export type ClientSettings = {
global: GlobalClientSettings,
user?: UserClientSettings
}

View File

@ -1,4 +1,4 @@
import { Collection } from '@discordjs/collection'; import { Collection as ExtendedMap } from '@discordjs/collection';
import { LoggerClient } from '@navy.gif/logger'; import { LoggerClient } from '@navy.gif/logger';
import { MongoDB, ObjectId, Document } from '@navy.gif/wrappers'; 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 Server from '../Server.js';
import { Entity, UserDatabaseInterface } from '../interfaces/index.js'; import { Entity, UserDatabaseInterface } from '../interfaces/index.js';
import { Role, User, UserApplication } from '../structures/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 { Util } from '../../util/index.js';
import { SignupCode } from '../../../@types/Other.js'; import { SignupCode } from '../../../@types/Other.js';
import { CountResult, RoleQuery, UserQuery } from '../interfaces/UserDatabaseInterface.js'; import { CountResult, RoleQuery, UserQuery } from '../interfaces/UserDatabaseInterface.js';
@ -15,6 +15,7 @@ type UserDBOptions = {
userCollection?: string, userCollection?: string,
appCollection?: string, appCollection?: string,
roleCollection?: string, roleCollection?: string,
settingsCollection?: string,
disableCache?: boolean disableCache?: boolean
} }
@ -32,12 +33,13 @@ class UserDatabase implements UserDatabaseInterface
#logger: LoggerClient; #logger: LoggerClient;
#disableCache: boolean; #disableCache: boolean;
#cache: Collection<string, Entity>; #cache: ExtendedMap<string, Entity>;
// #userCollection: MongoCollection<Entity> | null; // #userCollection: MongoCollection<Entity> | null;
#_userCollection: string; #userCollection: string;
#_appCollection: string; #appCollection: string;
#_roleCollection: string; #roleCollection: string;
#settingsCollection: string;
#amount: CountResult; #amount: CountResult;
@ -45,6 +47,7 @@ class UserDatabase implements UserDatabaseInterface
userCollection = 'users', userCollection = 'users',
appCollection = 'applications', appCollection = 'applications',
roleCollection = 'roles', roleCollection = 'roles',
settingsCollection = 'userSettings',
disableCache = false disableCache = false
}: UserDBOptions = {}) }: UserDBOptions = {})
{ {
@ -53,12 +56,13 @@ class UserDatabase implements UserDatabaseInterface
this.#db = db; this.#db = db;
this.#server = server; this.#server = server;
this.#logger = server.createLogger(this); this.#logger = server.createLogger(this);
this.#cache = new Collection(); this.#cache = new ExtendedMap();
// this.#userCollection = null; // this.#userCollection = null;
this.#_userCollection = userCollection; this.#userCollection = userCollection;
this.#_appCollection = appCollection; this.#appCollection = appCollection;
this.#_roleCollection = roleCollection; this.#roleCollection = roleCollection;
this.#settingsCollection = settingsCollection;
this.#disableCache = process.env.NODE_ENV === 'development' || disableCache; this.#disableCache = process.env.NODE_ENV === 'development' || disableCache;
this.#amount = { this.#amount = {
@ -71,8 +75,9 @@ class UserDatabase implements UserDatabaseInterface
async init () 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(coll, [ 'name' ], { unique: true });
await this.#db.ensureIndex(this.#settingsCollection, [ 'user' ], { unique: true });
await this.count(true); await this.count(true);
} }
@ -81,9 +86,9 @@ class UserDatabase implements UserDatabaseInterface
if (force) if (force)
{ {
const collections: string[] = [ const collections: string[] = [
this.#_userCollection, this.#userCollection,
this.#_appCollection, this.#appCollection,
this.#_roleCollection this.#roleCollection
]; ];
this.#amount = {} as CountResult; this.#amount = {} as CountResult;
@ -94,6 +99,20 @@ class UserDatabase implements UserDatabaseInterface
return this.#amount; return this.#amount;
} }
async fetchSettings (id: string): Promise<UserClientSettings>
{
const settings = await this.#db.collection<UserClientSettings>(this.#settingsCollection).findOne({ user: id }) as {_id?: ObjectId};
if (!settings)
return {};
delete settings._id;
return settings;
}
async updateSettings (id: string, settings: object): Promise<void>
{
await this.#db.updateOne(this.#settingsCollection, { user: id }, settings, true);
}
async fetchUsers ({ ids, page, pageSize }: UserQuery = {}): Promise<User[]> async fetchUsers ({ ids, page, pageSize }: UserQuery = {}): Promise<User[]>
{ {
@ -114,7 +133,7 @@ class UserDatabase implements UserDatabaseInterface
findOptions.skip = page * pageSize; findOptions.skip = page * pageSize;
} }
const data = await this.#db.find<UserData>(this.#_userCollection, query, findOptions); const data = await this.#db.find<UserData>(this.#userCollection, query, findOptions);
const users = []; const users = [];
for (const user of data) for (const user of data)
{ {
@ -159,13 +178,12 @@ class UserDatabase implements UserDatabaseInterface
data.roles = await this.fetchRoles(data.roles); data.roles = await this.fetchRoles(data.roles);
return data as T; return data as T;
} }
async fetchUser (id: string, force = false) async fetchUser (id: string, force = false)
{ {
const data = await this._fetch<UserData>(id, this.#_userCollection, force); const data = await this._fetch<UserData>(id, this.#userCollection, force);
if (!data) if (!data)
return null; return null;
if (data instanceof User) if (data instanceof User)
@ -182,7 +200,7 @@ class UserDatabase implements UserDatabaseInterface
async fetchApplication (id: string, force = false) async fetchApplication (id: string, force = false)
{ {
const data = await this._fetch<ApplicationData>(id, this.#_appCollection, force); const data = await this._fetch<ApplicationData>(id, this.#appCollection, force);
if (!data) if (!data)
return null; return null;
if (data instanceof UserApplication) if (data instanceof UserApplication)
@ -198,7 +216,7 @@ class UserDatabase implements UserDatabaseInterface
async fetchRole (id: string, force = false) async fetchRole (id: string, force = false)
{ {
const data = await this._fetch<RoleData>(id, this.#_roleCollection, force); const data = await this._fetch<RoleData>(id, this.#roleCollection, force);
if (!data) if (!data)
return null; return null;
if (data instanceof Role) if (data instanceof Role)
@ -227,7 +245,7 @@ class UserDatabase implements UserDatabaseInterface
findOptions.skip = page * pageSize; findOptions.skip = page * pageSize;
} }
const data = await this.#db.find<RoleData>(this.#_roleCollection, query, findOptions); const data = await this.#db.find<RoleData>(this.#roleCollection, query, findOptions);
const roles = []; const roles = [];
for (const role of data) for (const role of data)
{ {
@ -246,7 +264,7 @@ class UserDatabase implements UserDatabaseInterface
// Retrieves the role with the lowest position, where 0 is highest // Retrieves the role with the lowest position, where 0 is highest
async lowestRole () async lowestRole ()
{ {
const data = await this.#db.findOne<RoleData>(this.#_roleCollection, {}, { sort: { position: 'desc' } }); const data = await this.#db.findOne<RoleData>(this.#roleCollection, {}, { sort: { position: 'desc' } });
if (!data) if (!data)
return null; return null;
const role = this._createRole(data); const role = this._createRole(data);
@ -260,7 +278,7 @@ class UserDatabase implements UserDatabaseInterface
throw new Error('Missing user id'); throw new Error('Missing user id');
id = id.toString(); id = id.toString();
const data = await this.#db.find<ApplicationData>(this.#_appCollection, { ownerId: new ObjectId(id) }); const data = await this.#db.find<ApplicationData>(this.#appCollection, { ownerId: new ObjectId(id) });
if (!data || !data.length) if (!data || !data.length)
return []; return [];
@ -286,7 +304,7 @@ class UserDatabase implements UserDatabaseInterface
if (user) if (user)
return Promise.resolve(user); return Promise.resolve(user);
const data = await this.#db.findOne<UserData>(this.#_userCollection, { name }, { collation: { locale: 'en', strength: 2 } }); const data = await this.#db.findOne<UserData>(this.#userCollection, { name }, { collation: { locale: 'en', strength: 2 } });
if (!data) if (!data)
return null; return null;
@ -319,7 +337,7 @@ class UserDatabase implements UserDatabaseInterface
if (app) if (app)
return Promise.resolve(app); return Promise.resolve(app);
const data = await this.#db.findOne<ApplicationData>(this.#_appCollection, { token }); const data = await this.#db.findOne<ApplicationData>(this.#appCollection, { token });
if (!data) if (!data)
return null; return null;
@ -350,7 +368,7 @@ class UserDatabase implements UserDatabaseInterface
if (user) if (user)
return Promise.resolve(user); return Promise.resolve(user);
const data = await this.#db.findOne<UserData>(this.#_userCollection, { 'externalProfiles.discord.id': profile.id }); const data = await this.#db.findOne<UserData>(this.#userCollection, { 'externalProfiles.discord.id': profile.id });
if (data) if (data)
{ {
user = await this._createUser(data); user = await this._createUser(data);
@ -362,7 +380,7 @@ class UserDatabase implements UserDatabaseInterface
return null; 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 // 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) if (existing)
{ {
const temp = await this._createUser({ temporary: true, displayName: profile.username }); 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}; const json = user.jsonPrivate as {_id?: ObjectId, id?: string};
json._id = new ObjectId(json.id); json._id = new ObjectId(json.id);
delete json.id; delete json.id;
await this.#db.insertOne(this.#_userCollection, json); await this.#db.insertOne(this.#userCollection, json);
// await this.updateUser(user); // await this.updateUser(user);
if (!this.#disableCache) if (!this.#disableCache)
this.#cache.set(user.id, user); this.#cache.set(user.id, user);
@ -417,7 +435,7 @@ class UserDatabase implements UserDatabaseInterface
const json = app.jsonPrivate as {_id?: ObjectId, id?: string}; const json = app.jsonPrivate as {_id?: ObjectId, id?: string};
json._id = new ObjectId(json.id); json._id = new ObjectId(json.id);
delete json.id; delete json.id;
await this.#db.insertOne(this.#_appCollection, json); await this.#db.insertOne(this.#appCollection, json);
if (!this.#disableCache) if (!this.#disableCache)
this.#cache.set(app.id, app); this.#cache.set(app.id, app);
this.#amount.applications++; this.#amount.applications++;
@ -436,7 +454,7 @@ class UserDatabase implements UserDatabaseInterface
} }
const role = this._createRole(data); const role = this._createRole(data);
await this.#db.insertOne(this.#_roleCollection, role.jsonPrivate); await this.#db.insertOne(this.#roleCollection, role.jsonPrivate);
if (!this.#disableCache) if (!this.#disableCache)
this.#cache.set(role.id, role); this.#cache.set(role.id, role);
this.#amount.roles++; this.#amount.roles++;
@ -445,12 +463,12 @@ class UserDatabase implements UserDatabaseInterface
async deleteApplication (id: string) 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) 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) updateUser (user: User)
{ {
return this._updateEntity<User>(user, this.#_userCollection); return this._updateEntity<User>(user, this.#userCollection);
} }
updateApplication (app: UserApplication) updateApplication (app: UserApplication)
{ {
return this._updateEntity<UserApplication>(app, this.#_appCollection); return this._updateEntity<UserApplication>(app, this.#appCollection);
} }
updateRole (role: Role) updateRole (role: Role)
{ {
return this._updateEntity<Role>(role, this.#_roleCollection); return this._updateEntity<Role>(role, this.#roleCollection);
} }

View File

@ -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;

View File

@ -133,7 +133,7 @@ class Flags extends ApiEndpoint
flag.env = body.env; flag.env = body.env;
if (body.consumer) if (body.consumer)
flag.consumer = body.consumer; flag.consumer = body.consumer;
if (body.hierarchy) if ('hierarchy' in body)
flag.hierarchy = body.hierarchy; flag.hierarchy = body.hierarchy;
if (flag.value) if (flag.value)
try try

View File

@ -26,8 +26,8 @@ class Roles extends ApiEndpoint
this.subpaths = [ this.subpaths = [
[ 'delete', '/:roleid', this.deleteRole.bind(this), server.auth.createAuthoriser('administrator:roles:delete', 5) ], [ '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', 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/permissions', this.modifyPerms.bind(this), server.auth.createAuthoriser('administrator:roles:permissions', 5) ],
[ 'patch', '/:roleid/ratelimits', this.modifyLimits.bind(this), server.auth.createAuthoriser('administrator:roles:edit', 5) ] [ 'patch', '/:roleid/ratelimits', this.modifyLimits.bind(this), server.auth.createAuthoriser('administrator:roles:ratelimits', 5) ]
]; ];
this.#users = server.users; this.#users = server.users;

View File

@ -1,7 +1,8 @@
// const { ApiEndpoint } = require("../../../interfaces"); // 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 Server from '../../../Server.js';
import { ApiEndpoint } from '../../../interfaces/index.js'; import { ApiEndpoint } from '../../../interfaces/index.js';
import User from '../../../structures/User.js';
class SettingsEndpoint extends ApiEndpoint class SettingsEndpoint extends ApiEndpoint
{ {
@ -14,16 +15,31 @@ class SettingsEndpoint extends ApiEndpoint
}); });
this.methods = [ 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 ]; // 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<void>
{
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();
} }
} }

View File

@ -17,12 +17,12 @@ class UsersEndpoint extends ApiEndpoint
path: '/users', 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 = [ 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/avatar', this.avatar.bind(this) ],
[ 'get', '/:userid/applications', this.userApplications.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', 5) ]] [ 'post', '/:userid/permissions', this.updatePermissions.bind(this), [ server.auth.createAuthoriser('administrator:users:permissions', 5) ]]
]; ];
// this.middleware = [ server.auth.createAuthoriser('administrator', 10) ]; // this.middleware = [ server.auth.createAuthoriser('administrator', 10) ];

View File

@ -1,6 +1,6 @@
import { DeleteResult } from '@navy.gif/wrappers'; import { DeleteResult } from '@navy.gif/wrappers';
import { SignupCode } from '../../../@types/Other.js'; 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'; import { Role, User, UserApplication } from '../structures/index.js';
export type Query<T, TData> = { export type Query<T, TData> = {
@ -43,6 +43,10 @@ interface UserDatabaseInterface {
updateUser(user: User): Promise<User> updateUser(user: User): Promise<User>
fetchSettings(userId: string): Promise<UserClientSettings>
updateSettings(userId: string, settings: UserClientSettings): Promise<void>
fetchUserApplications(id: string): Promise<UserApplication[]> fetchUserApplications(id: string): Promise<UserApplication[]>
fetchApplication(id: string, force?: boolean): Promise<UserApplication | null> fetchApplication(id: string, force?: boolean): Promise<UserApplication | null>

View File

@ -4,7 +4,7 @@
import Argon2 from 'argon2'; import Argon2 from 'argon2';
import { AbstractUser } from '../interfaces/index.js'; import { AbstractUser } from '../interfaces/index.js';
import Server from '../Server.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 // Fields omitted in the json getter
// Should be keys used in jsonPrivate, not the ones defined in the constructor // Should be keys used in jsonPrivate, not the ones defined in the constructor
@ -39,6 +39,16 @@ class User extends AbstractUser
} }
clientSettings (): Promise<UserClientSettings>
{
return this.db.fetchSettings(this.id);
}
async updateClientSettings (settings: UserClientSettings): Promise<void>
{
await this.db.updateSettings(this.id, settings);
}
override save () override save ()
{ {
return this.db.updateUser(this); return this.db.updateUser(this);