feat: more implementing oidc-core
This commit is contained in:
parent
b38f5841e3
commit
c65ba92322
17 changed files with 333 additions and 49 deletions
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"printWidth": 100
|
||||
}
|
|
@ -32,6 +32,7 @@
|
|||
"@nestjs/config": "^3.2.3",
|
||||
"@nestjs/core": "^10.3.10",
|
||||
"@nestjs/platform-express": "^10.3.10",
|
||||
"@nestjs/swagger": "^7.4.0",
|
||||
"@nestjs/terminus": "^10.2.3",
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
|
@ -52,7 +53,8 @@
|
|||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"typia": "^6.5.1",
|
||||
"uuid": "^10.0.0"
|
||||
"uuid": "^10.0.0",
|
||||
"wildcard": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.4.2",
|
||||
|
|
|
@ -26,6 +26,9 @@ importers:
|
|||
'@nestjs/platform-express':
|
||||
specifier: ^10.3.10
|
||||
version: 10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10)
|
||||
'@nestjs/swagger':
|
||||
specifier: ^7.4.0
|
||||
version: 7.4.0(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)
|
||||
'@nestjs/terminus':
|
||||
specifier: ^10.2.3
|
||||
version: 10.2.3(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/typeorm@10.0.2(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.10.2)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3))))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.10.2)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3)))
|
||||
|
@ -89,6 +92,9 @@ importers:
|
|||
uuid:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
wildcard:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
devDependencies:
|
||||
'@nestjs/cli':
|
||||
specifier: ^10.4.2
|
||||
|
@ -4001,6 +4007,9 @@ packages:
|
|||
resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
wildcard@2.0.1:
|
||||
resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==}
|
||||
|
||||
with@7.0.2:
|
||||
resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
@ -8407,6 +8416,8 @@ snapshots:
|
|||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
||||
wildcard@2.0.1: {}
|
||||
|
||||
with@7.0.2:
|
||||
dependencies:
|
||||
'@babel/parser': 7.24.8
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
// This is the internal client ID for the application itself.
|
||||
export const internalClientId = 'system.internal.management';
|
||||
export const internalRedirectTag = 'system.internalRedirect.management';
|
||||
|
||||
// This is the master org configuration. This is the default org configuration that is used when no org is specified.
|
||||
export const masterOrgId = 1;
|
||||
export const masterOrgName = 'WaterWolf';
|
||||
export const masterOrgSlug = 'waterwolf';
|
||||
|
||||
export const masterOrgAdminRole = 'admin';
|
||||
export const masterOrgUserRole = 'user';
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { All, Controller, Req, Res, UseInterceptors } from '@nestjs/common';
|
||||
import { ApiExcludeController } from '@nestjs/swagger';
|
||||
|
||||
import { OidcService } from '../oidc/core.service';
|
||||
import { ExpressResErrorInterceptor } from '../../interceptor/express_res_error.interceptor';
|
||||
|
||||
@UseInterceptors(new ExpressResErrorInterceptor())
|
||||
@ApiExcludeController()
|
||||
@Controller('oidc')
|
||||
export class OidcController {
|
||||
constructor(private readonly oidcService: OidcService) {}
|
||||
@All('/*')
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
public mountedOidc(@Req() req: any, @Res() res: any) {
|
||||
req.url = req.originalUrl.replace('/oidc', '');
|
||||
const callback = this.oidcService.provider.callback();
|
||||
return callback(req, res);
|
||||
}
|
||||
}
|
|
@ -3,8 +3,9 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|||
import { promises as fs } from 'fs';
|
||||
import type Provider from 'oidc-provider';
|
||||
import psl from 'psl';
|
||||
import type { Configuration, errors, KoaContextWithOIDC } from 'oidc-provider';
|
||||
import type { Configuration, errors } from 'oidc-provider';
|
||||
import { createOidcAdapter } from './adapter';
|
||||
import wildcard from 'wildcard';
|
||||
import {
|
||||
ACCESS_TOKEN_LIFE,
|
||||
AUTHORIZATION_TOKEN_LIFE,
|
||||
|
@ -17,6 +18,12 @@ import {
|
|||
REFRESH_TOKEN_LIFE,
|
||||
SESSION_LIFE,
|
||||
} from './oidc.const';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { RedisService } from '../../redis/redis.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { Span } from 'nestjs-otel';
|
||||
import generateId from './helper/nanoid.helper';
|
||||
|
||||
// This is an async import for the oidc-provider package as it's now only esm and we need to use it in a commonjs environment.
|
||||
async function getProvider(): Promise<{
|
||||
|
@ -54,15 +61,24 @@ async function getProvider(): Promise<{
|
|||
* OidcService is the service that handles all interaction with the OIDC library and package
|
||||
*/
|
||||
export class OidcService implements OnModuleInit {
|
||||
private provider!: Provider;
|
||||
private _provider!: Provider;
|
||||
private jwks: any;
|
||||
private cookies: any;
|
||||
private readonly logger = new Logger(OidcService.name);
|
||||
|
||||
constructor() {}
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
|
||||
private readonly configService: ConfigService,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly redisService: RedisService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.loadKeys();
|
||||
|
||||
const { provider, providerErrors } = await getProvider();
|
||||
const baseUrl = this.configService.getOrThrow<string>('base.localUrl');
|
||||
|
||||
const isOrigin = (value: string): boolean => {
|
||||
if (typeof value !== 'string') {
|
||||
|
@ -182,17 +198,14 @@ export class OidcService implements OnModuleInit {
|
|||
},
|
||||
},
|
||||
clientBasedCORS(ctx, origin, client) {
|
||||
// ctx.oidc.route can be used to exclude endpoints from this behaviour, in that case just return
|
||||
// true to always allow CORS on them, false to deny
|
||||
// you may also allow some known internal origins if you want to
|
||||
return (client['client_cors'] as string[]).includes(origin);
|
||||
},
|
||||
jwks: this.jwks,
|
||||
cookies: {
|
||||
keys: this.cookies,
|
||||
},
|
||||
// TODO: FUCKING IMPLEMENT THIS
|
||||
findAccount: this.findAccount.bind(this),
|
||||
// TODO: FUCKING IMPLEMENT THIS AHHHHHHHHHH
|
||||
findAccount: this.userService.oidcFindAccount.bind(this),
|
||||
ttl: {
|
||||
AccessToken: ACCESS_TOKEN_LIFE,
|
||||
RefreshToken: function RefreshTokenTTL(ctx, token, client) {
|
||||
|
@ -223,8 +236,7 @@ export class OidcService implements OnModuleInit {
|
|||
}
|
||||
return (
|
||||
code.scopes.has('offline_access') ||
|
||||
(client.applicationType === 'web' &&
|
||||
client.tokenEndpointAuthMethod === 'none')
|
||||
(client.applicationType === 'web' && client.tokenEndpointAuthMethod === 'none')
|
||||
);
|
||||
},
|
||||
features: {
|
||||
|
@ -242,9 +254,9 @@ export class OidcService implements OnModuleInit {
|
|||
}
|
||||
|
||||
if (token.clientId !== ctx.oidc.client?.clientId) {
|
||||
return (
|
||||
client['allowed_introspection_targets'] as string[]
|
||||
).includes(token.clientId!);
|
||||
return (client['allowed_introspection_targets'] as string[]).includes(
|
||||
token.clientId!,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
@ -296,7 +308,6 @@ export class OidcService implements OnModuleInit {
|
|||
openid: ['sub'],
|
||||
profile: ['name', 'preferred_username', 'picture', 'displayName'],
|
||||
email: ['email', 'email_verified'],
|
||||
roles: ['roles', 'permissions'],
|
||||
},
|
||||
pkce: {
|
||||
methods: ['S256'],
|
||||
|
@ -305,6 +316,68 @@ export class OidcService implements OnModuleInit {
|
|||
},
|
||||
},
|
||||
} as Configuration;
|
||||
|
||||
const oidc = new provider(`${baseUrl}/oidc`, config) as Provider;
|
||||
oidc.proxy = true;
|
||||
|
||||
const { redirectUriAllowed, postLogoutRedirectUriAllowed } = oidc.Client.prototype;
|
||||
|
||||
const hasWildcardHost = (redirectUri: string) => {
|
||||
const { hostname } = new URL(redirectUri);
|
||||
return hostname.includes('*');
|
||||
};
|
||||
|
||||
const wildcardMatches = (redirectUri: string, wildcardUri: string) =>
|
||||
!!wildcard(wildcardUri, redirectUri);
|
||||
|
||||
const logger = this.logger;
|
||||
oidc.Client.prototype.redirectUriAllowed = function wildcardRedirectUriAllowed(redirectUri) {
|
||||
logger.log(
|
||||
{
|
||||
redirectUri,
|
||||
redirectUris: this.redirectUris,
|
||||
},
|
||||
'Checking redirect uri - 1',
|
||||
);
|
||||
if (!this.redirectUris) {
|
||||
return redirectUriAllowed.call(this, redirectUri);
|
||||
}
|
||||
|
||||
if (!this.redirectUris.some(hasWildcardHost)) {
|
||||
return redirectUriAllowed.call(this, redirectUri);
|
||||
}
|
||||
|
||||
const wildcardUris = this.redirectUris.filter(hasWildcardHost);
|
||||
if (wildcardUris.some(wildcardMatches.bind(undefined, redirectUri))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return redirectUriAllowed.call(this, redirectUri);
|
||||
};
|
||||
|
||||
oidc.Client.prototype.postLogoutRedirectUriAllowed =
|
||||
function wildcardPostLogoutRedirectUriAllowed(redirectUri) {
|
||||
if (!this.postLogoutRedirectUris) {
|
||||
return postLogoutRedirectUriAllowed.call(this, redirectUri);
|
||||
}
|
||||
|
||||
if (!this.postLogoutRedirectUris.some(hasWildcardHost)) {
|
||||
return postLogoutRedirectUriAllowed.call(this, redirectUri);
|
||||
}
|
||||
|
||||
const wildcardUris = this.postLogoutRedirectUris.filter(hasWildcardHost);
|
||||
if (wildcardUris.some(wildcardMatches.bind(undefined, redirectUri))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return postLogoutRedirectUriAllowed.call(this, redirectUri);
|
||||
};
|
||||
|
||||
this._provider = oidc;
|
||||
}
|
||||
|
||||
public get provider(): Provider {
|
||||
return this._provider;
|
||||
}
|
||||
|
||||
/// === SUPPORT FUNCTIONS === ///
|
||||
|
@ -317,18 +390,12 @@ export class OidcService implements OnModuleInit {
|
|||
const keysFolder = this.getKeysFolder();
|
||||
|
||||
try {
|
||||
this.jwks = JSON.parse(
|
||||
await fs.readFile(keysFolder + 'keys/jwks.json', 'utf8'),
|
||||
);
|
||||
this.cookies = JSON.parse(
|
||||
await fs.readFile(keysFolder + 'keys/cookies.json', 'utf8'),
|
||||
);
|
||||
this.jwks = JSON.parse(await fs.readFile(keysFolder + 'keys/jwks.json', 'utf8'));
|
||||
this.cookies = JSON.parse(await fs.readFile(keysFolder + 'keys/cookies.json', 'utf8'));
|
||||
|
||||
this.logger.debug(`Loaded oidc keys successfully`);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to load keys, keys should be located ${keysFolder}keys`,
|
||||
);
|
||||
throw new Error(`Failed to load keys, keys should be located ${keysFolder}keys`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -338,4 +405,13 @@ export class OidcService implements OnModuleInit {
|
|||
}
|
||||
return __dirname + '/../../../../';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JTI for an OIDC object
|
||||
* @returns string - The JTI
|
||||
*/
|
||||
@Span()
|
||||
async generateJti(): Promise<string> {
|
||||
return await generateId();
|
||||
}
|
||||
}
|
||||
|
|
43
src/auth/oidc/oidc.errors.ts
Normal file
43
src/auth/oidc/oidc.errors.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Write an error class that extends the base error class
|
||||
//
|
||||
|
||||
export enum OidcErrorType {
|
||||
InvalidRequest = 'invalid_request',
|
||||
InvalidClient = 'invalid_client',
|
||||
InvalidGrant = 'invalid_grant',
|
||||
UnauthorizedClient = 'unauthorized_client',
|
||||
InvalidSession = 'invalid_session',
|
||||
}
|
||||
export default class OidcError extends Error {
|
||||
constructor(
|
||||
public errorType: OidcErrorType,
|
||||
public errorDescription: string,
|
||||
public errorUri?: string,
|
||||
) {
|
||||
super(errorDescription);
|
||||
}
|
||||
|
||||
public toOidcError() {
|
||||
return {
|
||||
error: this.errorType,
|
||||
error_description: this.errorDescription,
|
||||
error_uri: this.errorUri,
|
||||
};
|
||||
}
|
||||
|
||||
public static fromOidcError(oidcError: any) {
|
||||
return new OidcError(oidcError.error, oidcError.error_description, oidcError.error_uri);
|
||||
}
|
||||
|
||||
public static invalidSession() {
|
||||
return new OidcError(OidcErrorType.InvalidSession, 'Session token is invalid');
|
||||
}
|
||||
|
||||
public static invalidRequest() {
|
||||
return new OidcError(OidcErrorType.InvalidRequest, 'Request is invalid');
|
||||
}
|
||||
|
||||
public static invalidClient() {
|
||||
return new OidcError(OidcErrorType.InvalidClient, 'Client is invalid');
|
||||
}
|
||||
}
|
|
@ -1,13 +1,6 @@
|
|||
import type { ClientAuthMethod, ResponseType } from 'oidc-provider';
|
||||
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
import { OidcClientPermission } from './oidc_client_permissions.model';
|
||||
import { MAX_STRING_LENGTH } from '../database.const';
|
||||
|
@ -20,7 +13,7 @@ export class OidcClient {
|
|||
|
||||
// Owner Org ID
|
||||
@Column({ type: 'varchar', length: MAX_STRING_LENGTH, nullable: false })
|
||||
ownerName: string;
|
||||
owner_org_id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: MAX_STRING_LENGTH, nullable: false })
|
||||
client_secret: string;
|
||||
|
@ -28,21 +21,24 @@ export class OidcClient {
|
|||
@Column({ type: 'varchar', length: MAX_STRING_LENGTH, nullable: true })
|
||||
client_name: string;
|
||||
|
||||
@Column({ type: 'simple-array', nullable: false })
|
||||
@Column({ type: 'simple-array', nullable: false, default: [] })
|
||||
redirect_uris: string[];
|
||||
|
||||
@Column({ type: 'simple-array', nullable: true })
|
||||
@Column({ type: 'simple-array', nullable: true, default: [] })
|
||||
client_cors: string[];
|
||||
|
||||
@Column({ type: 'simple-array', nullable: true })
|
||||
@Column({ type: 'simple-array', nullable: true, default: [] })
|
||||
allowed_introspection_targets: string[];
|
||||
|
||||
@Column({ type: 'simple-array', nullable: false })
|
||||
@Column({ type: 'simple-array', nullable: false, default: [] })
|
||||
include_permissions_from_client: string[];
|
||||
|
||||
@Column({ type: 'simple-array', nullable: false })
|
||||
@Column({ type: 'simple-array', nullable: false, default: [] })
|
||||
post_logout_redirect_uris: string[];
|
||||
|
||||
@Column({ type: 'simple-array', nullable: true })
|
||||
default_scopes: string[];
|
||||
|
||||
@Column({ type: 'simple-array', nullable: false })
|
||||
response_types: ResponseType[];
|
||||
|
||||
|
@ -64,10 +60,7 @@ export class OidcClient {
|
|||
@OneToMany(() => OidcClientPermission, (permission) => permission.client)
|
||||
permissions: OidcClientPermission[];
|
||||
|
||||
@ManyToMany(
|
||||
() => OidcClientPermission,
|
||||
(permission) => permission.assignedClients,
|
||||
)
|
||||
@ManyToMany(() => OidcClientPermission, (permission) => permission.assignedClients)
|
||||
@JoinTable({
|
||||
name: 'oidc_client_permissions',
|
||||
joinColumn: {
|
||||
|
|
|
@ -9,6 +9,9 @@ export class Organization {
|
|||
@Column({ length: MAX_STRING_LENGTH })
|
||||
name: string;
|
||||
|
||||
@Column({ length: MAX_STRING_LENGTH, unique: true })
|
||||
slug: string;
|
||||
|
||||
@Column({ length: MAX_STRING_LENGTH, nullable: true })
|
||||
logo?: string;
|
||||
|
||||
|
|
1
src/database/seeds/initial.seed.ts
Normal file
1
src/database/seeds/initial.seed.ts
Normal file
|
@ -0,0 +1 @@
|
|||
// This contains the inital setup data for the IdP. This should be standalone.
|
44
src/interceptor/express_res_error.interceptor.ts
Normal file
44
src/interceptor/express_res_error.interceptor.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import type { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
||||
import { HttpException, Injectable, Logger } from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import { catchError, type Observable, throwError } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Finishes the request if an error was unexpectedly thrown.
|
||||
* This ensures the requests don't hang.
|
||||
* See https://github.com/nestjs/nest/issues/5448
|
||||
*/
|
||||
@Injectable()
|
||||
export class ExpressResErrorInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(ExpressResErrorInterceptor.name);
|
||||
|
||||
intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
const res = ctx.switchToHttp().getResponse<Response>();
|
||||
|
||||
return next.handle().pipe(
|
||||
catchError((err: unknown) => {
|
||||
if (!res.writableEnded) {
|
||||
// Send an error response if it hasn't been done so the request doesn't hang.
|
||||
|
||||
this.logger.error(err, 'Uncaught internal error');
|
||||
|
||||
if (err instanceof HttpException) {
|
||||
const statusCode = err.getStatus();
|
||||
|
||||
res.status(statusCode).send({
|
||||
message: err.getResponse(),
|
||||
statusCode,
|
||||
});
|
||||
} else {
|
||||
res.status(500).send({
|
||||
message: 'Internal Server Error',
|
||||
error: 'Internal Server Error',
|
||||
statusCode: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
return throwError(() => err);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -83,7 +83,7 @@ export class RedisService implements OnApplicationShutdown {
|
|||
* @param key Key for the value to get
|
||||
* @returns
|
||||
*/
|
||||
public async get(key: string): Promise<string | object | null> {
|
||||
public async get<T = string>(key: string): Promise<T | null> {
|
||||
this.logger.debug(`Getting key ${key}`);
|
||||
const value = await this._ioredis.get(key);
|
||||
if (!value) {
|
||||
|
@ -91,9 +91,9 @@ export class RedisService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
return JSON.parse(value) as T;
|
||||
} catch (error) {
|
||||
return value;
|
||||
return value as T;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
0
src/s3/s3.module.ts
Normal file
0
src/s3/s3.module.ts
Normal file
0
src/s3/s3.service.ts
Normal file
0
src/s3/s3.service.ts
Normal file
6
src/user/user.constant.ts
Normal file
6
src/user/user.constant.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const USER_NOT_FOUND_ERROR = 'User not found';
|
||||
|
||||
// Caching Constants for Redis
|
||||
|
||||
export const userCacheTTL = 60 * 60 * 24; // 24 hours
|
||||
export const userCacheKey = 'ww-auth:user';
|
13
src/user/user.module.ts
Normal file
13
src/user/user.module.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { User } from '../database/models/user.model';
|
||||
import { RedisModule } from '../redis/redis.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User]), RedisModule],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
exports: [],
|
||||
})
|
||||
export class UserModule {}
|
62
src/user/user.service.ts
Normal file
62
src/user/user.service.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { User } from '../database/models/user.model';
|
||||
import { RedisService } from '../redis/redis.service';
|
||||
import { Span } from 'nestjs-otel';
|
||||
import { USER_NOT_FOUND_ERROR, userCacheKey } from './user.constant';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
private readonly redisService: RedisService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get a user by their ID
|
||||
* @param id The user's ID
|
||||
* @oaram relations The DB relations to include
|
||||
* @returns Promise<User> The user
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
@Span()
|
||||
async getUserById(id: string, relations: string[] = []): Promise<User> {
|
||||
const cacheKey = userCacheKey + id;
|
||||
const cachedUser = await this.redisService.get<User>(cacheKey);
|
||||
|
||||
if (cachedUser && relations.length === 0) {
|
||||
return cachedUser;
|
||||
}
|
||||
|
||||
const relationsToInclude: Record<string, boolean> = relations.reduce(
|
||||
(acc: Record<string, boolean>, relation: string) => {
|
||||
acc[relation] = true;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const queryBuilder = this.userRepository.createQueryBuilder('user');
|
||||
queryBuilder.where('user.id = :id', { id });
|
||||
|
||||
// TODO: Update once org roles are introduced
|
||||
for (const relation in relationsToInclude) {
|
||||
queryBuilder.leftJoinAndSelect(`user.${relation}`, relation);
|
||||
}
|
||||
|
||||
const user = await queryBuilder.getOne();
|
||||
|
||||
if (user && relations.length === 0) {
|
||||
await this.redisService.set(cacheKey, user);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(USER_NOT_FOUND_ERROR);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue