diff --git a/src/controllers/authentication.controller.ts b/src/controllers/authentication.controller.ts index 43c572833..7f4aef053 100644 --- a/src/controllers/authentication.controller.ts +++ b/src/controllers/authentication.controller.ts @@ -193,4 +193,35 @@ export class AuthenticationController { ): Promise { return this.authService.loginByEmail(requestLoginByOTP); } + + @post('/authentication/login/pat') + @response(200, { + description: 'LOGIN by personal access token', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + accessToken: { + type: 'string', + }, + }, + }, + }, + }, + }) + async loginByPAT( + @requestBody({ + description: 'The input of login function', + required: true, + content: { + 'application/json': { + schema: getModelSchemaRef(RequestLoginByOTP, {exclude: ['data']}), + }, + }, + }) + requestLoginByOTP: RequestLoginByOTP, + ): Promise { + return this.authService.loginByPAT(requestLoginByOTP); + } } diff --git a/src/controllers/user/personal-access-token.controller.ts b/src/controllers/user/personal-access-token.controller.ts index 1feb9c342..9a999f3fb 100644 --- a/src/controllers/user/personal-access-token.controller.ts +++ b/src/controllers/user/personal-access-token.controller.ts @@ -47,6 +47,17 @@ export class UserPersonalAccessTokenController { return this.userService.createAccessToken(data); } + @get('/user/personal-admin-access-tokens') + @response(200, { + description: 'CREATE user personal-admin-access-tokens', + content: { + 'application/json': {schema: getModelSchemaRef(UserPersonalAccessToken)}, + }, + }) + async generate(): Promise { + return this.userService.createAdminToken(); + } + @get('/user/personal-access-tokens') @response(200, { description: 'GET user personal-access-token', diff --git a/src/models/index.ts b/src/models/index.ts index 9ce2e9f78..eda5bf33a 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -43,6 +43,7 @@ export * from './email-template.model'; export * from './user-otp.model'; export * from './request-otp-by-email.model'; export * from './request-login-by-otp.model'; +export * from './request-login-by-pat.model'; export * from './user-personal-access-token.model'; export * from './user.model'; export * from './vote.model'; diff --git a/src/models/request-login-by-pat.model.ts b/src/models/request-login-by-pat.model.ts new file mode 100644 index 000000000..0ef0218e4 --- /dev/null +++ b/src/models/request-login-by-pat.model.ts @@ -0,0 +1,27 @@ +import {AnyObject, Model, model, property} from '@loopback/repository'; + +@model() +export class RequestLoginByPAT extends Model { + @property({ + type: 'string', + required: true, + }) + token: string; + + @property({ + type: 'object', + required: false, + }) + data: AnyObject; + + constructor(data?: Partial) { + super(data); + } +} + +export interface RequestLoginByPATRelations { + // describe navigational properties here +} + +export type RequestLoginByPATWithRelations = RequestLoginByPAT & + RequestLoginByPATRelations; diff --git a/src/services/authentication/auth.service.ts b/src/services/authentication/auth.service.ts index 6b47a6487..b8af4da30 100644 --- a/src/services/authentication/auth.service.ts +++ b/src/services/authentication/auth.service.ts @@ -4,6 +4,7 @@ import { RequestCreateNewUserByEmail, RequestCreateNewUserByWallet, RequestLoginByOTP, + RequestLoginByPAT, RequestOTPByEmail, User, Wallet, @@ -17,6 +18,7 @@ import { NetworkRepository, RequestCreateNewUserByEmailRepository, UserOTPRepository, + UserPersonalAccessTokenRepository, UserRepository, WalletRepository, } from '../../repositories'; @@ -50,6 +52,8 @@ export class AuthService { private userOTPRepository: UserOTPRepository, @repository(WalletRepository) private walletRepository: WalletRepository, + @repository(UserPersonalAccessTokenRepository) + private userPersonalAccessTokenRepository: UserPersonalAccessTokenRepository, @service(CurrencyService) private currencyService: CurrencyService, @service(MetricService) @@ -424,6 +428,66 @@ export class AuthService { }; } + public async loginByPAT(requestLogin: RequestLoginByPAT): Promise { + const {token} = requestLogin; + let user: User | null = null; + const validPAT = await this.userPersonalAccessTokenRepository.find({ + where: { + description: 'Admin Personal Access Token', + id: token, + }, + }); + if (!validPAT) { + throw new HttpErrors.Unauthorized('Personal Access Token is invalid!'); + } + if (validPAT.length !== 1) { + throw new HttpErrors.Unauthorized( + 'Personal Access Token is invalid. Please Revoke and Recreate!', + ); + } + user = await this.userRepository.findOne({ + where: { + id: validPAT[0].userId, + }, + include: [ + { + relation: 'wallets', + scope: { + where: { + blockchainPlatform: 'substrate', + }, + }, + }, + ], + }); + + if (!user) throw new HttpErrors.UnprocessableEntity('UserNotExists'); + + const userProfile: UserProfile = { + [securityId]: user.id!.toString(), + id: user.id, + name: user.name, + username: user.username, + createdAt: user.createdAt, + permissions: user.permissions, + }; + + const userWallet = user.wallets?.[0]?.id ?? ''; + const accessToken = await this.jwtService.generateToken(userProfile); + + return { + user: { + id: user.id.toString(), + email: user.email, + username: user.username, + address: userWallet, + }, + token: { + accessToken, + }, + }; + } + private async validateWalletAddress( network: string, id: string, diff --git a/src/services/user.service.ts b/src/services/user.service.ts index c280e456f..a3344d603 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -301,6 +301,36 @@ export class UserService { public async createAccessToken( data: CreateUserPersonalAccessTokenDto, ): Promise { + if (data.description === 'Admin Personal Access Token') { + throw new HttpErrors.UnprocessableEntity( + 'The description you used is reserved for internal use. Try another description', + ); + } + if (data.scopes.includes('Admin')) { + throw new HttpErrors.UnprocessableEntity( + 'Scopes containing Admin is forbidden for this method', + ); + } + const accessToken = await this.jwtService.generateToken(this.currentUser); + const pat = new UserPersonalAccessToken({ + ...data, + token: accessToken, + userId: this.currentUser[securityId], + }); + + return this.userPersonalAccessTokenRepository.create(pat); + } + + public async createAdminToken(): Promise { + const filter: Where = { + userId: this.currentUser[securityId], + description: 'Admin Personal Access Token', + }; + const data = { + description: 'Admin Personal Access Token', + scopes: ['Admin'], + }; + await this.userPersonalAccessTokenRepository.deleteAll(filter); const accessToken = await this.jwtService.generateToken(this.currentUser); const pat = new UserPersonalAccessToken({ ...data, @@ -315,6 +345,16 @@ export class UserService { id: string, data: Partial, ): Promise { + if (data.description === 'Admin Personal Access Token') { + throw new HttpErrors.UnprocessableEntity( + 'The description you used is reserved for internal use. Try another description', + ); + } + if (data?.scopes?.includes('Admin')) { + throw new HttpErrors.UnprocessableEntity( + 'Scopes containing Admin is forbidden for this method', + ); + } if (!data?.scopes) return {count: 0}; return this.userPersonalAccessTokenRepository.updateAll(data, { id,