refactor(auth): add functionality to complete auth cycle, utilizer user from request.
This commit is contained in:
parent
d1c41ea1bd
commit
1dbc0bf6c2
|
|
@ -221,6 +221,46 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/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",
|
||||
|
|
@ -680,6 +720,20 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/auth/me": {
|
||||
"get": {
|
||||
"operationId": "AuthController_getProfile",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Auth"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/oauth-sessions": {
|
||||
"post": {
|
||||
"operationId": "OauthSessionsController_create",
|
||||
|
|
@ -1463,6 +1517,10 @@
|
|||
"first_work_day"
|
||||
]
|
||||
},
|
||||
"EmployeeProfileItemDto": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"CreateWeekShiftsDto": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<any> {
|
||||
|
||||
// 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<any> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<EmployeeProfileItemDto> {
|
||||
// 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<EmployeeProfileItemDto> {
|
||||
return this.employeesService.findOneProfile(email);
|
||||
}
|
||||
|
||||
// @Delete(':email')
|
||||
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR )
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export class ShiftsHelpersService {
|
|||
|
||||
async ensureTimesheet(tx: Tx, employee_id: number, date_only: Date) {
|
||||
const start_of_week = weekStartSunday(date_only);
|
||||
console.log('start of week: ', start_of_week);
|
||||
return tx.timesheets.findUnique({
|
||||
where: { employee_id_start_date: { employee_id, start_date: start_of_week } },
|
||||
select: { id: true },
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
|||
email: string,
|
||||
date_iso: string,
|
||||
dto: UpsertShiftDto,
|
||||
): Promise<{ day: DayShiftResponse[]; }>{
|
||||
){
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const date_only = toDateOnly(date_iso); //converts to Date format
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
|
|
@ -174,6 +174,11 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
|||
const norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift');
|
||||
const bank_code_id = await this.typeResolver.findByType(norm_shift.type);
|
||||
|
||||
console.log('timesheet_id: ', timesheet.id );
|
||||
console.log('date: ', date_only);
|
||||
console.log('bank code id: ', bank_code_id.id);
|
||||
console.log('normalized old shift: ', norm_shift);
|
||||
|
||||
const existing = await this.helpersService.findExactOldShift(tx, {
|
||||
timesheet_id: timesheet.id,
|
||||
date_only,
|
||||
|
|
@ -184,9 +189,7 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
|||
|
||||
await tx.shifts.delete({ where: { id: existing.id } });
|
||||
|
||||
await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only);
|
||||
const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet.id, date_only);
|
||||
return { day: await this.helpersService.mapDay(fresh_shift)};
|
||||
// await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ export function normalizeShiftPayload(payload: {
|
|||
const asLocalDateOn = (input: string): Date => {
|
||||
// HH:mm ?
|
||||
const hm = /^(\d{2}):(\d{2})$/.exec((input ?? '').trim());
|
||||
if (hm) return new Date(year, mo - 1, d, Number(hm[1]), Number(hm[2]), 0, 0);
|
||||
if (hm) return new Date(Date.UTC(1970, 0, 1, Number(hm[1]), Number(hm[2])));
|
||||
const iso = new Date(input);
|
||||
if (isNaN(iso.getTime())) throw new Error(`Invalid time: "${input}"`);
|
||||
return new Date(year, mo - 1, d, iso.getHours(), iso.getMinutes(), iso.getSeconds(), iso.getMilliseconds());
|
||||
return new Date(Date.UTC(1970, 0, 1, iso.getHours(), iso.getMinutes(), iso.getSeconds()));
|
||||
};
|
||||
|
||||
const start_time = asLocalDateOn(payload.start_time);
|
||||
|
|
|
|||
|
|
@ -18,12 +18,20 @@ export abstract class AbstractUserService {
|
|||
return user;
|
||||
}
|
||||
|
||||
async findOneByEmail( email: string ): Promise<Users> {
|
||||
async findOneByEmail( email: string ): Promise<Partial<Users>> {
|
||||
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<Users> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user