Skip to content

Commit

Permalink
feat: add authentication with personal access token (#926)
Browse files Browse the repository at this point in the history
* feat: add pat

* fix: method type

* format fix
  • Loading branch information
RiXelanya authored Oct 24, 2023
1 parent 0b87bf4 commit b662acb
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 0 deletions.
31 changes: 31 additions & 0 deletions src/controllers/authentication.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,35 @@ export class AuthenticationController {
): Promise<UserToken> {
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<UserToken> {
return this.authService.loginByPAT(requestLoginByOTP);
}
}
11 changes: 11 additions & 0 deletions src/controllers/user/personal-access-token.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserPersonalAccessToken> {
return this.userService.createAdminToken();
}

@get('/user/personal-access-tokens')
@response(200, {
description: 'GET user personal-access-token',
Expand Down
1 change: 1 addition & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
27 changes: 27 additions & 0 deletions src/models/request-login-by-pat.model.ts
Original file line number Diff line number Diff line change
@@ -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<RequestLoginByPAT>) {
super(data);
}
}

export interface RequestLoginByPATRelations {
// describe navigational properties here
}

export type RequestLoginByPATWithRelations = RequestLoginByPAT &
RequestLoginByPATRelations;
64 changes: 64 additions & 0 deletions src/services/authentication/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
RequestCreateNewUserByEmail,
RequestCreateNewUserByWallet,
RequestLoginByOTP,
RequestLoginByPAT,
RequestOTPByEmail,
User,
Wallet,
Expand All @@ -17,6 +18,7 @@ import {
NetworkRepository,
RequestCreateNewUserByEmailRepository,
UserOTPRepository,
UserPersonalAccessTokenRepository,
UserRepository,
WalletRepository,
} from '../../repositories';
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -424,6 +428,66 @@ export class AuthService {
};
}

public async loginByPAT(requestLogin: RequestLoginByPAT): Promise<UserToken> {
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,
Expand Down
40 changes: 40 additions & 0 deletions src/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,36 @@ export class UserService {
public async createAccessToken(
data: CreateUserPersonalAccessTokenDto,
): Promise<UserPersonalAccessToken> {
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<UserPersonalAccessToken> {
const filter: Where<UserPersonalAccessToken> = {
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,
Expand All @@ -315,6 +345,16 @@ export class UserService {
id: string,
data: Partial<UpdateUserPersonalAccessTokenDto>,
): Promise<Count> {
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,
Expand Down

0 comments on commit b662acb

Please sign in to comment.