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": {
|
"/timesheets": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "TimesheetsController_getPeriodByQuery",
|
"operationId": "TimesheetsController_getPeriodByQuery",
|
||||||
|
|
@ -680,6 +720,20 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/auth/me": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "AuthController_getProfile",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Auth"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/oauth-sessions": {
|
"/oauth-sessions": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "OauthSessionsController_create",
|
"operationId": "OauthSessionsController_create",
|
||||||
|
|
@ -1463,6 +1517,10 @@
|
||||||
"first_work_day"
|
"first_work_day"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"EmployeeProfileItemDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
|
},
|
||||||
"CreateWeekShiftsDto": {
|
"CreateWeekShiftsDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {}
|
"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 { OIDCLoginGuard } from '../guards/authentik-auth.guard';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
|
|
||||||
@UseGuards(OIDCLoginGuard)
|
|
||||||
@Get('/v1/login')
|
|
||||||
login() {}
|
|
||||||
|
|
||||||
@Get('/callback')
|
@UseGuards(OIDCLoginGuard)
|
||||||
@UseGuards(OIDCLoginGuard)
|
@Get('/v1/login')
|
||||||
loginCallback(@Req() req: Request, @Res() res: Response) {
|
login() { }
|
||||||
res.redirect('http://localhost:9000/#/login-success');
|
|
||||||
}
|
@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 { PassportStrategy } from '@nestjs/passport';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { AuthentikAuthService } from '../services/authentik-auth.service';
|
import { AuthentikAuthService } from '../services/authentik-auth.service';
|
||||||
|
import { ValidationError } from 'class-validator';
|
||||||
|
|
||||||
export interface AuthentikPayload {
|
export interface AuthentikPayload {
|
||||||
iss: string; // Issuer
|
iss: string; // Issuer
|
||||||
sub: string; // Subject (user ID)
|
sub: string; // Subject (user ID)
|
||||||
aud: string; // Audience (client ID)
|
aud: string; // Audience (client ID)
|
||||||
exp: number; // Expiration time (Unix)
|
exp: number; // Expiration time (Unix)
|
||||||
iat: number; // Issued at time (Unix)
|
iat: number; // Issued at time (Unix)
|
||||||
auth_time: number; // Time of authentication (Unix)
|
auth_time: number; // Time of authentication (Unix)
|
||||||
acr?: string; // Auth Context Class Reference
|
acr?: string; // Auth Context Class Reference
|
||||||
amr?: string[]; // Auth Method References (e.g., ['pwd'])
|
amr?: string[]; // Auth Method References (e.g., ['pwd'])
|
||||||
email: string;
|
email: string;
|
||||||
email_verified: boolean;
|
email_verified: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
given_name?: string;
|
given_name?: string;
|
||||||
preferred_username?: string;
|
preferred_username?: string;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
groups?: string[];
|
groups?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthentikStrategy extends PassportStrategy(OIDCStrategy, 'openidconnect', 8) {
|
export class AuthentikStrategy extends PassportStrategy(OIDCStrategy, 'openidconnect', 8) {
|
||||||
constructor(private authentikAuthService: AuthentikAuthService) {
|
constructor(private authentikAuthService: AuthentikAuthService) {
|
||||||
super({
|
super({
|
||||||
issuer: process.env.AUTHENTIK_ISSUER || "ISSUER_MISSING",
|
issuer: process.env.AUTHENTIK_ISSUER || "ISSUER_MISSING",
|
||||||
clientID: process.env.AUTHENTIK_CLIENT_ID || 'CLIENT_ID_MISSING',
|
clientID: process.env.AUTHENTIK_CLIENT_ID || 'CLIENT_ID_MISSING',
|
||||||
clientSecret: process.env.AUTHENTIK_CLIENT_SECRET || 'CLIENT_SECRET_MISSING',
|
clientSecret: process.env.AUTHENTIK_CLIENT_SECRET || 'CLIENT_SECRET_MISSING',
|
||||||
callbackURL: process.env.AUTHENTIK_CALLBACK_URL || 'CALLBACK_URL_MISSING',
|
callbackURL: process.env.AUTHENTIK_CALLBACK_URL || 'CALLBACK_URL_MISSING',
|
||||||
authorizationURL: process.env.AUTHENTIK_AUTH_URL || 'AUTH_URL_MISSING',
|
authorizationURL: process.env.AUTHENTIK_AUTH_URL || 'AUTH_URL_MISSING',
|
||||||
tokenURL: process.env.AUTHENTIK_TOKEN_URL || 'TOKEN_URL_MISSING',
|
tokenURL: process.env.AUTHENTIK_TOKEN_URL || 'TOKEN_URL_MISSING',
|
||||||
userInfoURL: process.env.AUTHENTIK_USERINFO_URL || 'USERINFO_URL_MISSING',
|
userInfoURL: process.env.AUTHENTIK_USERINFO_URL || 'USERINFO_URL_MISSING',
|
||||||
scope: ['openid', 'email', 'profile', 'offline_access'],
|
scope: ['openid', 'email', 'profile', 'offline_access'],
|
||||||
},);
|
},);
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(
|
async validate(
|
||||||
issuer: string,
|
_issuer: string,
|
||||||
profile: Profile,
|
profile: Profile,
|
||||||
_context: any,
|
_context: any,
|
||||||
idToken: string,
|
_idToken: string,
|
||||||
accessToken: string,
|
_accessToken: string,
|
||||||
refreshToken: string,
|
_refreshToken: string,
|
||||||
params: any,
|
_params: any,
|
||||||
cb: VerifyCallback,
|
cb: VerifyCallback,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
try {
|
||||||
// saving all info from validate() into NestJS session under 'user'
|
const email = profile.emails?.[0]?.value;
|
||||||
/* TODO: Will need to add authorization logic with Prisma here later */
|
console.log('email: ', email);
|
||||||
return cb(null, { issuer, ...profile, idToken, accessToken, refreshToken, ...params });
|
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 { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { EmployeeListItemDto } from '../dtos/list-employee.dto';
|
import { EmployeeListItemDto } from '../dtos/list-employee.dto';
|
||||||
import { EmployeesArchivalService } from '../services/employees-archival.service';
|
import { EmployeesArchivalService } from '../services/employees-archival.service';
|
||||||
|
import { EmployeeProfileItemDto } from 'src/modules/employees/dtos/profil-employee.dto';
|
||||||
|
|
||||||
@ApiTags('Employees')
|
@ApiTags('Employees')
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
|
|
@ -76,14 +77,14 @@ export class EmployeesController {
|
||||||
// return this.employeesService.findOne(email);
|
// return this.employeesService.findOne(email);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// @Get('profile/:email')
|
@Get('profile/:email')
|
||||||
// @ApiOperation({summary: 'Find employee profile' })
|
@ApiOperation({summary: 'Find employee profile' })
|
||||||
// @ApiParam({ name: 'email', type: String, description: 'Identifier of the employee' })
|
@ApiParam({ name: 'email', type: String, description: 'Identifier of the employee' })
|
||||||
// @ApiResponse({ status: 200, description: 'Employee profile found', type: EmployeeProfileItemDto })
|
@ApiResponse({ status: 200, description: 'Employee profile found', type: EmployeeProfileItemDto })
|
||||||
// @ApiResponse({ status: 400, description: 'Employee profile not found' })
|
@ApiResponse({ status: 400, description: 'Employee profile not found' })
|
||||||
// findOneProfile(@Param('email') email: string): Promise<EmployeeProfileItemDto> {
|
findOneProfile(@Param('email') email: string): Promise<EmployeeProfileItemDto> {
|
||||||
// return this.employeesService.findOneProfile(email);
|
return this.employeesService.findOneProfile(email);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// @Delete(':email')
|
// @Delete(':email')
|
||||||
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR )
|
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR )
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export class ShiftsHelpersService {
|
||||||
|
|
||||||
async ensureTimesheet(tx: Tx, employee_id: number, date_only: Date) {
|
async ensureTimesheet(tx: Tx, employee_id: number, date_only: Date) {
|
||||||
const start_of_week = weekStartSunday(date_only);
|
const start_of_week = weekStartSunday(date_only);
|
||||||
|
console.log('start of week: ', start_of_week);
|
||||||
return tx.timesheets.findUnique({
|
return tx.timesheets.findUnique({
|
||||||
where: { employee_id_start_date: { employee_id, start_date: start_of_week } },
|
where: { employee_id_start_date: { employee_id, start_date: start_of_week } },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
||||||
email: string,
|
email: string,
|
||||||
date_iso: string,
|
date_iso: string,
|
||||||
dto: UpsertShiftDto,
|
dto: UpsertShiftDto,
|
||||||
): Promise<{ day: DayShiftResponse[]; }>{
|
){
|
||||||
return this.prisma.$transaction(async (tx) => {
|
return this.prisma.$transaction(async (tx) => {
|
||||||
const date_only = toDateOnly(date_iso); //converts to Date format
|
const date_only = toDateOnly(date_iso); //converts to Date format
|
||||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
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 norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift');
|
||||||
const bank_code_id = await this.typeResolver.findByType(norm_shift.type);
|
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, {
|
const existing = await this.helpersService.findExactOldShift(tx, {
|
||||||
timesheet_id: timesheet.id,
|
timesheet_id: timesheet.id,
|
||||||
date_only,
|
date_only,
|
||||||
|
|
@ -184,9 +189,7 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
||||||
|
|
||||||
await tx.shifts.delete({ where: { id: existing.id } });
|
await tx.shifts.delete({ where: { id: existing.id } });
|
||||||
|
|
||||||
await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only);
|
// 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)};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,10 @@ export function normalizeShiftPayload(payload: {
|
||||||
const asLocalDateOn = (input: string): Date => {
|
const asLocalDateOn = (input: string): Date => {
|
||||||
// HH:mm ?
|
// HH:mm ?
|
||||||
const hm = /^(\d{2}):(\d{2})$/.exec((input ?? '').trim());
|
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);
|
const iso = new Date(input);
|
||||||
if (isNaN(iso.getTime())) throw new Error(`Invalid time: "${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);
|
const start_time = asLocalDateOn(payload.start_time);
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,20 @@ export abstract class AbstractUserService {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByEmail( email: string ): Promise<Users> {
|
async findOneByEmail( email: string ): Promise<Partial<Users>> {
|
||||||
const user = await this.prisma.users.findUnique({ where: { email } });
|
const user = await this.prisma.users.findUnique({ where: { email } });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException(`No user with email #${email} exists`);
|
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> {
|
async remove(id: string): Promise<Users> {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user