Commit 17ad7f0e authored by Revant Nandgaonkar's avatar Revant Nandgaonkar

feat: Improve User Info Endpoint

Merge branch 'improved-profile' into 'develop'

See merge request castlecraft/building-blocks!301
parents 99b5f41d 01217f14
import { Test, TestingModule } from '@nestjs/testing';
import { CqrsModule } from '@nestjs/cqrs';
import { OAuth2Controller } from './oauth2.controller';
import { OAuth2Service } from './oauth2.service';
import { BearerTokenService } from '../../../auth/entities/bearer-token/bearer-token.service';
......@@ -9,9 +10,13 @@ describe('OAuth2Controller', () => {
let module: TestingModule;
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [CqrsModule],
controllers: [OAuth2Controller],
providers: [
OAuth2Service,
{
provide: OAuth2Service,
useValue: {},
},
{
provide: BearerTokenService,
useValue: {},
......
import { Test, TestingModule } from '@nestjs/testing';
import { HttpModule } from '@nestjs/common';
import { OAuth2Service } from './oauth2.service';
import { UserService } from '../../../user-management/entities/user/user.service';
import { BearerTokenService } from '../../../auth/entities/bearer-token/bearer-token.service';
import { ServerSettingsService } from '../../../system-settings/entities/server-settings/server-settings.service';
import { ClientService } from 'client-management/entities/client/client.service';
describe('OAuth2Service', () => {
let service: OAuth2Service;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [HttpModule],
providers: [
OAuth2Service,
{
......@@ -17,6 +21,14 @@ describe('OAuth2Service', () => {
provide: UserService,
useValue: {}, // provide mock values
},
{
provide: ServerSettingsService,
useValue: {},
},
{
provide: ClientService,
useValue: {},
},
],
}).compile();
service = module.get<OAuth2Service>(OAuth2Service);
......
import { Injectable } from '@nestjs/common';
import { Injectable, HttpService } from '@nestjs/common';
import { i18n } from '../../../i18n/i18n.config';
import { ROLES } from '../../../constants/app-strings';
import { from, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { switchMap, map, catchError } from 'rxjs/operators';
import { BearerTokenService } from '../../../auth/entities/bearer-token/bearer-token.service';
import { UserService } from '../../../user-management/entities/user/user.service';
import { ServerSettingsService } from '../../../system-settings/entities/server-settings/server-settings.service';
import { ClientService } from '../../../client-management/entities/client/client.service';
export const PROFILE_USERINFO_ENDPOINT = '/profile/v1/userinfo';
@Injectable()
export class OAuth2Service {
constructor(
private readonly bearerTokenService: BearerTokenService,
private readonly userService: UserService,
private readonly settings: ServerSettingsService,
private readonly http: HttpService,
private readonly clientService: ClientService,
) {}
async tokenRevoke(token) {
......@@ -62,16 +69,99 @@ export class OAuth2Service {
}
getProfile(req) {
return from(this.userService.findOne({ uuid: req.user.user })).pipe(
switchMap(user => {
return of({
uuid: user.uuid,
name: user.name,
email: user.email,
roles: user.roles,
verified_email: user.email,
verified: user.email ? true : false,
});
const accessToken = this.getAccessToken(req);
const uuid = req.user.user;
return from(this.settings.find()).pipe(
switchMap(settings => {
if (!settings.identityProviderClientId) {
return this.observeLocalProfile(uuid, accessToken);
} else {
return this.observeIdentityProviderProfile(
settings.identityProviderClientId,
uuid,
accessToken,
);
}
}),
);
}
getAccessToken(request) {
if (!request.headers.authorization) {
if (!request.query.access_token) return null;
}
return (
request.query.access_token ||
request.headers.authorization.split(' ')[1] ||
null
);
}
observeLocalProfile(uuid: string, accessToken: string) {
return from(this.settings.find()).pipe(
switchMap(settings => {
return from(this.bearerTokenService.findOne({ accessToken })).pipe(
switchMap(token => {
return from(this.userService.findOne({ uuid })).pipe(
switchMap(user => {
return of({
aud: token.client,
iss: settings.issuerUrl,
sub: user.uuid,
name: user.name,
email: user.email,
roles: user.roles,
verified_email: user.email,
verified: user.email ? true : false,
});
}),
);
}),
);
}),
);
}
observeIdentityProviderProfile(
identityProviderClientId: string,
uuid: string,
accessToken: string,
) {
return from(
this.clientService.findOne({
clientId: identityProviderClientId,
}),
).pipe(
switchMap(client => {
let parsedUrl: URL;
let url: string = '';
try {
parsedUrl = new URL(client.redirectUris[0]);
url = `${parsedUrl.protocol}//${parsedUrl.hostname}`;
if (parsedUrl.port) url += `:${parsedUrl.port}`;
return this.http
.get(url + PROFILE_USERINFO_ENDPOINT, {
headers: {
Authorization: 'Bearer ' + accessToken,
},
})
.pipe(
catchError(error => of({ data: {} })),
map(res => res.data),
);
} catch (error) {
return of({});
}
}),
switchMap(userInfo => {
return this.observeLocalProfile(uuid, accessToken).pipe(
map(localUser => {
return {
...localUser,
...userInfo,
};
}),
);
}),
);
}
......
......@@ -6,6 +6,7 @@ export interface IDTokenClaims {
iat?: number;
email?: string;
verified_email?: string;
verified?: boolean;
name?: string;
family_name?: string;
given_name?: string;
......
......@@ -6,3 +6,4 @@ export const COMMUNICATION_SERVER = 'communication-server';
export const PUBLIC = 'public';
export const APP_NAME = 'identity-provider';
export const SWAGGER_ROUTE = 'api-docs';
export const PROFILE = 'profile';
export const SETTINGS_ALREADY_EXISTS = 'Settings already exists';
export const PLEASE_RUN_SETUP = 'Please run setup';
export const SOMETHING_WENT_WRONG = 'Something went wrong';
export const INVALID_USER = 'Invalid User';
......@@ -14,6 +14,7 @@ import {
UploadedFile,
Delete,
} from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { FileInterceptor } from '@nestjs/platform-express';
import { ProfileService } from '../../../profile-management/entities/profile/profile.service';
import { PersonalDetailsDTO } from './personal-details-dto';
......@@ -21,10 +22,14 @@ import { Profile } from '../../../profile-management/entities/profile/profile.en
import { ProfileDetailsDTO } from './profile-details-dto';
import { TokenGuard } from '../../../auth/guards/token.guard';
import { multerAvatarConnection } from './multer-avatar.connection';
import { GetUserInfoQuery } from 'profile-management/queries/get-user-info/get-user-info.query';
@Controller('profile')
export class ProfileController {
constructor(private readonly profileService: ProfileService) {}
constructor(
private readonly profileService: ProfileService,
private readonly queryBus: QueryBus,
) {}
@Post('v1/update_profile_details')
@UseGuards(TokenGuard)
......@@ -117,4 +122,11 @@ export class ProfileController {
return await profile.save();
}
}
@Get('v1/userinfo')
@UseGuards(TokenGuard)
userInfo(@Req() req) {
const token = req.token;
return this.queryBus.execute(new GetUserInfoQuery(token));
}
}
......@@ -95,4 +95,7 @@ export class Profile extends BaseEntity implements IDTokenClaims {
@Column()
roles: string[];
@Column()
verified: boolean;
}
......@@ -6,6 +6,7 @@ import { UploadAvatarMetaDataService } from './policies/upload-avatar-meta-data/
import { ProfileManagementCommandHandlers } from './commands';
import { CqrsModule } from '@nestjs/cqrs';
import { ProfileManagementEventHandlers } from './events';
import { ProfileManagementQueryHandlers } from './queries';
@Global()
@Module({
......@@ -17,6 +18,7 @@ import { ProfileManagementEventHandlers } from './events';
UploadAvatarMetaDataService,
...ProfileManagementCommandHandlers,
...ProfileManagementEventHandlers,
...ProfileManagementQueryHandlers,
],
})
export class ProfileManagementModule {}
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { NotImplementedException } from '@nestjs/common';
import { GetUserInfoQuery } from './get-user-info.query';
import { ProfileService } from '../../entities/profile/profile.service';
import { PLEASE_RUN_SETUP } from '../../../constants/messages';
import { IDTokenClaims } from '../../../auth/entities/token-cache/id-token-claims.interfaces';
import { PROFILE } from '../../../constants/app-strings';
import { ServerSettingsService } from '../../../system-settings/entities/server-settings/server-settings.service';
@QueryHandler(GetUserInfoQuery)
export class GetUserInfoHandler implements IQueryHandler<GetUserInfoQuery> {
constructor(
private readonly profileService: ProfileService,
private readonly settings: ServerSettingsService,
) {}
async execute(query: GetUserInfoQuery) {
const settings = await this.settings.find();
const scopedProfile: IDTokenClaims = {};
if (!settings) {
throw new NotImplementedException(PLEASE_RUN_SETUP);
}
const { token } = query;
const profile = await this.profileService.findOne({ uuid: token.sub });
if (profile && token.scope.includes(PROFILE)) {
scopedProfile.family_name = profile.familyName;
scopedProfile.aud = token.clientId;
scopedProfile.given_name = profile.givenName;
scopedProfile.middle_name = profile.middleName;
scopedProfile.nickname = profile.nickname;
scopedProfile.preferred_username = profile.preferredUsername;
scopedProfile.profile = profile.profile;
scopedProfile.picture = profile.picture;
scopedProfile.website = profile.website;
scopedProfile.gender = profile.gender;
scopedProfile.birthdate = profile.birthdate;
scopedProfile.zoneinfo = profile.zoneinfo;
scopedProfile.locale = profile.locale;
scopedProfile.updated_at = profile.modified;
}
return scopedProfile;
}
}
import { IQuery } from '@nestjs/cqrs';
import { TokenCache } from '../../../auth/entities/token-cache/token-cache.entity';
export class GetUserInfoQuery implements IQuery {
constructor(public readonly token: TokenCache) {}
}
import { GetUserInfoHandler } from './get-user-info/get-user-info.handler';
export const ProfileManagementQueryHandlers = [GetUserInfoHandler];
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment