From dc7f4215af30abecbad8488815685d0d823e9ab1 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Wed, 16 Mar 2022 20:37:50 +0200 Subject: [PATCH] OAuth2 authorization view --- package-lock.json | 78 ++++++++++++++ package.json | 1 + src/app.module.ts | 5 + src/middleware/auth.middleware.ts | 5 +- src/middleware/user.middleware.ts | 4 +- .../oauth2/adapter/access-token.adapter.ts | 2 +- .../features/oauth2/oauth2.controller.ts | 58 +++++++++- src/modules/features/oauth2/oauth2.module.ts | 1 + src/modules/features/oauth2/oauth2.service.ts | 39 ++++++- .../features/register/register.controller.ts | 2 + .../two-factor/two-factor.controller.ts | 6 +- .../features/two-factor/two-factor.module.ts | 2 +- .../oauth2-client/oauth2-client.entity.ts | 5 +- .../oauth2-client/oauth2-client.service.ts | 31 ++++-- .../oauth2-token/oauth2-token.service.ts | 12 ++- src/modules/objects/user/user.entity.ts | 3 +- src/modules/objects/user/user.service.ts | 32 +++--- src/modules/utility/services/token.service.ts | 4 + src/scss/_authorize.scss | 102 ++++++++++++++++++ src/scss/_block.scss | 8 +- src/scss/_flex.scss | 3 + src/scss/_index.scss | 19 ++++ views/authorize.pug | 53 ++++++++- views/login/login.pug | 2 +- 24 files changed, 432 insertions(+), 45 deletions(-) create mode 100644 src/scss/_authorize.scss diff --git a/package-lock.json b/package-lock.json index 9e6d498..7978c76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@nestjs/common": "^8.0.0", "@nestjs/core": "^8.0.0", "@nestjs/platform-express": "^8.0.0", + "@nestjs/throttler": "^2.0.1", "bcrypt": "^5.0.1", "class-transformer": "^0.5.1", "class-validator": "^0.13.2", @@ -1646,6 +1647,19 @@ } } }, + "node_modules/@nestjs/throttler": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-2.0.1.tgz", + "integrity": "sha512-ginW73rmOjBN27USuGidetEoa8VSGzxW3kEuCquEd5mETEtBfgIm4901b9tuLDnsczttE01imCHZ53J7+AuLJg==", + "dependencies": { + "md5": "^2.2.1" + }, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0", + "reflect-metadata": "^0.1.13" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3199,6 +3213,14 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -3629,6 +3651,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "engines": { + "node": "*" + } + }, "node_modules/cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -5400,6 +5430,11 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-core-module": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", @@ -6836,6 +6871,16 @@ "tmpl": "1.0.5" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -11313,6 +11358,14 @@ "tslib": "2.3.1" } }, + "@nestjs/throttler": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-2.0.1.tgz", + "integrity": "sha512-ginW73rmOjBN27USuGidetEoa8VSGzxW3kEuCquEd5mETEtBfgIm4901b9tuLDnsczttE01imCHZ53J7+AuLJg==", + "requires": { + "md5": "^2.2.1" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -12575,6 +12628,11 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -12921,6 +12979,11 @@ "which": "^2.0.1" } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, "cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -14254,6 +14317,11 @@ "binary-extensions": "^2.0.0" } }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "is-core-module": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", @@ -15362,6 +15430,16 @@ "tmpl": "1.0.5" } }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", diff --git a/package.json b/package.json index 09b5556..decab6d 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@nestjs/common": "^8.0.0", "@nestjs/core": "^8.0.0", "@nestjs/platform-express": "^8.0.0", + "@nestjs/throttler": "^2.0.1", "bcrypt": "^5.0.1", "class-transformer": "^0.5.1", "class-validator": "^0.13.2", diff --git a/src/app.module.ts b/src/app.module.ts index 89e1f90..f6b22f5 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,4 +1,5 @@ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { ThrottlerModule } from '@nestjs/throttler'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { CSRFMiddleware } from './middleware/csrf.middleware'; @@ -18,6 +19,10 @@ import { UtilityModule } from './modules/utility/utility.module'; @Module({ imports: [ + ThrottlerModule.forRoot({ + ttl: 10, + limit: 10, + }), ConfigurationModule, UtilityModule, DatabaseModule, diff --git a/src/middleware/auth.middleware.ts b/src/middleware/auth.middleware.ts index 1903ff0..652d490 100644 --- a/src/middleware/auth.middleware.ts +++ b/src/middleware/auth.middleware.ts @@ -9,7 +9,10 @@ import { NextFunction, Request, Response } from 'express'; export class AuthMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { if (!req.session.user) { - if (req.header('content-type')?.includes('application/json')) { + if ( + req.header('content-type')?.includes('application/json') || + req.header('accept')?.includes('application/json') + ) { throw new UnauthorizedException('Unauthorized'); } diff --git a/src/middleware/user.middleware.ts b/src/middleware/user.middleware.ts index fd19b3a..ed626e8 100644 --- a/src/middleware/user.middleware.ts +++ b/src/middleware/user.middleware.ts @@ -11,7 +11,9 @@ export class UserMiddleware implements NestMiddleware { // TODO: Cache user requests // Might not be a big deal though, there is no expected volume in visitors // TODO: check for bans - const userObj = await this.userService.getByUUID(req.session.user); + const userObj = await this.userService.getByUUID(req.session.user, [ + 'picture', + ]); if (userObj && userObj.activated) { req.user = userObj; } else { diff --git a/src/modules/features/oauth2/adapter/access-token.adapter.ts b/src/modules/features/oauth2/adapter/access-token.adapter.ts index f91316c..c4e6229 100644 --- a/src/modules/features/oauth2/adapter/access-token.adapter.ts +++ b/src/modules/features/oauth2/adapter/access-token.adapter.ts @@ -8,7 +8,7 @@ import { OAuth2Service } from '../oauth2.service'; export class AccessTokenAdapter implements OAuth2AccessTokenAdapter { constructor(private _service: OAuth2Service) {} - public ttl = 3600; + public ttl = 604800; getToken(token: OAuth2AccessToken): string { return token.token; diff --git a/src/modules/features/oauth2/oauth2.controller.ts b/src/modules/features/oauth2/oauth2.controller.ts index 1c204c8..75febc0 100644 --- a/src/modules/features/oauth2/oauth2.controller.ts +++ b/src/modules/features/oauth2/oauth2.controller.ts @@ -1,10 +1,23 @@ -import { Controller, Get, Next, Post, Req, Res } from '@nestjs/common'; +import { OAuth2AccessToken } from '@icynet/oauth2-provider'; +import { + Controller, + Get, + Next, + NotFoundException, + Post, + Req, + Res, +} from '@nestjs/common'; import { NextFunction, Request, Response } from 'express'; +import { ConfigurationService } from 'src/modules/config/config.service'; import { OAuth2Service } from './oauth2.service'; @Controller('oauth2') export class OAuth2Controller { - constructor(private _service: OAuth2Service) {} + constructor( + private _service: OAuth2Service, + private _config: ConfigurationService, + ) {} // These requests are just passed straight on to the provider controller @@ -43,4 +56,45 @@ export class OAuth2Controller { ): void { return this._service.oauth.controller.introspection(req, res, next); } + + // User information endpoint + // TODO: Move to API + + @Get('user') + public async userInfo( + @Res({ passthrough: true }) res: Response, + ): Promise> { + const token = res.locals.accessToken as OAuth2AccessToken; + const user = await this._service.userService.getById( + token.user_id as number, + ['picture'], + ); + + if (!user) { + throw new NotFoundException('No such user'); + } + + const userData: Record = { + id: user.id, + uuid: user.uuid, + username: user.username, + display_name: user.display_name, + }; + + if (token.scope.includes('email') || token.scope.includes('user:email')) { + userData.email = user.email; + } + + if ( + (token.scope.includes('image') || token.scope.includes('user:image')) && + user.picture + ) { + userData.image = `${this._config.get('app.base_url')}/uploads/${ + user.picture.file + }`; + userData.image_file = user.picture.file; + } + + return userData; + } } diff --git a/src/modules/features/oauth2/oauth2.module.ts b/src/modules/features/oauth2/oauth2.module.ts index 70c26d3..8438a79 100644 --- a/src/modules/features/oauth2/oauth2.module.ts +++ b/src/modules/features/oauth2/oauth2.module.ts @@ -18,6 +18,7 @@ export class OAuth2Module implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(this._service.oauth.express()).forRoutes('oauth2/*'); + consumer.apply(this._service.oauth.bearer).forRoutes('oauth2/user'); consumer.apply(AuthMiddleware).forRoutes('oauth2/authorize'); } } diff --git a/src/modules/features/oauth2/oauth2.service.ts b/src/modules/features/oauth2/oauth2.service.ts index 5492eb7..8a5e472 100644 --- a/src/modules/features/oauth2/oauth2.service.ts +++ b/src/modules/features/oauth2/oauth2.service.ts @@ -10,6 +10,14 @@ import { CodeAdapter } from './adapter/code.adapter'; import { RefreshTokenAdapter } from './adapter/refresh-token.adapter'; import { UserAdapter } from './adapter/user.adapter'; +const SCOPE_DESCRIPTION: Record = { + email: 'Email address', + image: 'Profile picture', +}; + +const ALWAYS_AVAILABLE = ['Username and display name']; +const ALWAYS_UNAVAILABLE = ['Password and other account settings']; + @Injectable() export class OAuth2Service { private _oauthAdapter: OAuth2AdapterModel = { @@ -20,9 +28,30 @@ export class OAuth2Service { code: new CodeAdapter(this), }; - public oauth = new OAuth2Provider(this._oauthAdapter, async (req, res) => { - res.render('authorize'); - }); + public oauth = new OAuth2Provider( + this._oauthAdapter, + async (req, res, client, scope, user, redirectUri) => { + const fullClient = await this.clientService.getById(client.id as string); + const allowedScopes = [...ALWAYS_AVAILABLE]; + const disallowedScopes = [...ALWAYS_UNAVAILABLE]; + + Object.keys(SCOPE_DESCRIPTION).forEach((item) => { + if (scope.includes(item)) { + allowedScopes.push(SCOPE_DESCRIPTION[item]); + } else { + disallowedScopes.push(SCOPE_DESCRIPTION[item]); + } + }); + + res.render('authorize', { + csrf: req.session.csrf, + user: req.user, + client: fullClient, + allowedScopes, + disallowedScopes, + }); + }, + ); constructor( public token: TokenService, @@ -36,6 +65,10 @@ export class OAuth2Service { } public splitScope(scope: string): string[] { + if (!scope) { + return []; + } + return scope.includes(',') ? scope.split(',').map((item) => item.trim()) : scope.split(' '); diff --git a/src/modules/features/register/register.controller.ts b/src/modules/features/register/register.controller.ts index 21277c9..b8bbd7e 100644 --- a/src/modules/features/register/register.controller.ts +++ b/src/modules/features/register/register.controller.ts @@ -9,6 +9,7 @@ import { 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'; @@ -32,6 +33,7 @@ export class RegisterController { } @Post() + @Throttle(3, 10) public async registerRequest( @Req() req: Request, @Res() res: Response, diff --git a/src/modules/features/two-factor/two-factor.controller.ts b/src/modules/features/two-factor/two-factor.controller.ts index c506d80..5ac38d1 100644 --- a/src/modules/features/two-factor/two-factor.controller.ts +++ b/src/modules/features/two-factor/two-factor.controller.ts @@ -6,7 +6,7 @@ import { FormUtilityService } from 'src/modules/utility/services/form-utility.se import { QRCodeService } from 'src/modules/utility/services/qr-code.service'; import { TokenService } from 'src/modules/utility/services/token.service'; -@Controller('/two-factor') +@Controller('/account/two-factor') export class TwoFactorController { constructor( private totp: UserTOTPService, @@ -15,7 +15,7 @@ export class TwoFactorController { private form: FormUtilityService, ) {} - @Get() + @Get('activate') public async twoFAStatus( @Session() session: SessionData, @Req() req: Request, @@ -52,7 +52,7 @@ export class TwoFactorController { res.redirect('/'); } - @Post() + @Post('activate') public async twoFAActivate( @Session() session: SessionData, @Body() body: { code: string }, diff --git a/src/modules/features/two-factor/two-factor.module.ts b/src/modules/features/two-factor/two-factor.module.ts index 3fe300a..3911cf0 100644 --- a/src/modules/features/two-factor/two-factor.module.ts +++ b/src/modules/features/two-factor/two-factor.module.ts @@ -11,7 +11,7 @@ import { TwoFactorController } from './two-factor.controller'; }) export class TwoFactorModule implements NestModule { configure(consumer: MiddlewareConsumer) { - consumer.apply(AuthMiddleware).forRoutes('two-factor'); + consumer.apply(AuthMiddleware).forRoutes('/account/two-factor/activate'); consumer.apply(FlashMiddleware).forRoutes('*'); } } diff --git a/src/modules/objects/oauth2-client/oauth2-client.entity.ts b/src/modules/objects/oauth2-client/oauth2-client.entity.ts index 33a7a1e..e48c128 100644 --- a/src/modules/objects/oauth2-client/oauth2-client.entity.ts +++ b/src/modules/objects/oauth2-client/oauth2-client.entity.ts @@ -2,6 +2,7 @@ import { Column, CreateDateColumn, Entity, + ManyToOne, OneToMany, OneToOne, PrimaryGeneratedColumn, @@ -53,10 +54,10 @@ export class OAuth2Client { }) public updated_at: Date; - @OneToOne(() => Upload, { nullable: true, onDelete: 'SET NULL' }) + @ManyToOne(() => Upload, { nullable: true, onDelete: 'SET NULL' }) public picture: Upload; - @OneToOne(() => User, { nullable: true, onDelete: 'SET NULL' }) + @ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' }) public owner: User; @OneToMany(() => OAuth2ClientURL, (url) => url.client) diff --git a/src/modules/objects/oauth2-client/oauth2-client.service.ts b/src/modules/objects/oauth2-client/oauth2-client.service.ts index 8594853..bc911eb 100644 --- a/src/modules/objects/oauth2-client/oauth2-client.service.ts +++ b/src/modules/objects/oauth2-client/oauth2-client.service.ts @@ -22,9 +22,15 @@ export class OAuth2ClientService { let client: OAuth2Client; if (typeof id === 'string') { - client = await this.clientRepository.findOne({ client_id: id }); + client = await this.clientRepository.findOne( + { client_id: id }, + { relations: ['urls', 'picture'] }, + ); } else { - client = await this.clientRepository.findOne({ id }); + client = await this.clientRepository.findOne( + { id }, + { relations: ['urls', 'picture'] }, + ); } return client; @@ -34,14 +40,23 @@ export class OAuth2ClientService { id: string, type?: OAuth2ClientURLType, ): Promise { - return this.clientUrlRepository.find({ client: { client_id: id }, type }); + return this.clientUrlRepository.find({ + where: { + client: { client_id: id }, + type, + }, + relations: ['client'], + }); } public async checkRedirectURI(id: string, url: string): Promise { - return !!(await this.clientUrlRepository.findOne({ - client: { client_id: id }, - url, - type: OAuth2ClientURLType.REDIRECT_URI, - })); + return !!(await this.clientUrlRepository.findOne( + { + client: { client_id: id }, + url, + type: OAuth2ClientURLType.REDIRECT_URI, + }, + { relations: ['client'] }, + )); } } diff --git a/src/modules/objects/oauth2-token/oauth2-token.service.ts b/src/modules/objects/oauth2-token/oauth2-token.service.ts index 9c1ba3a..b52bd1c 100644 --- a/src/modules/objects/oauth2-token/oauth2-token.service.ts +++ b/src/modules/objects/oauth2-token/oauth2-token.service.ts @@ -36,10 +36,13 @@ export class OAuth2TokenService { token: string, type: OAuth2TokenType, ): Promise { - return this.tokenRepository.findOne({ - token, - type, - }); + return this.tokenRepository.findOne( + { + token, + type, + }, + { relations: ['client', 'user'] }, + ); } public async fetchByUserIdClientId( @@ -57,6 +60,7 @@ export class OAuth2TokenService { }, type, }, + relations: ['client', 'user'], }); } diff --git a/src/modules/objects/user/user.entity.ts b/src/modules/objects/user/user.entity.ts index 0cec3c4..8ec0107 100644 --- a/src/modules/objects/user/user.entity.ts +++ b/src/modules/objects/user/user.entity.ts @@ -2,6 +2,7 @@ import { Column, CreateDateColumn, Entity, + ManyToOne, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, @@ -47,7 +48,7 @@ export class User { }) public updated_at: Date; - @OneToOne(() => Upload, { + @ManyToOne(() => Upload, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE', diff --git a/src/modules/objects/user/user.service.ts b/src/modules/objects/user/user.service.ts index 153f544..4d811c6 100644 --- a/src/modules/objects/user/user.service.ts +++ b/src/modules/objects/user/user.service.ts @@ -21,36 +21,42 @@ export class UserService { private config: ConfigurationService, ) {} - public async getById(id: number): Promise { - return this.userRepository.findOne({ id }); + public async getById(id: number, relations?: string[]): Promise { + return this.userRepository.findOne({ id }, { relations }); } - public async getByUUID(uuid: string): Promise { - return this.userRepository.findOne({ uuid }); + public async getByUUID(uuid: string, relations?: string[]): Promise { + return this.userRepository.findOne({ uuid }, { relations }); } - public async getByEmail(email: string): Promise { - return this.userRepository.findOne({ email }); + public async getByEmail(email: string, relations?: string[]): Promise { + return this.userRepository.findOne({ email }, { relations }); } - public async getByUsername(username: string): Promise { - return this.userRepository.findOne({ username }); + public async getByUsername( + username: string, + relations?: string[], + ): Promise { + return this.userRepository.findOne({ username }, { relations }); } - public async get(input: string | number): Promise { + public async get( + input: string | number, + relations?: string[], + ): Promise { if (typeof input === 'number') { - return this.getById(input); + return this.getById(input, relations); } if (input.includes('@')) { - return this.getByEmail(input); + return this.getByEmail(input, relations); } if (input.length === 36 && input.includes('-')) { - return this.getByUUID(input); + return this.getByUUID(input, relations); } - return this.getByUsername(input); + return this.getByUsername(input, relations); } public async updateUser(user: User): Promise { diff --git a/src/modules/utility/services/token.service.ts b/src/modules/utility/services/token.service.ts index 231ad15..15f649f 100644 --- a/src/modules/utility/services/token.service.ts +++ b/src/modules/utility/services/token.service.ts @@ -14,6 +14,10 @@ export class TokenService { return crypto.randomBytes(length).toString('hex').slice(0, length); } + public generateSecret(): string { + return crypto.randomBytes(256 / 8).toString('hex'); + } + public createUUID(): string { return v4(); } diff --git a/src/scss/_authorize.scss b/src/scss/_authorize.scss new file mode 100644 index 0000000..3685d51 --- /dev/null +++ b/src/scss/_authorize.scss @@ -0,0 +1,102 @@ +.authorize { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + background-color: #005b74; + margin: 2rem -4rem; + padding: 2rem 1rem; + box-shadow: inset 0px 6px 62px -14px rgba(0, 0, 0, 0.45); + + @include break-on(xs, down) { + margin: 1rem -1rem; + padding: 1rem; + } + + &__user, + &__client { + display: flex; + flex-direction: row; + + &-image { + width: 120px; + height: 120px; + flex-shrink: 0; + background-color: #b5b5b5; + } + + &-content { + display: flex; + flex-direction: column; + padding: 0 8px; + } + + &-title { + font-size: 1.5rem; + } + } + + &__center { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' style='width:24px;height:24px' viewBox='0 0 24 24'%3E%3Cpath fill='currentColor' d='M10,4H14V13L17.5,9.5L19.92,11.92L12,19.84L4.08,11.92L6.5,9.5L10,13V4Z' /%3E%3C/svg%3E"); + width: 80px; + height: 80px; + opacity: 0.4; + margin: 2rem; + flex-shrink: 0; + } + + &__user { + &-user { + color: #b5b5b5; + } + } + + &__client { + min-height: 120px; + + &-urls { + margin-top: auto; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + } + + &-url { + margin-top: 2px; + } + + &-description { + margin-bottom: 4px; + } + } +} + +.scopes { + display: flex; + flex-direction: column; + max-width: 400px; + margin: auto; + + .scopes__scope { + display: flex; + flex-direction: row; + align-items: center; + font-weight: bold; + + &::before { + content: ''; + display: block; + width: 32px; + height: 32px; + margin: 4px; + } + + &--allowed::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' style='width:24px;height:24px' viewBox='0 0 24 24'%3E%3Cpath fill='%2300f000' d='M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z' /%3E%3C/svg%3E"); + } + + &--disallowed::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' style='width:24px;height:24px' viewBox='0 0 24 24'%3E%3Cpath fill='%23f00000' d='M20 6.91L17.09 4L12 9.09L6.91 4L4 6.91L9.09 12L4 17.09L6.91 20L12 14.91L17.09 20L20 17.09L14.91 12L20 6.91Z' /%3E%3C/svg%3E"); + } + } +} diff --git a/src/scss/_block.scss b/src/scss/_block.scss index 2d40c9b..0f7a1c2 100644 --- a/src/scss/_block.scss +++ b/src/scss/_block.scss @@ -10,7 +10,7 @@ max-width: 800px; background-color: #2e6b81; color: #fff; - margin: 2rem auto 0; + margin: 2rem auto; padding: 4rem; position: relative; @@ -26,12 +26,16 @@ background-color: #042b3a; overflow: hidden; max-width: 600px; - margin: 0 auto; + margin: 0 auto 2rem auto; padding: 16px; text-align: center; box-shadow: 0px 6px 62px -14px rgba(0, 0, 0, 0.45); } + &--no-margin { + margin-bottom: 0; + } + @include break-on(xs, down) { padding: 1rem; } diff --git a/src/scss/_flex.scss b/src/scss/_flex.scss index 793fb7d..30f9f6b 100644 --- a/src/scss/_flex.scss +++ b/src/scss/_flex.scss @@ -16,3 +16,6 @@ .flex-row { flex-direction: row; } +.text-center { + text-align: center; +} diff --git a/src/scss/_index.scss b/src/scss/_index.scss index 263674d..9365ed6 100644 --- a/src/scss/_index.scss +++ b/src/scss/_index.scss @@ -4,6 +4,7 @@ @import 'button'; @import 'flex'; @import 'alert'; +@import 'authorize'; *, *::before, @@ -23,3 +24,21 @@ body { background-color: #314550; text-shadow: black 1px 1px 2px; } + +a { + color: #fff; + + &:hover { + text-decoration: none; + } + + &[target='_blank']::after { + content: ''; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' style='width:24px;height:24px' viewBox='0 0 24 24'%3E%3Cpath fill='%23ffffff' d='M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z' /%3E%3C/svg%3E"); + width: 1rem; + height: 1rem; + display: inline-block; + margin-left: 4px; + vertical-align: top; + } +} diff --git a/views/authorize.pug b/views/authorize.pug index a8faef1..4602a39 100644 --- a/views/authorize.pug +++ b/views/authorize.pug @@ -1,7 +1,56 @@ extends partials/layout.pug block title - |Icy Network | Authorize + |Icy Network | Authorize application block body - h1 Authorize + include partials/logo.pug + div.container + div.center-box + h1 Authorize application + + .authorize + .authorize__user + .authorize__user-image + .authorize__user-content + span.authorize__user-title #{user.display_name} + span.authorize__user-user @#{user.username} + .authorize__center + .authorize__client + .authorize__client-image + .authorize__client-content + span.authorize__client-title #{client.title} + span.authorize__client-description #{client.description} + .authorize__client-urls + each url in client.urls + if url.type == 'website' + a.authorize__client-url(href=url.url, target="_blank", rel="nofollow") Visit website + if url.type == 'privacy' + a.authorize__client-url(href=url.url, target="_blank", rel="nofollow") Privacy Policy + if url.type == 'terms' + a.authorize__client-url(href=url.url, target="_blank", rel="nofollow") Terms of Service + + h2.text-center This application will have access to.. + + .scopes + each allowed in allowedScopes + span.scopes__scope.scopes__scope--allowed #{allowed} + + + h2.text-center This application will not have access to.. + + .scopes + each allowed in disallowedScopes + span.scopes__scope.scopes__scope--disallowed #{allowed} + + form(method="POST", action="") + div.form-container + input(type="hidden", name="csrf", value=csrf) + input(type="hidden", name="decision", value="1") + button.btn.btn-primary(type="submit") Authorize + + form(method="POST", action="") + div.form-container + input(type="hidden", name="csrf", value=csrf) + input(type="hidden", name="decision", value="0") + button.btn.btn-link(type="submit") Reject diff --git a/views/login/login.pug b/views/login/login.pug index c400d4c..6a8da61 100644 --- a/views/login/login.pug +++ b/views/login/login.pug @@ -6,7 +6,7 @@ block title block body include ../partials/logo.pug div.container - div.center-box + div.center-box.center-box--no-margin h1 Log in if message.text if message.error