diff --git a/src/auth/middleware/auth.middleware.ts b/src/auth/middleware/auth.middleware.ts index 11b1c33..1226adc 100644 --- a/src/auth/middleware/auth.middleware.ts +++ b/src/auth/middleware/auth.middleware.ts @@ -15,6 +15,7 @@ export class AuthMiddleware implements NestMiddleware { const { session, user } = await this.validateSession(req, res); // set the session and user data in the CLS + delete user.password; this.clsService.set('authType', 'session'); this.clsService.set('user', user); this.clsService.set('session', session); diff --git a/src/auth/oidc/adapter.ts b/src/auth/oidc/adapter.ts index 0f58fce..0ade65e 100644 --- a/src/auth/oidc/adapter.ts +++ b/src/auth/oidc/adapter.ts @@ -567,7 +567,7 @@ export const createOidcAdapter: (db: DataSource, redis: RedisService, baseUrl: s */ async genericFind(id: string): Promise { const key = this.key(id); - const data = await redis.jsonGet(key); + const data = await redis.jsonGet(key); if (!data) { return undefined; diff --git a/src/main.ts b/src/main.ts index cfde4aa..1f726ef 100644 --- a/src/main.ts +++ b/src/main.ts @@ -44,9 +44,9 @@ async function bootstrap() { .setDescription('An OpenSource Identity Provider written by Waterwolf') .setVersion('1.0') .addTag('Authentication', 'Initial login and registration') - .addTag('Client') - .addTag('Organization') .addTag('User') + .addTag('Organization') + .addTag('Client') .build(); const document = SwaggerModule.createDocument(app, config); diff --git a/src/redis/redis.module.ts b/src/redis/redis.module.ts index 3843a78..47705b9 100644 --- a/src/redis/redis.module.ts +++ b/src/redis/redis.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { RedisService } from './service/redis.service'; +import { SearchService } from './service/search.service'; @Module({ - providers: [RedisService, ConfigService], - exports: [RedisService], + providers: [RedisService, SearchService, ConfigService], + exports: [RedisService, SearchService], }) export class RedisModule {} diff --git a/src/redis/service/redis.service.ts b/src/redis/service/redis.service.ts index a8db0fc..cb2c5bb 100644 --- a/src/redis/service/redis.service.ts +++ b/src/redis/service/redis.service.ts @@ -174,7 +174,7 @@ export class RedisService implements OnApplicationShutdown { * @param key Key for the value to get * @returns Promise */ - public async jsonGet(key: string): Promise { + public async jsonGet(key: string): Promise { const value = (await this._ioredis.call('JSON.GET', key)) as string | null; if (!value) { return null; @@ -193,11 +193,11 @@ export class RedisService implements OnApplicationShutdown { * @param value Value to set * @returns Promise */ - public async jsonSet(key: string, value: string | object): Promise { + public async jsonSet(key: string, value: string | object, path = '$'): Promise { if (typeof value === 'object') { value = JSON.stringify(value); } - await this._ioredis.set(key, value); + await this._ioredis.call('JSON.SET', key, path, value); } /** diff --git a/src/redis/service/search.service.ts b/src/redis/service/search.service.ts new file mode 100644 index 0000000..dbba7e8 --- /dev/null +++ b/src/redis/service/search.service.ts @@ -0,0 +1,108 @@ +import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; + +import { RedisService } from './redis.service'; +import { userCacheKey } from '../../user/user.constant'; + +@Injectable() +export class SearchService implements OnApplicationBootstrap { + private readonly logger = new Logger(SearchService.name); + constructor(private readonly redisService: RedisService) {} + async onApplicationBootstrap() { + this.logger.debug('Checking Index in Redis Serch'); + + if (!(await this.checkIndexExists('idx:users'))) { + this.logger.log('Created users redis index'); + this.createIndex( + 'idx:users', + userCacheKey + ':', + '$.id', + 'AS', + 'id', + 'TEXT', + 'WEIGHT', + '5', + '$.email', + 'AS', + 'email', + 'TEXT', + '$.username', + 'AS', + 'username', + 'TEXT', + '$.displayName', + 'AS', + 'displayName', + 'TEXT', + ); + } + } + + public async createIndex(indexName: string, keyPrefix: string, ...schema: string[]) { + this.redisService.ioredis.call( + 'FT.CREATE', + indexName, + 'ON', + 'JSON', + 'PREFIX', + '1', + keyPrefix, + 'SCHEMA', + ...schema, + ); + } + + /** + * Checks if an Index Exists + * @param indexName The index name to check if it exists + * @returns boolean + */ + public async checkIndexExists(indexName: string): Promise { + try { + await this.redisService.ioredis.call('FT.INFO', indexName); + return true; + } catch (e) { + if (e.message === 'Unknown Index name') return false; + + throw e; + } + } + + public async search(index: string, searchQuery: string, field?: string) { + let query: string = ''; + + searchQuery = searchQuery.replace(/[.@\\]/g, '\\$&'); + + if (field) { + query = `@${field}:(${searchQuery})`; + } + + query = searchQuery; + + this.logger.debug(`Searching index ${index} for ${query}`); + + const redisSearch = (await this.redisService.ioredis.call('FT.SEARCH', index, query)) as any; + + if (redisSearch[0] === 0) { + return []; + } + + delete redisSearch[2][0]; + + const redisSearchResults = redisSearch[2] + .filter((result) => result !== null) + .map((result) => { + // try to parse the result, if fails, skip + try { + return JSON.parse(result); + } catch (e) { + this.logger.error('Failed to parse search result', e); + return null; + } + }); + + return { + totalResults: redisSearch[0], + results: redisSearchResults, + }; + } +} diff --git a/src/user/controller/user.controller.ts b/src/user/controller/user.controller.ts index bffde3e..5aaa984 100644 --- a/src/user/controller/user.controller.ts +++ b/src/user/controller/user.controller.ts @@ -1,11 +1,23 @@ -import { Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; +import { ApiParam, ApiTags } from '@nestjs/swagger'; import { UserService } from '../service/user.service'; +import { RedisService } from '../../redis/service/redis.service'; +import { SearchService } from '../../redis/service/search.service'; @Controller('user') @ApiTags('User') export class UserController { - constructor(private readonly userService: UserService) {} + constructor( + private readonly userService: UserService, + private readonly redisService: RedisService, + private readonly searchService: SearchService, + ) {} + + @Get('/search') + @ApiParam({ name: 'field', required: false }) + public async search(@Query('query') query: string, @Query('field') field?: string) { + return await this.searchService.search('idx:users', query, field); + } @Get() // Admin: Paginated list of users. @@ -14,11 +26,15 @@ export class UserController { } @Get(':id') + // Authenticated : Has to be publicly seen due to org inviting. Filter amount of data unless your an admin. + // Allow self reflection to get all user data. @me public async getUser(@Param('id') id: string): Promise { return await this.userService.getUserById(id); } @Delete(':id') + // Admin: Marks a user for deletion + // Allow self reflection to allow user to delete their own profile. public async deleteUser(@Param('id') id: string): Promise { return await this.userService.deleteUser(id); } diff --git a/src/user/service/user.service.ts b/src/user/service/user.service.ts index df45a76..f004971 100644 --- a/src/user/service/user.service.ts +++ b/src/user/service/user.service.ts @@ -18,7 +18,6 @@ import { INVALID_CREDENTIALS_ERROR, USER_NOT_FOUND_ERROR, userCacheKeyGenerate, - userCacheTTL, } from '../user.constant'; import { ClsService } from 'nestjs-cls'; @@ -45,7 +44,7 @@ export class UserService { return this.clsService.get('user'); } } - const cachedUser = await this.redisService.get(userCacheKeyGenerate(id)); + const cachedUser = await this.redisService.jsonGet(userCacheKeyGenerate(id)); if (cachedUser && relations.length === 0) { return cachedUser; } @@ -77,7 +76,7 @@ export class UserService { } if (relations.length === 0) { - await this.redisService.set(userCacheKeyGenerate(id), user, userCacheTTL); + await this.redisService.jsonSet(userCacheKeyGenerate(id), user); } return user; diff --git a/src/user/user.module.ts b/src/user/user.module.ts index 4b97b59..f6d1704 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -4,10 +4,11 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { RedisModule } from '../redis/redis.module'; import { UserService } from './service/user.service'; import { DATABASE_ENTITIES } from '../database/database.entities'; +import { UserController } from './controller/user.controller'; @Module({ imports: [TypeOrmModule.forFeature(DATABASE_ENTITIES), RedisModule], - controllers: [], + controllers: [UserController], providers: [UserService], exports: [UserService], }) diff --git a/src/view/controllers/view.controller.ts b/src/view/controllers/view.controller.ts index 2770c96..c5e30df 100644 --- a/src/view/controllers/view.controller.ts +++ b/src/view/controllers/view.controller.ts @@ -3,6 +3,7 @@ import { ApiExcludeController, ApiExcludeEndpoint } from '@nestjs/swagger'; import { LoginGuard } from '../../auth/guard/login.guard'; import { User } from '../../auth/decorators/user.decorator'; +import { User as UserObject } from '../../database/models/user.model'; @ApiExcludeController() @Controller() @@ -51,9 +52,16 @@ export class ViewController { }; } - @Get('auth/auth-test') + @Get('home') + @Render('home/index') @ApiExcludeEndpoint() - public async getAuthTest(@User() user: any): Promise { - return user; + public async getHomeView(@User() user: UserObject): Promise { + return { + user: { + name: user.displayName ?? user.username, + avatar: user.avatar, + email: user.email, + }, + }; } } diff --git a/views/home/index.hbs b/views/home/index.hbs index dd8c015..e67ce2d 100644 --- a/views/home/index.hbs +++ b/views/home/index.hbs @@ -1,10 +1,57 @@ - - - - App - - - {{ message }} - + + + + + + Profile Management + + + + + +
+

Profile Management

+ +
+
+ User Avatar +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ +
+
+
+ + + \ No newline at end of file