diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 25ae454..718184d 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -153,6 +153,423 @@ ] } }, +<<<<<<< HEAD +======= + "/employees/profile/{email}": { + "get": { + "operationId": "EmployeesController_findOneProfile", + "parameters": [ + { + "name": "email", + "required": true, + "in": "path", + "description": "Identifier of the employee", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Employee profile found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmployeeProfileItemDto" + } + } + } + }, + "400": { + "description": "Employee profile not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find employee profile", + "tags": [ + "Employees" + ] + } + }, + "/timesheets": { + "get": { + "operationId": "TimesheetsController_getPeriodByQuery", + "parameters": [ + { + "name": "year", + "required": true, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "period_no", + "required": true, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "email", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Timesheets" + ] + } + }, + "/timesheets/{email}": { + "get": { + "operationId": "TimesheetsController_getByEmail", + "parameters": [ + { + "name": "email", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Timesheets" + ] + } + }, + "/timesheets/shifts/{email}": { + "post": { + "operationId": "TimesheetsController_createTimesheetShifts", + "parameters": [ + { + "name": "email", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWeekShiftsDto" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Timesheets" + ] + } + }, + "/Expenses/upsert/{email}/{date}": { + "put": { + "operationId": "ExpensesController_upsert_by_date", + "parameters": [ + { + "name": "email", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "date", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpsertExpenseDto" + } + } + } + }, + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Expenses" + ] + } + }, + "/Expenses/list/{email}/{year}/{period_no}": { + "get": { + "operationId": "ExpensesController_findExpenseListByPayPeriodAndEmail", + "parameters": [ + { + "name": "email", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "year", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "period_no", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Expenses" + ] + } + }, + "/shifts/upsert/{email}": { + "put": { + "operationId": "ShiftsController_upsert_by_date", + "parameters": [ + { + "name": "email", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "action", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpsertShiftDto" + } + } + } + }, + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Shifts" + ] + } + }, + "/shifts/delete/{email}/{date}": { + "delete": { + "operationId": "ShiftsController_remove", + "parameters": [ + { + "name": "email", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "date", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpsertShiftDto" + } + } + } + }, + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Shifts" + ] + } + }, + "/shifts/approval/{id}": { + "patch": { + "operationId": "ShiftsController_approve", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Shifts" + ] + } + }, + "/shifts/summary": { + "get": { + "operationId": "ShiftsController_getSummary", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Shifts" + ] + } + }, + "/shifts/export.csv": { + "get": { + "operationId": "ShiftsController_exportCsv", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Shifts" + ] + } + }, +>>>>>>> 88f7c0cb0e3824f6faed91058a7d55a9cca048a7 "/notifications/summary": { "get": { "operationId": "NotificationsController_summary", @@ -181,6 +598,80 @@ ] } }, +<<<<<<< HEAD +======= + "/leave-requests/upsert": { + "post": { + "operationId": "LeaveRequestController_upsertLeaveRequest", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpsertLeaveRequestDto" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Leave Requests" + ] + } + }, + "/auth/v1/login": { + "get": { + "operationId": "AuthController_login", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Auth" + ] + } + }, + "/auth/callback": { + "get": { + "operationId": "AuthController_loginCallback", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Auth" + ] + } + }, + "/auth/me": { + "get": { + "operationId": "AuthController_getProfile", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Auth" + ] + } + }, +>>>>>>> 88f7c0cb0e3824f6faed91058a7d55a9cca048a7 "/oauth-sessions": { "post": { "operationId": "OauthSessionsController_create", @@ -836,6 +1327,29 @@ "first_work_day" ] }, +<<<<<<< HEAD +======= + "EmployeeProfileItemDto": { + "type": "object", + "properties": {} + }, + "CreateWeekShiftsDto": { + "type": "object", + "properties": {} + }, + "UpsertExpenseDto": { + "type": "object", + "properties": {} + }, + "UpsertShiftDto": { + "type": "object", + "properties": {} + }, + "UpsertLeaveRequestDto": { + "type": "object", + "properties": {} + }, +>>>>>>> 88f7c0cb0e3824f6faed91058a7d55a9cca048a7 "CreateOauthSessionDto": { "type": "object", "properties": { diff --git a/src/modules/authentication/controllers/auth.controller.ts b/src/modules/authentication/controllers/auth.controller.ts index d3fbf12..248c4d1 100644 --- a/src/modules/authentication/controllers/auth.controller.ts +++ b/src/modules/authentication/controllers/auth.controller.ts @@ -1,17 +1,26 @@ -import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'; +import { Controller, Get, Req, Res, UnauthorizedException, UseGuards } from '@nestjs/common'; import { OIDCLoginGuard } from '../guards/authentik-auth.guard'; import { Request, Response } from 'express'; @Controller('auth') export class AuthController { - - @UseGuards(OIDCLoginGuard) - @Get('/v1/login') - login() {} - @Get('/callback') - @UseGuards(OIDCLoginGuard) - loginCallback(@Req() req: Request, @Res() res: Response) { - res.redirect('http://localhost:9000/#/login-success'); - } + @UseGuards(OIDCLoginGuard) + @Get('/v1/login') + login() { } + + @Get('/callback') + @UseGuards(OIDCLoginGuard) + loginCallback(@Req() req: Request, @Res() res: Response) { + res.redirect('http://localhost:9000/#/login-success'); + } + + @Get('/me') + getProfile(@Req() req: Request) { + if (!req.user) { + throw new UnauthorizedException('Not logged in'); + } + return req.user; + } + } diff --git a/src/modules/authentication/strategies/authentik.strategy.ts b/src/modules/authentication/strategies/authentik.strategy.ts index 492028a..36115e1 100644 --- a/src/modules/authentication/strategies/authentik.strategy.ts +++ b/src/modules/authentication/strategies/authentik.strategy.ts @@ -3,53 +3,63 @@ import { Strategy as OIDCStrategy, Profile, VerifyCallback } from 'passport-open import { PassportStrategy } from '@nestjs/passport'; import { Injectable } from '@nestjs/common'; import { AuthentikAuthService } from '../services/authentik-auth.service'; +import { ValidationError } from 'class-validator'; export interface AuthentikPayload { - iss: string; // Issuer - sub: string; // Subject (user ID) - aud: string; // Audience (client ID) - exp: number; // Expiration time (Unix) - iat: number; // Issued at time (Unix) - auth_time: number; // Time of authentication (Unix) - acr?: string; // Auth Context Class Reference - amr?: string[]; // Auth Method References (e.g., ['pwd']) - email: string; - email_verified: boolean; - name?: string; - given_name?: string; - preferred_username?: string; - nickname?: string; - groups?: string[]; + iss: string; // Issuer + sub: string; // Subject (user ID) + aud: string; // Audience (client ID) + exp: number; // Expiration time (Unix) + iat: number; // Issued at time (Unix) + auth_time: number; // Time of authentication (Unix) + acr?: string; // Auth Context Class Reference + amr?: string[]; // Auth Method References (e.g., ['pwd']) + email: string; + email_verified: boolean; + name?: string; + given_name?: string; + preferred_username?: string; + nickname?: string; + groups?: string[]; } @Injectable() export class AuthentikStrategy extends PassportStrategy(OIDCStrategy, 'openidconnect', 8) { - constructor(private authentikAuthService: AuthentikAuthService) { - super({ - issuer: process.env.AUTHENTIK_ISSUER || "ISSUER_MISSING", - clientID: process.env.AUTHENTIK_CLIENT_ID || 'CLIENT_ID_MISSING', - clientSecret: process.env.AUTHENTIK_CLIENT_SECRET || 'CLIENT_SECRET_MISSING', - callbackURL: process.env.AUTHENTIK_CALLBACK_URL || 'CALLBACK_URL_MISSING', - authorizationURL: process.env.AUTHENTIK_AUTH_URL || 'AUTH_URL_MISSING', - tokenURL: process.env.AUTHENTIK_TOKEN_URL || 'TOKEN_URL_MISSING', - userInfoURL: process.env.AUTHENTIK_USERINFO_URL || 'USERINFO_URL_MISSING', - scope: ['openid', 'email', 'profile', 'offline_access'], - },); - } - - async validate( - issuer: string, - profile: Profile, - _context: any, - idToken: string, - accessToken: string, - refreshToken: string, - params: any, - cb: VerifyCallback, - ): Promise { - - // saving all info from validate() into NestJS session under 'user' - /* TODO: Will need to add authorization logic with Prisma here later */ - return cb(null, { issuer, ...profile, idToken, accessToken, refreshToken, ...params }); - } + constructor(private authentikAuthService: AuthentikAuthService) { + super({ + issuer: process.env.AUTHENTIK_ISSUER || "ISSUER_MISSING", + clientID: process.env.AUTHENTIK_CLIENT_ID || 'CLIENT_ID_MISSING', + clientSecret: process.env.AUTHENTIK_CLIENT_SECRET || 'CLIENT_SECRET_MISSING', + callbackURL: process.env.AUTHENTIK_CALLBACK_URL || 'CALLBACK_URL_MISSING', + authorizationURL: process.env.AUTHENTIK_AUTH_URL || 'AUTH_URL_MISSING', + tokenURL: process.env.AUTHENTIK_TOKEN_URL || 'TOKEN_URL_MISSING', + userInfoURL: process.env.AUTHENTIK_USERINFO_URL || 'USERINFO_URL_MISSING', + scope: ['openid', 'email', 'profile', 'offline_access'], + },); + } + + async validate( + _issuer: string, + profile: Profile, + _context: any, + _idToken: string, + _accessToken: string, + _refreshToken: string, + _params: any, + cb: VerifyCallback, + ): Promise { + try { + const email = profile.emails?.[0]?.value; + console.log('email: ', email); + if (!email) return cb(new Error('Missing email in OIDC profile'), false); + + const user = await this.authentikAuthService.validateUser(email); + console.log('user: ', user); + if (!user) return cb(new Error('User not found'), false); + + return cb(null, user); + } catch (err) { + return cb(err, false); + } + } } diff --git a/src/modules/employees/controllers/employees.controller.ts b/src/modules/employees/controllers/employees.controller.ts index 2026a28..e647547 100644 --- a/src/modules/employees/controllers/employees.controller.ts +++ b/src/modules/employees/controllers/employees.controller.ts @@ -6,6 +6,7 @@ import { RolesAllowed } from '../../../common/decorators/roles.decorators'; import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { EmployeeListItemDto } from '../dtos/list-employee.dto'; import { EmployeesArchivalService } from '../services/employees-archival.service'; +import { EmployeeProfileItemDto } from 'src/modules/employees/dtos/profil-employee.dto'; @ApiTags('Employees') @ApiBearerAuth('access-token') @@ -76,14 +77,14 @@ export class EmployeesController { // return this.employeesService.findOne(email); // } - // @Get('profile/:email') - // @ApiOperation({summary: 'Find employee profile' }) - // @ApiParam({ name: 'email', type: String, description: 'Identifier of the employee' }) - // @ApiResponse({ status: 200, description: 'Employee profile found', type: EmployeeProfileItemDto }) - // @ApiResponse({ status: 400, description: 'Employee profile not found' }) - // findOneProfile(@Param('email') email: string): Promise { - // return this.employeesService.findOneProfile(email); - // } + @Get('profile/:email') + @ApiOperation({summary: 'Find employee profile' }) + @ApiParam({ name: 'email', type: String, description: 'Identifier of the employee' }) + @ApiResponse({ status: 200, description: 'Employee profile found', type: EmployeeProfileItemDto }) + @ApiResponse({ status: 400, description: 'Employee profile not found' }) + findOneProfile(@Param('email') email: string): Promise { + return this.employeesService.findOneProfile(email); + } // @Delete(':email') // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR ) diff --git a/src/modules/users-management/services/abstract-user.service.ts b/src/modules/users-management/services/abstract-user.service.ts index 51fd719..4e0da2e 100644 --- a/src/modules/users-management/services/abstract-user.service.ts +++ b/src/modules/users-management/services/abstract-user.service.ts @@ -18,12 +18,20 @@ export abstract class AbstractUserService { return user; } - async findOneByEmail( email: string ): Promise { + async findOneByEmail( email: string ): Promise> { const user = await this.prisma.users.findUnique({ where: { email } }); if (!user) { throw new NotFoundException(`No user with email #${email} exists`); } - return user; + + const clean_user = { + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + role: user.role, + } + + return clean_user; } async remove(id: string): Promise {