OAuth2 authorization view
This commit is contained in:
parent
5fa307b5b7
commit
dc7f4215af
78
package-lock.json
generated
78
package-lock.json
generated
@ -13,6 +13,7 @@
|
|||||||
"@nestjs/common": "^8.0.0",
|
"@nestjs/common": "^8.0.0",
|
||||||
"@nestjs/core": "^8.0.0",
|
"@nestjs/core": "^8.0.0",
|
||||||
"@nestjs/platform-express": "^8.0.0",
|
"@nestjs/platform-express": "^8.0.0",
|
||||||
|
"@nestjs/throttler": "^2.0.1",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.13.2",
|
"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": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@ -3199,6 +3213,14 @@
|
|||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.5.3",
|
"version": "3.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||||
@ -3629,6 +3651,14 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/cssom": {
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
|
||||||
@ -5400,6 +5430,11 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/is-core-module": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
|
||||||
@ -6836,6 +6871,16 @@
|
|||||||
"tmpl": "1.0.5"
|
"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": {
|
"node_modules/media-typer": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
@ -11313,6 +11358,14 @@
|
|||||||
"tslib": "2.3.1"
|
"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": {
|
"@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@ -12575,6 +12628,11 @@
|
|||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"charenc": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||||
|
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
|
||||||
|
},
|
||||||
"chokidar": {
|
"chokidar": {
|
||||||
"version": "3.5.3",
|
"version": "3.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||||
@ -12921,6 +12979,11 @@
|
|||||||
"which": "^2.0.1"
|
"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": {
|
"cssom": {
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
|
||||||
@ -14254,6 +14317,11 @@
|
|||||||
"binary-extensions": "^2.0.0"
|
"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": {
|
"is-core-module": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
|
||||||
@ -15362,6 +15430,16 @@
|
|||||||
"tmpl": "1.0.5"
|
"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": {
|
"media-typer": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
"@nestjs/common": "^8.0.0",
|
"@nestjs/common": "^8.0.0",
|
||||||
"@nestjs/core": "^8.0.0",
|
"@nestjs/core": "^8.0.0",
|
||||||
"@nestjs/platform-express": "^8.0.0",
|
"@nestjs/platform-express": "^8.0.0",
|
||||||
|
"@nestjs/throttler": "^2.0.1",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.13.2",
|
"class-validator": "^0.13.2",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||||
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { CSRFMiddleware } from './middleware/csrf.middleware';
|
import { CSRFMiddleware } from './middleware/csrf.middleware';
|
||||||
@ -18,6 +19,10 @@ import { UtilityModule } from './modules/utility/utility.module';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
ThrottlerModule.forRoot({
|
||||||
|
ttl: 10,
|
||||||
|
limit: 10,
|
||||||
|
}),
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
UtilityModule,
|
UtilityModule,
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
|
@ -9,7 +9,10 @@ import { NextFunction, Request, Response } from 'express';
|
|||||||
export class AuthMiddleware implements NestMiddleware {
|
export class AuthMiddleware implements NestMiddleware {
|
||||||
use(req: Request, res: Response, next: NextFunction) {
|
use(req: Request, res: Response, next: NextFunction) {
|
||||||
if (!req.session.user) {
|
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');
|
throw new UnauthorizedException('Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,9 @@ export class UserMiddleware implements NestMiddleware {
|
|||||||
// TODO: Cache user requests
|
// TODO: Cache user requests
|
||||||
// Might not be a big deal though, there is no expected volume in visitors
|
// Might not be a big deal though, there is no expected volume in visitors
|
||||||
// TODO: check for bans
|
// 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) {
|
if (userObj && userObj.activated) {
|
||||||
req.user = userObj;
|
req.user = userObj;
|
||||||
} else {
|
} else {
|
||||||
|
@ -8,7 +8,7 @@ import { OAuth2Service } from '../oauth2.service';
|
|||||||
export class AccessTokenAdapter implements OAuth2AccessTokenAdapter {
|
export class AccessTokenAdapter implements OAuth2AccessTokenAdapter {
|
||||||
constructor(private _service: OAuth2Service) {}
|
constructor(private _service: OAuth2Service) {}
|
||||||
|
|
||||||
public ttl = 3600;
|
public ttl = 604800;
|
||||||
|
|
||||||
getToken(token: OAuth2AccessToken): string {
|
getToken(token: OAuth2AccessToken): string {
|
||||||
return token.token;
|
return token.token;
|
||||||
|
@ -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 { NextFunction, Request, Response } from 'express';
|
||||||
|
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||||
import { OAuth2Service } from './oauth2.service';
|
import { OAuth2Service } from './oauth2.service';
|
||||||
|
|
||||||
@Controller('oauth2')
|
@Controller('oauth2')
|
||||||
export class OAuth2Controller {
|
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
|
// These requests are just passed straight on to the provider controller
|
||||||
|
|
||||||
@ -43,4 +56,45 @@ export class OAuth2Controller {
|
|||||||
): void {
|
): void {
|
||||||
return this._service.oauth.controller.introspection(req, res, next);
|
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<Record<string, any>> {
|
||||||
|
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<string, any> = {
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ export class OAuth2Module implements NestModule {
|
|||||||
|
|
||||||
configure(consumer: MiddlewareConsumer) {
|
configure(consumer: MiddlewareConsumer) {
|
||||||
consumer.apply(this._service.oauth.express()).forRoutes('oauth2/*');
|
consumer.apply(this._service.oauth.express()).forRoutes('oauth2/*');
|
||||||
|
consumer.apply(this._service.oauth.bearer).forRoutes('oauth2/user');
|
||||||
consumer.apply(AuthMiddleware).forRoutes('oauth2/authorize');
|
consumer.apply(AuthMiddleware).forRoutes('oauth2/authorize');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,14 @@ import { CodeAdapter } from './adapter/code.adapter';
|
|||||||
import { RefreshTokenAdapter } from './adapter/refresh-token.adapter';
|
import { RefreshTokenAdapter } from './adapter/refresh-token.adapter';
|
||||||
import { UserAdapter } from './adapter/user.adapter';
|
import { UserAdapter } from './adapter/user.adapter';
|
||||||
|
|
||||||
|
const SCOPE_DESCRIPTION: Record<string, string> = {
|
||||||
|
email: 'Email address',
|
||||||
|
image: 'Profile picture',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALWAYS_AVAILABLE = ['Username and display name'];
|
||||||
|
const ALWAYS_UNAVAILABLE = ['Password and other account settings'];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OAuth2Service {
|
export class OAuth2Service {
|
||||||
private _oauthAdapter: OAuth2AdapterModel = {
|
private _oauthAdapter: OAuth2AdapterModel = {
|
||||||
@ -20,10 +28,31 @@ export class OAuth2Service {
|
|||||||
code: new CodeAdapter(this),
|
code: new CodeAdapter(this),
|
||||||
};
|
};
|
||||||
|
|
||||||
public oauth = new OAuth2Provider(this._oauthAdapter, async (req, res) => {
|
public oauth = new OAuth2Provider(
|
||||||
res.render('authorize');
|
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(
|
constructor(
|
||||||
public token: TokenService,
|
public token: TokenService,
|
||||||
public userService: UserService,
|
public userService: UserService,
|
||||||
@ -36,6 +65,10 @@ export class OAuth2Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public splitScope(scope: string): string[] {
|
public splitScope(scope: string): string[] {
|
||||||
|
if (!scope) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return scope.includes(',')
|
return scope.includes(',')
|
||||||
? scope.split(',').map((item) => item.trim())
|
? scope.split(',').map((item) => item.trim())
|
||||||
: scope.split(' ');
|
: scope.split(' ');
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
Res,
|
Res,
|
||||||
Session,
|
Session,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { Throttle } from '@nestjs/throttler';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { SessionData } from 'express-session';
|
import { SessionData } from 'express-session';
|
||||||
import { UserService } from 'src/modules/objects/user/user.service';
|
import { UserService } from 'src/modules/objects/user/user.service';
|
||||||
@ -32,6 +33,7 @@ export class RegisterController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@Throttle(3, 10)
|
||||||
public async registerRequest(
|
public async registerRequest(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
|
@ -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 { QRCodeService } from 'src/modules/utility/services/qr-code.service';
|
||||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||||
|
|
||||||
@Controller('/two-factor')
|
@Controller('/account/two-factor')
|
||||||
export class TwoFactorController {
|
export class TwoFactorController {
|
||||||
constructor(
|
constructor(
|
||||||
private totp: UserTOTPService,
|
private totp: UserTOTPService,
|
||||||
@ -15,7 +15,7 @@ export class TwoFactorController {
|
|||||||
private form: FormUtilityService,
|
private form: FormUtilityService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get('activate')
|
||||||
public async twoFAStatus(
|
public async twoFAStatus(
|
||||||
@Session() session: SessionData,
|
@Session() session: SessionData,
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@ -52,7 +52,7 @@ export class TwoFactorController {
|
|||||||
res.redirect('/');
|
res.redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post('activate')
|
||||||
public async twoFAActivate(
|
public async twoFAActivate(
|
||||||
@Session() session: SessionData,
|
@Session() session: SessionData,
|
||||||
@Body() body: { code: string },
|
@Body() body: { code: string },
|
||||||
|
@ -11,7 +11,7 @@ import { TwoFactorController } from './two-factor.controller';
|
|||||||
})
|
})
|
||||||
export class TwoFactorModule implements NestModule {
|
export class TwoFactorModule implements NestModule {
|
||||||
configure(consumer: MiddlewareConsumer) {
|
configure(consumer: MiddlewareConsumer) {
|
||||||
consumer.apply(AuthMiddleware).forRoutes('two-factor');
|
consumer.apply(AuthMiddleware).forRoutes('/account/two-factor/activate');
|
||||||
consumer.apply(FlashMiddleware).forRoutes('*');
|
consumer.apply(FlashMiddleware).forRoutes('*');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
@ -53,10 +54,10 @@ export class OAuth2Client {
|
|||||||
})
|
})
|
||||||
public updated_at: Date;
|
public updated_at: Date;
|
||||||
|
|
||||||
@OneToOne(() => Upload, { nullable: true, onDelete: 'SET NULL' })
|
@ManyToOne(() => Upload, { nullable: true, onDelete: 'SET NULL' })
|
||||||
public picture: Upload;
|
public picture: Upload;
|
||||||
|
|
||||||
@OneToOne(() => User, { nullable: true, onDelete: 'SET NULL' })
|
@ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' })
|
||||||
public owner: User;
|
public owner: User;
|
||||||
|
|
||||||
@OneToMany(() => OAuth2ClientURL, (url) => url.client)
|
@OneToMany(() => OAuth2ClientURL, (url) => url.client)
|
||||||
|
@ -22,9 +22,15 @@ export class OAuth2ClientService {
|
|||||||
let client: OAuth2Client;
|
let client: OAuth2Client;
|
||||||
|
|
||||||
if (typeof id === 'string') {
|
if (typeof id === 'string') {
|
||||||
client = await this.clientRepository.findOne({ client_id: id });
|
client = await this.clientRepository.findOne(
|
||||||
|
{ client_id: id },
|
||||||
|
{ relations: ['urls', 'picture'] },
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
client = await this.clientRepository.findOne({ id });
|
client = await this.clientRepository.findOne(
|
||||||
|
{ id },
|
||||||
|
{ relations: ['urls', 'picture'] },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
@ -34,14 +40,23 @@ export class OAuth2ClientService {
|
|||||||
id: string,
|
id: string,
|
||||||
type?: OAuth2ClientURLType,
|
type?: OAuth2ClientURLType,
|
||||||
): Promise<OAuth2ClientURL[]> {
|
): Promise<OAuth2ClientURL[]> {
|
||||||
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<boolean> {
|
public async checkRedirectURI(id: string, url: string): Promise<boolean> {
|
||||||
return !!(await this.clientUrlRepository.findOne({
|
return !!(await this.clientUrlRepository.findOne(
|
||||||
|
{
|
||||||
client: { client_id: id },
|
client: { client_id: id },
|
||||||
url,
|
url,
|
||||||
type: OAuth2ClientURLType.REDIRECT_URI,
|
type: OAuth2ClientURLType.REDIRECT_URI,
|
||||||
}));
|
},
|
||||||
|
{ relations: ['client'] },
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,10 +36,13 @@ export class OAuth2TokenService {
|
|||||||
token: string,
|
token: string,
|
||||||
type: OAuth2TokenType,
|
type: OAuth2TokenType,
|
||||||
): Promise<OAuth2Token> {
|
): Promise<OAuth2Token> {
|
||||||
return this.tokenRepository.findOne({
|
return this.tokenRepository.findOne(
|
||||||
|
{
|
||||||
token,
|
token,
|
||||||
type,
|
type,
|
||||||
});
|
},
|
||||||
|
{ relations: ['client', 'user'] },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchByUserIdClientId(
|
public async fetchByUserIdClientId(
|
||||||
@ -57,6 +60,7 @@ export class OAuth2TokenService {
|
|||||||
},
|
},
|
||||||
type,
|
type,
|
||||||
},
|
},
|
||||||
|
relations: ['client', 'user'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
@ -47,7 +48,7 @@ export class User {
|
|||||||
})
|
})
|
||||||
public updated_at: Date;
|
public updated_at: Date;
|
||||||
|
|
||||||
@OneToOne(() => Upload, {
|
@ManyToOne(() => Upload, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
onDelete: 'SET NULL',
|
onDelete: 'SET NULL',
|
||||||
onUpdate: 'CASCADE',
|
onUpdate: 'CASCADE',
|
||||||
|
@ -21,36 +21,42 @@ export class UserService {
|
|||||||
private config: ConfigurationService,
|
private config: ConfigurationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getById(id: number): Promise<User> {
|
public async getById(id: number, relations?: string[]): Promise<User> {
|
||||||
return this.userRepository.findOne({ id });
|
return this.userRepository.findOne({ id }, { relations });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getByUUID(uuid: string): Promise<User> {
|
public async getByUUID(uuid: string, relations?: string[]): Promise<User> {
|
||||||
return this.userRepository.findOne({ uuid });
|
return this.userRepository.findOne({ uuid }, { relations });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getByEmail(email: string): Promise<User> {
|
public async getByEmail(email: string, relations?: string[]): Promise<User> {
|
||||||
return this.userRepository.findOne({ email });
|
return this.userRepository.findOne({ email }, { relations });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getByUsername(username: string): Promise<User> {
|
public async getByUsername(
|
||||||
return this.userRepository.findOne({ username });
|
username: string,
|
||||||
|
relations?: string[],
|
||||||
|
): Promise<User> {
|
||||||
|
return this.userRepository.findOne({ username }, { relations });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(input: string | number): Promise<User> {
|
public async get(
|
||||||
|
input: string | number,
|
||||||
|
relations?: string[],
|
||||||
|
): Promise<User> {
|
||||||
if (typeof input === 'number') {
|
if (typeof input === 'number') {
|
||||||
return this.getById(input);
|
return this.getById(input, relations);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.includes('@')) {
|
if (input.includes('@')) {
|
||||||
return this.getByEmail(input);
|
return this.getByEmail(input, relations);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.length === 36 && input.includes('-')) {
|
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<User> {
|
public async updateUser(user: User): Promise<User> {
|
||||||
|
@ -14,6 +14,10 @@ export class TokenService {
|
|||||||
return crypto.randomBytes(length).toString('hex').slice(0, length);
|
return crypto.randomBytes(length).toString('hex').slice(0, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public generateSecret(): string {
|
||||||
|
return crypto.randomBytes(256 / 8).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
public createUUID(): string {
|
public createUUID(): string {
|
||||||
return v4();
|
return v4();
|
||||||
}
|
}
|
||||||
|
102
src/scss/_authorize.scss
Normal file
102
src/scss/_authorize.scss
Normal file
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,7 @@
|
|||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
background-color: #2e6b81;
|
background-color: #2e6b81;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin: 2rem auto 0;
|
margin: 2rem auto;
|
||||||
padding: 4rem;
|
padding: 4rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@ -26,12 +26,16 @@
|
|||||||
background-color: #042b3a;
|
background-color: #042b3a;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto 2rem auto;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0px 6px 62px -14px rgba(0, 0, 0, 0.45);
|
box-shadow: 0px 6px 62px -14px rgba(0, 0, 0, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--no-margin {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@include break-on(xs, down) {
|
@include break-on(xs, down) {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -16,3 +16,6 @@
|
|||||||
.flex-row {
|
.flex-row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
@import 'button';
|
@import 'button';
|
||||||
@import 'flex';
|
@import 'flex';
|
||||||
@import 'alert';
|
@import 'alert';
|
||||||
|
@import 'authorize';
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
@ -23,3 +24,21 @@ body {
|
|||||||
background-color: #314550;
|
background-color: #314550;
|
||||||
text-shadow: black 1px 1px 2px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,7 +1,56 @@
|
|||||||
extends partials/layout.pug
|
extends partials/layout.pug
|
||||||
|
|
||||||
block title
|
block title
|
||||||
|Icy Network | Authorize
|
|Icy Network | Authorize application
|
||||||
|
|
||||||
block body
|
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
|
||||||
|
@ -6,7 +6,7 @@ block title
|
|||||||
block body
|
block body
|
||||||
include ../partials/logo.pug
|
include ../partials/logo.pug
|
||||||
div.container
|
div.container
|
||||||
div.center-box
|
div.center-box.center-box--no-margin
|
||||||
h1 Log in
|
h1 Log in
|
||||||
if message.text
|
if message.text
|
||||||
if message.error
|
if message.error
|
||||||
|
Reference in New Issue
Block a user