diff --git a/src/modules/objects/database/ormconfig.js b/ormconfig.js similarity index 80% rename from src/modules/objects/database/ormconfig.js rename to ormconfig.js index e4e5053..d0813d9 100644 --- a/src/modules/objects/database/ormconfig.js +++ b/ormconfig.js @@ -9,6 +9,8 @@ dotenv.config(); const CONFIG_ENV = process.env.NODE_ENV === 'production' ? 'prod' : 'dev'; const CONFIG_FILENAME = process.env.CONFIG || `config.${CONFIG_ENV}.toml`; const CONFIG_PATH = join(process.cwd(), CONFIG_FILENAME); -const config = toml.parse(readFileSync(CONFIG_PATH, { encoding: 'utf-8' })); +const config = JSON.parse( + JSON.stringify(toml.parse(readFileSync(CONFIG_PATH, { encoding: 'utf-8' }))), +); module.exports = config.database; diff --git a/src/app.module.ts b/src/app.module.ts index 86c022a..4128aba 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -21,6 +21,7 @@ import { DatabaseModule } from './modules/objects/database/database.module'; import { EmailModule } from './modules/objects/email/email.module'; import { OAuth2ClientModule } from './modules/objects/oauth2-client/oauth2-client.module'; import { OAuth2TokenModule } from './modules/objects/oauth2-token/oauth2-token.module'; +import { PrivilegeModule } from './modules/objects/privilege/privilege.module'; import { UploadModule } from './modules/objects/upload/upload.module'; import { UserModule } from './modules/objects/user/user.module'; import { UtilityModule } from './modules/utility/utility.module'; @@ -48,6 +49,7 @@ import { UtilityModule } from './modules/utility/utility.module'; OAuth2Module, TwoFactorModule, SettingsModule, + PrivilegeModule, ], controllers: [AppController], providers: [AppService, CSRFMiddleware], diff --git a/src/fe/scss/_colors.scss b/src/fe/scss/_colors.scss index 675d996..7a3c0b4 100644 --- a/src/fe/scss/_colors.scss +++ b/src/fe/scss/_colors.scss @@ -11,6 +11,7 @@ --main-background: #314550; --main: #2e6b81; --main-light: #519eb9; + --main-darkish: #006683; --main-darker: #005b74; --main-dark: #042b3a; diff --git a/src/fe/scss/_settings.scss b/src/fe/scss/_settings.scss index a102245..48809fb 100644 --- a/src/fe/scss/_settings.scss +++ b/src/fe/scss/_settings.scss @@ -17,9 +17,15 @@ } &__nav { + display: flex; padding: 2rem 0rem; background-color: var(--main-darker); + &-content { + display: flex; + flex-grow: 1; + } + ul { display: flex; flex-direction: column; @@ -43,13 +49,17 @@ &.active { border-right-color: var(--main-light); - font-weight: bold; + background-color: var(--main-darkish); } &:focus { outline: 4px solid var(--focus-outline); } } + + &:last-child { + margin-top: auto; + } } } } @@ -177,6 +187,7 @@ padding: 0; background-color: var(--main-darker); box-shadow: 0px 6px 62px -14px rgba(0, 0, 0, 0.45); + flex-direction: column; & > .settings__nav-content { display: block; diff --git a/src/fe/ts/upload.ts b/src/fe/ts/upload.ts new file mode 100644 index 0000000..037e1ac --- /dev/null +++ b/src/fe/ts/upload.ts @@ -0,0 +1,30 @@ +export class UploadInput { + private wrap: HTMLElement; + private inner: HTMLElement; + private hover = false; + + constructor(public input: HTMLInputElement) {} + + initialize(): void { + this.wrap = document.createElement('div'); + this.wrap.classList.add('upload__wrapper'); + + this.inner = document.createElement('div'); + this.inner.classList.add('upload'); + this.wrap.appendChild(this.inner); + + this.inner.addEventListener('dragenter', () => { + this.hover = true; + this.inner.classList.add('upload--hovered'); + }); + + this.inner.addEventListener('dragleave', () => { + this.hover = false; + this.inner.classList.remove('upload--hovered'); + }); + + this.inner.addEventListener('dragover', () => {}); + + this.input.parentElement.appendChild(this.wrap); + } +} diff --git a/src/middleware/user.middleware.ts b/src/middleware/user.middleware.ts index ed626e8..6f1d759 100644 --- a/src/middleware/user.middleware.ts +++ b/src/middleware/user.middleware.ts @@ -13,6 +13,7 @@ export class UserMiddleware implements NestMiddleware { // TODO: check for bans const userObj = await this.userService.getByUUID(req.session.user, [ 'picture', + 'privileges', ]); if (userObj && userObj.activated) { req.user = userObj; diff --git a/src/modules/features/oauth2/oauth2.controller.ts b/src/modules/features/oauth2/oauth2.controller.ts index f1125b8..2e9f8e4 100644 --- a/src/modules/features/oauth2/oauth2.controller.ts +++ b/src/modules/features/oauth2/oauth2.controller.ts @@ -67,7 +67,7 @@ export class OAuth2Controller { const token = res.locals.accessToken as OAuth2AccessToken; const user = await this._service.userService.getById( token.user_id as number, - ['picture'], + ['picture', 'privileges'], ); if (!user) { @@ -101,6 +101,13 @@ export class OAuth2Controller { userData.image_file = user.picture.file; } + if ( + token.scope.includes('privileges') || + (token.scope.includes('user:privileges') && user.privileges?.length) + ) { + userData.privileges = user.privileges; + } + return userData; } } diff --git a/src/modules/features/register/register.controller.ts b/src/modules/features/register/register.controller.ts index 204239d..9d0417a 100644 --- a/src/modules/features/register/register.controller.ts +++ b/src/modules/features/register/register.controller.ts @@ -7,11 +7,9 @@ import { Render, Req, Res, - Session, } from '@nestjs/common'; import { Throttle } from '@nestjs/throttler'; import { Request, Response } from 'express'; -import { SessionData } from 'express-session'; import { UserService } from 'src/modules/objects/user/user.service'; import { FormUtilityService } from 'src/modules/utility/services/form-utility.service'; import { RegisterDto } from './register.interfaces'; diff --git a/src/modules/objects/privilege/privilege.entity.ts b/src/modules/objects/privilege/privilege.entity.ts new file mode 100644 index 0000000..4a39b57 --- /dev/null +++ b/src/modules/objects/privilege/privilege.entity.ts @@ -0,0 +1,10 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity() +export class Privilege { + @PrimaryGeneratedColumn() + id: number; + + @Column({ type: 'text', nullable: false }) + name: string; +} diff --git a/src/modules/objects/privilege/privilege.module.ts b/src/modules/objects/privilege/privilege.module.ts new file mode 100644 index 0000000..89a2df1 --- /dev/null +++ b/src/modules/objects/privilege/privilege.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from '../database/database.module'; +import { privilegeProviders } from './privilege.providers'; +import { PrivilegeService } from './privilege.service'; + +@Module({ + imports: [DatabaseModule], + providers: [...privilegeProviders, PrivilegeService], + exports: [PrivilegeService], +}) +export class PrivilegeModule {} diff --git a/src/modules/objects/privilege/privilege.providers.ts b/src/modules/objects/privilege/privilege.providers.ts new file mode 100644 index 0000000..b0af33e --- /dev/null +++ b/src/modules/objects/privilege/privilege.providers.ts @@ -0,0 +1,10 @@ +import { Connection } from 'typeorm'; +import { Privilege } from './privilege.entity'; + +export const privilegeProviders = [ + { + provide: 'PRIVILEGE_REPOSITORY', + useFactory: (connection: Connection) => connection.getRepository(Privilege), + inject: ['DATABASE_CONNECTION'], + }, +]; diff --git a/src/modules/objects/privilege/privilege.service.ts b/src/modules/objects/privilege/privilege.service.ts new file mode 100644 index 0000000..ebe4eb9 --- /dev/null +++ b/src/modules/objects/privilege/privilege.service.ts @@ -0,0 +1,11 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Repository } from 'typeorm'; +import { Privilege } from './privilege.entity'; + +@Injectable() +export class PrivilegeService { + constructor( + @Inject('PRIVILEGE_REPOSITORY') + private privilegeRepository: Repository, + ) {} +} diff --git a/src/modules/objects/user/user.entity.ts b/src/modules/objects/user/user.entity.ts index 8ec0107..4343b02 100644 --- a/src/modules/objects/user/user.entity.ts +++ b/src/modules/objects/user/user.entity.ts @@ -2,11 +2,13 @@ import { Column, CreateDateColumn, Entity, + JoinTable, + ManyToMany, ManyToOne, - OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; +import { Privilege } from '../privilege/privilege.entity'; import { Upload } from '../upload/upload.entity'; @Entity() @@ -54,4 +56,8 @@ export class User { onUpdate: 'CASCADE', }) public picture: Upload; + + @ManyToMany(() => Privilege) + @JoinTable() + public privileges: Privilege[]; } diff --git a/src/modules/objects/user/user.module.ts b/src/modules/objects/user/user.module.ts index 297f0ff..75e826f 100644 --- a/src/modules/objects/user/user.module.ts +++ b/src/modules/objects/user/user.module.ts @@ -1,13 +1,20 @@ import { Module } from '@nestjs/common'; import { DatabaseModule } from '../database/database.module'; import { EmailModule } from '../email/email.module'; +import { PrivilegeModule } from '../privilege/privilege.module'; import { UploadModule } from '../upload/upload.module'; import { UserTokenModule } from '../user-token/user-token.module'; import { userProviders } from './user.providers'; import { UserService } from './user.service'; @Module({ - imports: [DatabaseModule, EmailModule, UserTokenModule, UploadModule], + imports: [ + DatabaseModule, + EmailModule, + UserTokenModule, + UploadModule, + PrivilegeModule, + ], providers: [...userProviders, UserService], exports: [UserService], }) diff --git a/views/settings/layout.pug b/views/settings/layout.pug index 7a053ba..f8ed746 100644 --- a/views/settings/layout.pug +++ b/views/settings/layout.pug @@ -10,11 +10,23 @@ block body .settings__nav-content ul li - a(href="/account/general", class=path === '/account/general' ? 'active' : '') General + a( + href="/account/general", + class=path === '/account/general' ? 'active' : '' + aria-selected=path === '/account/general' ? 'true' : 'false' + ) General li - a(href="/account/oauth2", class=path === '/account/oauth2' ? 'active' : '') Authorizations + a( + href="/account/oauth2", + class=path === '/account/oauth2' ? 'active' : '' + aria-selected=path === '/account/oauth2' ? 'true' : 'false' + ) Authorizations li - a(href="/account/security", class=path === '/account/security' ? 'active' : '') Security + a( + href="/account/security", + class=path === '/account/security' ? 'active' : '' + aria-selected=path === '/account/security' ? 'true' : 'false' + ) Security li a(href="/account/logout?csrf=" + csrf) Log out section.content.settings__content