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/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",
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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<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) {
|
||||
consumer.apply(this._service.oauth.express()).forRoutes('oauth2/*');
|
||||
consumer.apply(this._service.oauth.bearer).forRoutes('oauth2/user');
|
||||
consumer.apply(AuthMiddleware).forRoutes('oauth2/authorize');
|
||||
}
|
||||
}
|
||||
|
@ -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<string, string> = {
|
||||
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,10 +28,31 @@ 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,
|
||||
public userService: UserService,
|
||||
@ -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(' ');
|
||||
|
@ -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,
|
||||
|
@ -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 },
|
||||
|
@ -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('*');
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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<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> {
|
||||
return !!(await this.clientUrlRepository.findOne({
|
||||
return !!(await this.clientUrlRepository.findOne(
|
||||
{
|
||||
client: { client_id: id },
|
||||
url,
|
||||
type: OAuth2ClientURLType.REDIRECT_URI,
|
||||
}));
|
||||
},
|
||||
{ relations: ['client'] },
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -36,10 +36,13 @@ export class OAuth2TokenService {
|
||||
token: string,
|
||||
type: OAuth2TokenType,
|
||||
): Promise<OAuth2Token> {
|
||||
return this.tokenRepository.findOne({
|
||||
return this.tokenRepository.findOne(
|
||||
{
|
||||
token,
|
||||
type,
|
||||
});
|
||||
},
|
||||
{ relations: ['client', 'user'] },
|
||||
);
|
||||
}
|
||||
|
||||
public async fetchByUserIdClientId(
|
||||
@ -57,6 +60,7 @@ export class OAuth2TokenService {
|
||||
},
|
||||
type,
|
||||
},
|
||||
relations: ['client', 'user'],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -21,36 +21,42 @@ export class UserService {
|
||||
private config: ConfigurationService,
|
||||
) {}
|
||||
|
||||
public async getById(id: number): Promise<User> {
|
||||
return this.userRepository.findOne({ id });
|
||||
public async getById(id: number, relations?: string[]): Promise<User> {
|
||||
return this.userRepository.findOne({ id }, { relations });
|
||||
}
|
||||
|
||||
public async getByUUID(uuid: string): Promise<User> {
|
||||
return this.userRepository.findOne({ uuid });
|
||||
public async getByUUID(uuid: string, relations?: string[]): Promise<User> {
|
||||
return this.userRepository.findOne({ uuid }, { relations });
|
||||
}
|
||||
|
||||
public async getByEmail(email: string): Promise<User> {
|
||||
return this.userRepository.findOne({ email });
|
||||
public async getByEmail(email: string, relations?: string[]): Promise<User> {
|
||||
return this.userRepository.findOne({ email }, { relations });
|
||||
}
|
||||
|
||||
public async getByUsername(username: string): Promise<User> {
|
||||
return this.userRepository.findOne({ username });
|
||||
public async getByUsername(
|
||||
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') {
|
||||
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<User> {
|
||||
|
@ -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();
|
||||
}
|
||||
|
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;
|
||||
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;
|
||||
}
|
||||
|
@ -16,3 +16,6 @@
|
||||
.flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user