use ports package and add more tests

This commit is contained in:
admin 2025-08-24 00:08:01 +02:00
parent 0099302033
commit acdbc9d402
33 changed files with 126 additions and 305 deletions

14
package-lock.json generated
View File

@ -9,7 +9,9 @@
"version": "1.0.5",
"license": "MIT",
"dependencies": {
"@apihub24/authentication": "^1.0.1",
"@apihub24/repository": "^1.0.3",
"@apihub24/translation": "^1.0.1",
"@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.6",
@ -39,12 +41,24 @@
"node": ">=6.0.0"
}
},
"node_modules/@apihub24/authentication": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@apihub24/authentication/-/authentication-1.0.1.tgz",
"integrity": "sha512-nWw75ofQKHxE0dI7PzvNBQNcQrX/HSrzuAJTYNu42BoCROba1NUz8QAodTn5+3dIeQEzw127gtSb6D7yW0B8Jg==",
"license": "MIT"
},
"node_modules/@apihub24/repository": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@apihub24/repository/-/repository-1.0.3.tgz",
"integrity": "sha512-m2twcVPrdnKAcnNQFabGzQ/18/kQUEtuqAuSzVBTEc3mxBKBQ5ex1+Cx4JP/sZ1HqdS4GisFXDa8zfrnpdcLaA==",
"license": "MIT"
},
"node_modules/@apihub24/translation": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@apihub24/translation/-/translation-1.0.1.tgz",
"integrity": "sha512-LebjTK9vkT2DCzQLyi0/t/LaOq0NLBWuqwGClddjnvMweBb05E9Y3S84rl4yoBdG6L9kEtsXd/LmXjgeOjtpgg==",
"license": "MIT"
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",

View File

@ -15,7 +15,9 @@
"@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.6",
"@apihub24/authentication": "^1.0.1",
"@apihub24/repository": "^1.0.3",
"@apihub24/translation": "^1.0.1",
"class-validator": "^0.14.2",
"class-transformer": "^0.5.1"
},

View File

@ -1,32 +0,0 @@
import { IGroup } from "./group";
/**
* represent the Users Account
*/
export interface IAccount {
id: string;
/**
* the Accounts name
*/
accountName: string;
/**
* the hashed Password
*/
passwordHash: string;
/**
* a E-Mail Address for this Account
*/
email: string;
/**
* represents if the E-Mail Address was verified
*/
emailVerified: boolean;
/**
* represents if the User was activated for Login
*/
active: boolean;
/**
* the Groups this User was member
*/
groups: IGroup[];
}

View File

@ -1,21 +0,0 @@
import { IOrganization } from "./organization";
import { IRight } from "./right";
/**
* represents a Group that a User can Join
*/
export interface IGroup {
id: string;
/**
* the Name of the Group
*/
name: string;
/**
* the Organization in that the Group are
*/
organization: IOrganization;
/**
* the Rights that have the Group
*/
rights: IRight[];
}

View File

@ -1,10 +0,0 @@
/**
* represent a Organization Unit
*/
export interface IOrganization {
id: string;
/**
* the Name of the Organization
*/
name: string;
}

View File

@ -1,25 +0,0 @@
/**
* represents Registration Data
*/
export interface IRegistration {
/**
* the Account Name to register
*/
accountName: string;
/**
* the Password to register
*/
password: string;
/**
* the repeated Password
*/
passwordComparer: string;
/**
* the E-Mail Address to register
*/
email: string;
/**
* the Group Ids the Account must be Part
*/
groupIds: string[];
}

View File

@ -1,10 +0,0 @@
/**
* represent a Application Right
*/
export interface IRight {
id: string;
/**
* the Right Name
*/
name: string;
}

View File

@ -1,16 +0,0 @@
import { IAccount } from "./account";
/**
* represents a Session
*/
export interface ISession {
id: string;
/**
* the Account of the Session
*/
account: IAccount;
/**
* some MetaData
*/
metaData: Record<string, any>;
}

View File

@ -1,13 +0,0 @@
/**
* represent a Sign In Request
*/
export interface ISignIn {
/**
* the Account Name to sign in
*/
accountName: string;
/**
* the Password to sign in
*/
password: string;
}

View File

@ -1,25 +0,0 @@
import { IAccount } from "../models/account";
/**
* @interface IMailVerificationService
* @description Defines the contract for a service that handles email verification,
* including sending verification emails and validating verification codes.
*/
export interface IMailVerificationService {
/**
* Sends a verification email to a specified account.
* The implementation should generate a unique verification code and send it to the account's email address.
* @param account The `IAccount` object to which the verification email should be sent.
* @returns A promise that resolves to `void` upon successful sending of the email.
*/
sendVerificationMail(account: IAccount): Promise<void>;
/**
* Verifies a given code against a user's email address.
* This method checks if the provided code matches the one sent to the specified email address.
* @param email The email address to verify.
* @param code The verification code submitted by the user.
* @returns A promise that resolves to `true` if the code is valid, otherwise `false`.
*/
verify(email: string, code: string): Promise<boolean>;
}

View File

@ -1,24 +0,0 @@
/**
* @interface IPasswordService
* @description Defines the contract for a service that handles secure password operations,
* including hashing and verification.
*/
export interface IPasswordService {
/**
* Hashes a plain-text password to a secure, one-way hash.
* This method should be used to store passwords securely in a database.
* @param plainTextPassword The password string in plain text to be hashed.
* @returns A promise that resolves to the hashed password string.
*/
hash(plainTextPassword: string): Promise<string>;
/**
* Verifies a plain-text password against a stored password hash.
* This method is crucial for user authentication, ensuring the entered password
* matches the stored hash without needing to store the plain-text version.
* @param plainTextPassword The password string in plain text provided by the user.
* @param passwordHash The stored, hashed password string to compare against.
* @returns A promise that resolves to `true` if the plain-text password matches the hash, otherwise `false`.
*/
verify(plainTextPassword: string, passwordHash: string): Promise<boolean>;
}

View File

@ -1,37 +0,0 @@
import { IAccount } from "../models/account";
import { ISession } from "../models/session";
/**
* @interface ISessionService
* @description Defines the contract for a service that manages user sessions.
* It provides methods for creating, retrieving, and removing sessions.
*/
export interface ISessionService {
/**
* Creates a new session for a given account.
* @param account The `IAccount` object for which to create the session.
* @returns A promise that resolves to the newly created `ISession` object.
*/
create(account: IAccount): Promise<ISession>;
/**
* Retrieves sessions that match a given filter.
* @param filter A function that takes an `IAccount` object and returns a boolean, serving as the condition for selecting sessions.
* @returns A promise that resolves to an array of `ISession` objects.
*/
getBy(filter: (account: IAccount) => boolean): Promise<ISession[]>;
/**
* Retrieves a single session by its unique identifier.
* @param sessionId The ID of the session to retrieve.
* @returns A promise that resolves to the `ISession` object, or `null` if no session is found.
*/
getById(sessionId: string): Promise<ISession | null>;
/**
* Removes a session based on its unique identifier.
* @param sessionId The ID of the session to remove.
* @returns A promise that resolves to `void` after the session has been removed.
*/
remove(sessionId: string): Promise<void>;
}

View File

@ -1,40 +0,0 @@
import { IAccount } from "../models/account";
import { ISession } from "../models/session";
export type Algorithm = "HS256" | "HS384" | "HS512";
/**
* @interface ITokenService
* @description Defines the contract for a service that handles token-related operations,
* such as generation, validation, and extraction of account information from tokens.
*/
export interface ITokenService {
/**
* Generates a new JWT token for a given session and subject.
* @param session The session object containing data to be included in the token payload.
* @param subject The subject of the token, typically the account name or ID.
* @param expires Optional string specifying the token's expiration duration (e.g., '1h', '30m').
* @param algorithm Optional string specifying the signing algorithm (e.g., 'HS256', 'RS512').
* @returns A promise that resolves to the generated token string.
*/
generate(
session: ISession,
subject: string,
expires?: string,
algorithm?: Algorithm
): Promise<string>;
/**
* Validates a given token to ensure it is authentic and not expired.
* @param token The token string to be validated.
* @returns A promise that resolves to `true` if the token is valid, otherwise `false`.
*/
validate(token: string): Promise<boolean>;
/**
* Extracts and retrieves the account associated with a given token.
* @param token The token string from which to extract account information.
* @returns A promise that resolves to the `IAccount` object or `null` if the token is invalid or no account is found.
*/
getAccount(token: string): Promise<IAccount | null>;
}

View File

@ -7,8 +7,8 @@ import {
import { Reflector } from "@nestjs/core";
import { RIGHTS_KEY } from "./rights.decorator";
import { ROLES_KEY } from "./roles.decorator";
import * as tokenService from "../contracts/services/token.service";
import { ORGANIZATIONS_KEY } from "./organization.decorator";
import * as authentication from "@apihub24/authentication";
/**
* The `TokenGuard` is a NestJS guard that checks for user authorization based on a JWT token.
@ -24,7 +24,7 @@ export class TokenGuard implements CanActivate {
constructor(
private reflector: Reflector,
@Inject("@apihub24/token_service")
private readonly tokenService: tokenService.ITokenService
private readonly tokenService: authentication.ITokenService
) {}
/**

View File

@ -1,16 +1,3 @@
export * from "./contracts/models/account";
export * from "./contracts/models/group";
export * from "./contracts/models/organization";
export * from "./contracts/models/registration";
export * from "./contracts/models/right";
export * from "./contracts/models/session";
export * from "./contracts/models/sign.in";
export * from "./contracts/services/mail.verification.service";
export * from "./contracts/services/password.service";
export * from "./contracts/services/session.service";
export * from "./contracts/services/token.service";
export * from "./guards/organization.decorator";
export * from "./guards/rights.decorator";
export * from "./guards/roles.decorator";

View File

@ -1,4 +1,4 @@
import { IAccount } from "../contracts/models/account";
import { IAccount } from "@apihub24/authentication";
import { Group } from "./group";
import {
IsEmail,

View File

@ -1,4 +1,4 @@
import { IGroup } from "../contracts/models/group";
import { IGroup } from "@apihub24/authentication";
import { Organization } from "./organization";
import { Right } from "./right";
import { IsNotEmpty, MinLength, ValidateNested } from "class-validator";

View File

@ -1,5 +1,5 @@
import { IOrganization } from "@apihub24/authentication";
import { IsNotEmpty, MinLength } from "class-validator";
import { IOrganization } from "../contracts/models/organization";
/**
* represent a Organization Unit

View File

@ -1,5 +1,5 @@
import { IRegistration } from "@apihub24/authentication";
import { IsEmail, IsNotEmpty, MinLength } from "class-validator";
import { IRegistration } from "../contracts/models/registration";
/**
* represents Registration Data

View File

@ -1,5 +1,5 @@
import { IRight } from "@apihub24/authentication";
import { MinLength } from "class-validator";
import { IRight } from "../contracts/models/right";
/**
* represent a Application Right

View File

@ -1,6 +1,6 @@
import { IsNotEmpty, ValidateNested } from "class-validator";
import { Account } from "./account";
import { ISession } from "../contracts/models/session";
import { ISession } from "@apihub24/authentication";
/**
* represents a Session

View File

@ -1,5 +1,5 @@
import { ISignIn } from "@apihub24/authentication";
import { MinLength } from "class-validator";
import { ISignIn } from "../contracts/models/sign.in";
/**
* represent a Sign In Request

View File

@ -0,0 +1,76 @@
import { Test, TestingModule } from "@nestjs/testing";
import {
AccountFactoryService,
GroupService,
TokenAuthenticationModule,
} from "../";
import {
APIHUB24_PASSWORD_SERVICE,
IRegistration,
} from "@apihub24/authentication";
import { MailServiceMockModule } from "../../test/mail.verification.service.mock";
import { PasswordServiceMockModule } from "../../test/password.service.mock";
import { SessionServiceMockModule } from "../../test/session.service.mock";
import { TokenServiceMockModule } from "../../test/token.service.mock";
import { AccountRepositoryMockModule } from "../../test/account.repository.mock";
import { GroupRepositoryMockModule } from "../../test/group.repository.mock";
import { RightRepositoryMockModule } from "../../test/right.repository.mock";
import { OrganizationRepositoryMockModule } from "../../test/organization.repository.mock";
import { Group } from "../models/group";
import { Organization } from "../models/organization";
describe("AccountFactoryService Tests", () => {
let module: TestingModule | null = null;
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
MailServiceMockModule.forRoot(),
PasswordServiceMockModule.forRoot(),
SessionServiceMockModule.forRoot(),
TokenServiceMockModule.forRoot(),
AccountRepositoryMockModule.forRoot(),
GroupRepositoryMockModule.forRoot(),
RightRepositoryMockModule.forRoot(),
OrganizationRepositoryMockModule.forRoot(),
TokenAuthenticationModule.forRoot(),
],
}).compile();
});
it("should create Account from Registration", async () => {
expect(module).toBeDefined();
const service = module?.get(AccountFactoryService);
const groupService = module?.get(GroupService);
const passwordService = module?.get(APIHUB24_PASSWORD_SERVICE);
expect(service).toBeDefined();
expect(groupService).toBeDefined();
expect(passwordService).toBeDefined();
const adminGroup = new Group();
adminGroup.id = "1";
adminGroup.name = "admin";
adminGroup.organization = new Organization();
adminGroup.organization.id = "1";
adminGroup.organization.name = "root";
adminGroup.rights = [];
const registration: IRegistration = {
accountName: "test",
email: "test@example.de",
password: "123",
passwordComparer: "123",
groupIds: ["1"],
};
jest
.spyOn(groupService!, "getBy")
.mockReturnValue(Promise.resolve([adminGroup]));
jest.spyOn(passwordService, "hash").mockReturnValue(registration.password);
const user = await service?.createFromRegistration(registration);
expect(user).toBeDefined();
expect(user?.accountName).toBe(registration.accountName);
expect(user?.email).toBe(registration.email);
expect(user?.passwordHash).toBe(registration.password);
expect(user?.emailVerified).toBeFalsy();
expect(user?.active).toBeFalsy();
});
});

View File

@ -1,18 +1,16 @@
import { Inject, Injectable } from "@nestjs/common";
import { validate } from "class-validator";
import { GroupService } from "./group.service";
import * as passwordService from "../contracts/services/password.service";
import { IRegistration } from "../contracts/models/registration";
import { IAccount } from "../contracts/models/account";
import { Account } from "../models/account";
import { plainToClass } from "class-transformer";
import { Registration } from "../models/registration";
import * as authentication from "@apihub24/authentication";
@Injectable()
export class AccountFactoryService {
constructor(
@Inject("@apihub24/password_service")
private readonly passwordService: passwordService.IPasswordService,
@Inject(authentication.APIHUB24_PASSWORD_SERVICE)
private readonly passwordService: authentication.IPasswordService,
@Inject(GroupService)
private readonly groupService: GroupService
) {}
@ -22,7 +20,9 @@ export class AccountFactoryService {
* @param registration a Registration
* @returns a new User Account
*/
async createFromRegistration(registration: IRegistration): Promise<IAccount> {
async createFromRegistration(
registration: authentication.IRegistration
): Promise<authentication.IAccount> {
let validationErrors = await validate(
plainToClass(Registration, registration)
);
@ -41,7 +41,7 @@ export class AccountFactoryService {
);
account.active = false;
account.groups = groups;
validationErrors = await validate(account);
validationErrors = await validate(plainToClass(Account, account));
if (validationErrors?.length) {
throw new Error(validationErrors[0].toString());
}

View File

@ -1,7 +1,6 @@
import { IAccount, IGroup } from "@apihub24/authentication";
import * as repository from "@apihub24/repository";
import { Inject, Injectable } from "@nestjs/common";
import { IAccount } from "../contracts/models/account";
import { IGroup } from "../contracts/models/group";
@Injectable()
export class AccountService {

View File

@ -2,8 +2,8 @@ import { Inject, Injectable } from "@nestjs/common";
import { validate } from "class-validator";
import { OrganizationService } from "./organization.service";
import { RightService } from "./right.service";
import { IGroup } from "../contracts/models/group";
import { Group } from "../models/group";
import { IGroup } from "@apihub24/authentication";
@Injectable()
export class GroupFactoryService {

View File

@ -1,7 +1,6 @@
import { Inject, Injectable } from "@nestjs/common";
import * as repository from "@apihub24/repository";
import { IGroup } from "../contracts/models/group";
import { IRight } from "../contracts/models/right";
import { IGroup, IRight } from "@apihub24/authentication";
@Injectable()
export class GroupService {

View File

@ -1,19 +1,16 @@
import { Inject, Injectable } from "@nestjs/common";
import { AccountService } from "./account.service";
import * as tokenService from "../contracts/services/token.service";
import * as passwordService from "../contracts/services/password.service";
import * as sessionService from "../contracts/services/session.service";
import { ISignIn } from "../contracts/models/sign.in";
import * as authentication from "@apihub24/authentication";
@Injectable()
export class LoginService {
constructor(
@Inject("@apihub24/token_service")
private readonly tokenService: tokenService.ITokenService,
private readonly tokenService: authentication.ITokenService,
@Inject("@apihub24/password_service")
private readonly passwordService: passwordService.IPasswordService,
private readonly passwordService: authentication.IPasswordService,
@Inject("@apihub24/session_service")
private readonly sessionService: sessionService.ISessionService,
private readonly sessionService: authentication.ISessionService,
@Inject(AccountService)
private readonly accountService: AccountService
) {}
@ -27,9 +24,9 @@ export class LoginService {
* @returns A Promise that resolves to a JWT token string. Returns an empty string if authentication fails.
*/
async signIn(
signIn: ISignIn,
signIn: authentication.ISignIn,
expires: string = "1h",
algorithm: tokenService.Algorithm = "HS512"
algorithm: authentication.Algorithm = "HS512"
): Promise<string> {
const accounts = await this.accountService.getBy(
(x) =>

View File

@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common";
import { IOrganization } from "../contracts/models/organization";
import { Organization } from "../models/organization";
import { IOrganization } from "@apihub24/authentication";
@Injectable()
export class OrganizationFactoryService {

View File

@ -1,6 +1,6 @@
import { Inject, Injectable } from "@nestjs/common";
import * as repository from "@apihub24/repository";
import { IOrganization } from "src/contracts/models/organization";
import { IOrganization } from "@apihub24/authentication";
@Injectable()
export class OrganizationService {

View File

@ -1,9 +1,7 @@
import { Inject, Injectable } from "@nestjs/common";
import * as mailVerificationService from "../contracts/services/mail.verification.service";
import { AccountService } from "./account.service";
import { AccountFactoryService } from "./account.factory.service";
import { IRegistration } from "../contracts/models/registration";
import { IAccount } from "../contracts/models/account";
import * as authentication from "@apihub24/authentication";
@Injectable()
export class RegistrationService {
@ -13,7 +11,7 @@ export class RegistrationService {
@Inject(AccountFactoryService)
private readonly accountFactory: AccountFactoryService,
@Inject("@apihub24/mail_verification_service")
private readonly mailVerificationService: mailVerificationService.IMailVerificationService
private readonly mailVerificationService: authentication.IMailVerificationService
) {}
/**
@ -22,7 +20,9 @@ export class RegistrationService {
* @param registration An `IRegistration` object containing the data for the new account.
* @returns A Promise that resolves to the newly created and saved `IAccount` object, or `null` if the operation fails.
*/
async registerAccount(registration: IRegistration): Promise<IAccount | null> {
async registerAccount(
registration: authentication.IRegistration
): Promise<authentication.IAccount | null> {
const newAccount = await this.accountFactory.createFromRegistration(
registration
);

View File

@ -1,7 +1,7 @@
import { Injectable } from "@nestjs/common";
import { validate } from "class-validator";
import { IRight } from "../contracts/models/right";
import { Right } from "../models/right";
import { IRight } from "@apihub24/authentication";
@Injectable()
export class RightFactoryService {

View File

@ -1,6 +1,6 @@
import { Inject, Injectable } from "@nestjs/common";
import * as repository from "@apihub24/repository";
import { IRight } from "../contracts/models/right";
import { IRight } from "@apihub24/authentication";
@Injectable()
export class RightService {