refactor(auth): add auth controller, but almost certain that implementation is shaky at best. Will rework structure after further learning NestJS architecture

This commit is contained in:
Nicolas Drolet 2025-07-22 16:41:13 -04:00
parent ab8587e769
commit 5e644b6a6c
7 changed files with 108 additions and 32 deletions

29
package-lock.json generated
View File

@ -2438,13 +2438,14 @@
}
},
"node_modules/@nestjs/platform-express": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.3.tgz",
"integrity": "sha512-hEDNMlaPiBO72fxxX/CuRQL3MEhKRc/sIYGVoXjrnw6hTxZdezvvM6A95UaLsYknfmcZZa/CdG1SMBZOu9agHQ==",
"version": "11.1.5",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.5.tgz",
"integrity": "sha512-OsoiUBY9Shs5IG3uvDIt9/IDfY5OlvWBESuB/K4Eun8xILw1EK5d5qMfC3d2sIJ+kA3l+kBR1d/RuzH7VprLIg==",
"license": "MIT",
"dependencies": {
"cors": "2.8.5",
"express": "5.1.0",
"multer": "2.0.1",
"multer": "2.0.2",
"path-to-regexp": "8.2.0",
"tslib": "2.8.1"
},
@ -4628,7 +4629,8 @@
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/arch": {
"version": "3.0.0",
@ -5428,6 +5430,7 @@
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
@ -8557,6 +8560,7 @@
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
@ -8570,9 +8574,10 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/multer": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz",
"integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
"integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
@ -8590,6 +8595,7 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@ -8598,6 +8604,7 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@ -8606,6 +8613,7 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
@ -8617,6 +8625,7 @@
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
@ -10817,7 +10826,8 @@
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.8.3",
@ -11355,6 +11365,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}

View File

@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthentikAuthService } from './services/authentik-auth.service';
@Module({
imports: [
@ -10,5 +11,8 @@ import { PassportModule } from '@nestjs/passport';
signOptions: { expiresIn: '1h' },
}),
],
providers: [ AuthentikAuthService, ],
exports: [ AuthentikAuthService ],
})
export class AuthenticationModule {}

View File

@ -0,0 +1,19 @@
import { Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import { Request } from 'express';
@Controller('auth')
export class AuthController {
@Post('/login')
login() {
// Passport handles redirection to Authentik
}
@Get('callback')
// @UseGuards(AuthGuard('openidconnect'))
callback(@Req() req: Request) {
console.log('✅ Auth complete, here is req.user:');
console.dir(req.user, { depth: null });
return req.user;
}
}

View File

@ -0,0 +1,14 @@
import { Injectable } from '@nestjs/common';
import { UsersService } from 'src/modules/users-management/services/users.service';
@Injectable()
export class AuthentikAuthService {
constructor(private usersService: UsersService) {}
async validateUser(user_email: string): Promise<any> {
const user = await this.usersService.findOneByEmail(user_email);
return user;
}
}

View File

@ -11,7 +11,7 @@ export class AuthService {
) {}
async validateUser(user_id: UUID): Promise<any> {
const user = await this.usersService.findOne(user_id);
const user = await this.usersService.findOne( user_id );
if (user) {
return user;
}

View File

@ -1,28 +1,48 @@
import { Injectable } from '@nestjs/common';
import { Strategy as OIDCStrategy } from 'passport-openidconnect';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-openidconnect';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthentikAuthService } from '../services/authentik-auth.service';
export interface AuthentikPayload {
//TODO: check Authentik payload contents
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(Strategy) {
constructor() {
export class AuthentikStrategy extends PassportStrategy(OIDCStrategy) {
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",
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_ISSUER}/authorize/`,
tokenURL: `${process.env.AUTHENTIK_ISSUER}/token/`,
userInfoURL: `${process.env.AUTHENTIK_ISSUER}/userinfo/`,
scope: [],
})
scope: ['openid', 'email', 'profile'],
});
}
async validate(): Promise<any> {
// Check what kind of payload we're actually handling here
return null;
async validate(payload: AuthentikPayload): Promise<any> {
// if ( !payload.email_verified ) { throw new UnauthorizedException(); }
const user = await this.authentikAuthService.validateUser(payload.email);
if ( !user ) { throw new UnauthorizedException(); }
return user;
}
}

View File

@ -10,7 +10,7 @@ export abstract class AbstractUserService {
return this.prisma.users.findMany();
}
async findOne(id: string): Promise<Users> {
async findOne( id: string ): Promise<Users> {
const user = await this.prisma.users.findUnique({ where: { id } });
if (!user) {
throw new NotFoundException(`User #${id} not found`);
@ -18,6 +18,14 @@ export abstract class AbstractUserService {
return user;
}
async findOneByEmail( email: string ): Promise<Users> {
const user = await this.prisma.users.findUnique({ where: { email } });
if (!user) {
throw new NotFoundException(`No user with email #${email} exists`);
}
return user;
}
async remove(id: string): Promise<Users> {
await this.findOne(id);
return this.prisma.users.delete({ where: { id } });