add Documentation and start with tests

This commit is contained in:
admin 2025-08-22 17:35:47 +02:00
parent 264da24033
commit 9ed6c497c0
44 changed files with 804 additions and 56 deletions

View File

@ -57,15 +57,14 @@ const passwordModule = PasswordHasherModule.forRoot();
@Module({
imports: [
ConfigModule.forRoot(),
emailVerificationModule,
sessionModule,
jwtModule,
passwordModule,
TokenAuthenticationModule.forRoot([
...takeExportedProviders(emailVerificationModule),
...takeExportedProviders(sessionModule),
...takeExportedProviders(jwtModule),
...takeExportedProviders(passwordModule),
EmailVerificationModule.forRoot(),
InMemorySessionsModule.forRoot(),
JwtGeneratorModule.forRoot(),
PasswordHasherModule.forRoot(),
TokenAuthenticationModule.forRoot(),
],
controllers: [LoginController, LogoutController],
providers: [
// register the Repositories with InMemoryRepository
{
provide: "@apihub24/organization_repository",
@ -83,10 +82,7 @@ const passwordModule = PasswordHasherModule.forRoot();
provide: "@apihub24/right_repository",
useClass: InMemoryRepository<Right>,
},
]),
],
controllers: [LoginController, LogoutController],
providers: [],
// required exports for the TokenGuard
exports: [TokenAuthenticationModule, jwtModule],
})

View File

@ -1,11 +1,32 @@
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,9 +1,21 @@
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,4 +1,10 @@
/**
* represent a Organization Unit
*/
export interface IOrganization {
id: string;
/**
* the Name of the Organization
*/
name: string;
}

View File

@ -1,7 +1,25 @@
/**
* 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,4 +1,10 @@
/**
* represent a Application Right
*/
export interface IRight {
id: string;
/**
* the Right Name
*/
name: string;
}

View File

@ -1,7 +1,16 @@
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,4 +1,13 @@
/**
* 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,6 +1,25 @@
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,4 +1,24 @@
/**
* @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,9 +1,37 @@
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

@ -3,13 +3,38 @@ 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

@ -1,5 +1,9 @@
import { SetMetadata } from "@nestjs/common";
export const ORGANIZATIONS_KEY = "organizations";
/**
* register some Organizations for the TokenGuard that are required for this Operation
* @param organizations some Organization Names that are required
*/
export const Organizations = (...organizations: string[]) =>
SetMetadata(ORGANIZATIONS_KEY, organizations);

View File

@ -1,4 +1,8 @@
import { SetMetadata } from "@nestjs/common";
export const RIGHTS_KEY = "rights";
/**
* register some Right for the TokenGuard that are required for this Operation
* @param rights some Right Names that are required
*/
export const Rights = (...rights: string[]) => SetMetadata(RIGHTS_KEY, rights);

View File

@ -1,4 +1,8 @@
import { SetMetadata } from "@nestjs/common";
export const ROLES_KEY = "roles";
/**
* register some Roles for the TokenGuard that are required for this Operation
* @param roles some Role Names that are required
*/
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

View File

@ -10,14 +10,29 @@ import { ROLES_KEY } from "./roles.decorator";
import * as tokenService from "../contracts/services/token.service";
import { ORGANIZATIONS_KEY } from "./organization.decorator";
/**
* The `TokenGuard` is a NestJS guard that checks for user authorization based on a JWT token.
* It verifies if the user has the required rights, roles, and/or organizations to access a specific route.
*/
@Injectable()
export class TokenGuard implements CanActivate {
/**
* Initializes the guard with the `Reflector` to read metadata and the `tokenService` for token validation.
* @param reflector The `Reflector` instance to retrieve custom metadata.
* @param tokenService a SessionService implementation apihub24/token_service
*/
constructor(
private reflector: Reflector,
@Inject("@apihub24/token_service")
private readonly tokenService: tokenService.ITokenService
) {}
/**
* Determines if the current request can proceed.
* This is the core logic of the guard. It checks the token and the user's permissions.
* @param context The `ExecutionContext` object, providing access to the request, response, and handler.
* @returns A Promise that resolves to `true` if the request is authorized, otherwise `false`.
*/
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredRights = this.reflector.getAllAndOverride<string[]>(
RIGHTS_KEY,

View File

@ -27,4 +27,4 @@ export * from "./services/registration.service";
export * from "./services/right.factory.service";
export * from "./services/right.service";
export * from "./token-authentication.module";
export * from "./token.authentication.module";

View File

@ -7,23 +7,52 @@ import {
ValidateNested,
} from "class-validator";
/**
* represent the Users Account
*/
export class Account implements IAccount {
id: string;
@MinLength(3)
/**
* the Accounts name
*/
@MinLength(3, { message: "Account accountName must have min. 3 letters" })
accountName: string;
@MinLength(3)
/**
* the hashed Password
*/
@MinLength(3, { message: "Account passwordHash must have min. 3 letters" })
passwordHash: string;
@IsEmail()
/**
* a E-Mail Address for this Account
*/
@IsEmail(undefined, {
message: "Account email must be a valid email address",
})
email: string;
/**
* represents if the E-Mail Address was verified
*/
emailVerified: boolean;
/**
* represents if the User was activated for Login
*/
active: boolean;
@IsNotEmpty({ each: true })
@ValidateNested({ each: true })
/**
* the Groups this User was member
*/
@IsNotEmpty({
each: true,
message: "Account groups can not be undefined or null",
})
@ValidateNested({
each: true,
message: "Account groups some group was invalid",
})
groups: Group[];
}

View File

@ -3,17 +3,32 @@ import { Organization } from "./organization";
import { Right } from "./right";
import { IsNotEmpty, MinLength, ValidateNested } from "class-validator";
/**
* represents a Group that a User can Join
*/
export class Group implements IGroup {
id: string;
@MinLength(3)
/**
* the Name of the Group
*/
@MinLength(3, { message: "Group name must have min. 3 letters" })
name: string;
@IsNotEmpty()
@ValidateNested()
/**
* the Organization in that the Group are
*/
@IsNotEmpty({ message: "Group organization can not be undefined or null" })
@ValidateNested({ message: "Group organization was invalid" })
organization: Organization;
@IsNotEmpty({ each: true })
@ValidateNested({ each: true })
/**
* the Rights that have the Group
*/
@IsNotEmpty({
each: true,
message: "Group rights can not be undefined or null",
})
@ValidateNested({ each: true, message: "Group rights some right is invalid" })
rights: Right[];
}

View File

@ -1,10 +1,16 @@
import { IsNotEmpty, MinLength } from "class-validator";
import { IOrganization } from "../contracts/models/organization";
/**
* represent a Organization Unit
*/
export class Organization implements IOrganization {
id: string;
@IsNotEmpty()
@MinLength(3)
/**
* the Name of the Organization
*/
@IsNotEmpty({ message: "Organization name can not be undefined or null" })
@MinLength(3, { message: "Organization name must have min. 3 letters" })
name: string;
}

View File

@ -1,19 +1,46 @@
import { IsEmail, IsNotEmpty, MinLength } from "class-validator";
import { IRegistration } from "../contracts/models/registration";
/**
* represents Registration Data
*/
export class Registration implements IRegistration {
@MinLength(3)
/**
* the Account Name to register
*/
@MinLength(3, {
message: "Registration accountName must have min. 3 letters",
})
accountName: string;
@MinLength(3)
/**
* the Password to register
*/
@MinLength(3, { message: "Registration password must have min. 3 letters" })
password: string;
@MinLength(3)
/**
* the repeated Password
*/
@MinLength(3, {
message: "Registration passwordComparer must have min. 3 letters",
})
passwordComparer: string;
@IsEmail()
/**
* the E-Mail Address to register
*/
@IsEmail(undefined, {
message: "Registration email must be a valid email address",
})
email: string;
@IsNotEmpty({ each: true })
/**
* the Group Ids the Account must be Part
*/
@IsNotEmpty({
each: true,
message: "Registration groupIds can not be undefined or empty",
})
groupIds: string[];
}

View File

@ -1,9 +1,15 @@
import { MinLength } from "class-validator";
import { IRight } from "../contracts/models/right";
/**
* represent a Application Right
*/
export class Right implements IRight {
id: string;
@MinLength(3)
/**
* the Right Name
*/
@MinLength(3, { message: "Right name must have min. 3 letters" })
name: string;
}

View File

@ -2,12 +2,21 @@ import { IsNotEmpty, ValidateNested } from "class-validator";
import { Account } from "./account";
import { ISession } from "../contracts/models/session";
/**
* represents a Session
*/
export class Session implements ISession {
id: string;
@IsNotEmpty()
@ValidateNested()
/**
* the Account of the Session
*/
@IsNotEmpty({ message: "Session account can not be undefined or empty" })
@ValidateNested({ message: "Session account is invalid" })
account: Account;
/**
* some MetaData
*/
metaData: Record<string, any>;
}

View File

@ -1,10 +1,19 @@
import { MinLength } from "class-validator";
import { ISignIn } from "../contracts/models/sign.in";
/**
* represent a Sign In Request
*/
export class SignIn implements ISignIn {
@MinLength(3)
/**
* the Account Name to sign in
*/
@MinLength(3, { message: "SignIn accountName must have min. 3 letters" })
accountName: string;
@MinLength(3)
/**
* the Password to sign in
*/
@MinLength(3, { message: "SignIn password must have min. 3 letters" })
password: string;
}

View File

@ -17,6 +17,11 @@ export class AccountFactoryService {
private readonly groupService: GroupService
) {}
/**
* create a new Account from a Registration
* @param registration a Registration
* @returns a new User Account
*/
async createFromRegistration(registration: IRegistration): Promise<IAccount> {
let validationErrors = await validate(
plainToClass(Registration, registration)

View File

@ -12,19 +12,46 @@ export class AccountService {
private readonly groupRepository: repository.IRepository<IGroup>
) {}
/**
* This asynchronous method retrieves accounts that match a given filter.
* @param filter A function that takes an `IAccount` object and returns a boolean. It serves as the condition for selecting accounts.
* @returns A Promise that resolves to an array of `IAccount` objects that satisfy the filter condition.
*/
async getBy(filter: (account: IAccount) => boolean): Promise<IAccount[]> {
return await this.accountRepository.getBy(filter);
}
/**
* This asynchronous method saves a single `IAccount` object. It uses the `accountRepository` to persist the account. If the save operation fails, it logs a warning and returns null.
* @param account The `IAccount` object to be saved.
* @returns A Promise that resolves to the saved `IAccount` object, or `null` if the save operation fails.
*/
async save(account: IAccount): Promise<IAccount | null> {
try {
const accounts = await this.accountRepository.save([account]);
return accounts?.length ? accounts[0] : null;
} catch (err) {
console.warn(err);
return null;
}
}
/**
* This asynchronous method deletes accounts based on a provided filter.
* @param filter A function that takes an `IAccount` object and returns a boolean. It specifies which accounts to delete.
* @returns A Promise that resolves to `true` if the deletion is successful, and `false` otherwise.
*/
async delete(filter: (account: IAccount) => boolean): Promise<boolean> {
return await this.accountRepository.deleteBy(filter);
}
/**
* This asynchronous method adds a specific account to a group. It first finds both the account and the group using their IDs. If they are found, it adds the group to the account's groups array and saves the updated account.
* @param accountId The unique identifier of the account.
* @param groupId The unique identifier of the group to which the account should be added.
* @returns A Promise that resolves to the updated `IAccount` object.
* @throws An error if the account or the group cannot be found, or if the updated account cannot be saved.
*/
async addAccountToGroup(
accountId: string,
groupId: string
@ -39,6 +66,13 @@ export class AccountService {
return accountsSaved[0];
}
/**
* This asynchronous method removes a specific account from a group. It first finds the account and group, then filters the group out of the account's groups array before saving the updated account.
* @param accountId The unique identifier of the account.
* @param groupId The unique identifier of the group to be removed.
* @returns A Promise that resolves to the updated `IAccount` object.
* @throws An error if the account or the group cannot be found, or if the updated account cannot be saved.
*/
async removeAccountFromGroup(
accountId: string,
groupId: string

View File

@ -14,6 +14,13 @@ export class GroupFactoryService {
private readonly organizationService: OrganizationService
) {}
/**
* create a new Group in a Organization and Adds some Rights
* @param name a Name of the Group
* @param organizationId the Organization Id
* @param rightIds the Right Ids
* @returns the new Group
*/
async createGroup(
name: string,
organizationId: string,

View File

@ -12,19 +12,41 @@ export class GroupService {
private readonly rightRepository: repository.IRepository<IRight>
) {}
/**
* This asynchronous method retrieves groups that match a given filter.
* @param filter A function that takes a `IGroup` object and returns a boolean. It serves as the condition for selecting groups.
* @returns A Promise that resolves to an array of `IGroup` objects that satisfy the filter condition.
*/
async getBy(filter: (group: IGroup) => boolean): Promise<IGroup[]> {
return await this.groupRepository.getBy(filter);
}
/**
* This asynchronous method saves a single `IGroup` object. It uses the `groupRepository` to persist the group.
* @param group The `IGroup` object to be saved.
* @returns A Promise that resolves to the saved `IGroup` object, or `null` if the save operation fails.
*/
async save(group: IGroup): Promise<IGroup | null> {
const groups = await this.groupRepository.save([group]);
return groups?.length ? groups[0] : null;
}
/**
* This asynchronous method deletes groups based on a provided filter.
* @param filter A function that takes a `IGroup` object and returns a boolean. It specifies which groups to delete.
* @returns A Promise that resolves to `true` if the deletion is successful, and `false` otherwise.
*/
async delete(filter: (group: IGroup) => boolean): Promise<boolean> {
return this.groupRepository.deleteBy(filter);
}
/**
* This asynchronous method adds a specific right to a group. It first finds both the group and the right using their IDs. If they are found, it adds the right to the group's rights array and saves the updated group.
* @param groupId The unique identifier of the group.
* @param rightId The unique identifier of the right to be added.
* @returns A Promise that resolves to the updated `IGroup` object.
* @throws An error if the group or right is not found, or if the updated group cannot be saved.
*/
async addRightToGroup(groupId: string, rightId: string) {
const [group, right] = await this.getGroupAndRight(groupId, rightId);
group.rights = group.rights.filter((x) => x.id !== rightId);
@ -36,6 +58,13 @@ export class GroupService {
return savedGroups[0];
}
/**
* This asynchronous method removes a specific right from a group. It first finds the group and right, then filters the right out of the group's rights array before saving the updated group.
* @param groupId The unique identifier of the group.
* @param rightId The unique identifier of the right to be removed.
* @returns A Promise that resolves to the updated `IGroup` object.
* @throws An error if the group or right is not found, or if the updated group cannot be saved.
*/
async removeRightFromGroup(groupId: string, rightId: string) {
const [group] = await this.getGroupAndRight(groupId, rightId);
group.rights = group.rights.filter((x) => x.id !== rightId);

View File

@ -18,6 +18,14 @@ export class LoginService {
private readonly accountService: AccountService
) {}
/**
* This asynchronous method authenticates a user and creates a JWT token for a new session.
* It first finds an active account with the given `accountName`, verifies the password, and if successful, creates a new session and generates a signed token.
* @param signIn An `ISignIn` object containing the `accountName` and `password`.
* @param expires An optional string specifying the token's expiration duration (default: "1h").
* @param algorithm An optional string specifying the signing algorithm for the token (default: "HS512").
* @returns A Promise that resolves to a JWT token string. Returns an empty string if authentication fails.
*/
async signIn(
signIn: ISignIn,
expires: string = "1h",
@ -48,6 +56,12 @@ export class LoginService {
return token;
}
/**
* This asynchronous method logs a user out by removing their active session.
* It finds the account by its `id` and then locates and removes the corresponding session.
* @param accountId The unique identifier (`id`) of the account to be logged out.
* @returns A Promise that resolves to `void` after the operation is complete.
*/
async signOut(accountId: string): Promise<void> {
const accounts = await this.accountService.getBy((x) => x.id === accountId);
if (!accounts.length) {

View File

@ -4,6 +4,11 @@ import { Organization } from "../models/organization";
@Injectable()
export class OrganizationFactoryService {
/**
* creates a Organization from a Name
* @param name a Organization Name
* @returns the new Organization
*/
createFromName(name: string): IOrganization {
const organization = new Organization();
organization.name = name;

View File

@ -9,12 +9,23 @@ export class OrganizationService {
private readonly organizationRepository: repository.IRepository<IOrganization>
) {}
/**
* This asynchronous method retrieves organizations that match a given filter.
* @param filter A function that takes an `IOrganization` object and returns a boolean. It serves as the condition for selecting organizations.
* @returns A Promise that resolves to an array of `IOrganization` objects that satisfy the filter condition.
*/
async getBy(
filter: (organization: IOrganization) => boolean
): Promise<IOrganization[]> {
return await this.organizationRepository.getBy(filter);
}
/**
* This asynchronous method saves a single `IOrganization` object.
* @param organization The `IOrganization` object to be saved.
* @returns A Promise that resolves to the saved `IOrganization` object.
* @throws An error if the organization could not be saved.
*/
async save(organization: IOrganization): Promise<IOrganization> {
const savedOrganizations = await this.organizationRepository.save([
organization,
@ -25,6 +36,11 @@ export class OrganizationService {
return savedOrganizations[0];
}
/**
* This asynchronous method deletes organizations based on a provided filter.
* @param filter A function that takes an `IOrganization` object and returns a boolean. It specifies which organizations to delete.
* @returns A Promise that resolves to `true` if the deletion is successful, and `false` otherwise.
*/
async deleteBy(
filter: (organization: IOrganization) => boolean
): Promise<boolean> {

View File

@ -16,6 +16,12 @@ export class RegistrationService {
private readonly mailVerificationService: mailVerificationService.IMailVerificationService
) {}
/**
* This asynchronous method registers a new account based on the provided registration data.
* It uses an `AccountFactoryService` to create a new `IAccount` object and then saves it using the `AccountService`.
* @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> {
const newAccount = await this.accountFactory.createFromRegistration(
registration
@ -23,6 +29,13 @@ export class RegistrationService {
return this.accountService.save(newAccount);
}
/**
* This asynchronous method verifies a user's email address.
* It checks the provided code with the `mailVerificationService`. If successful, it finds all unverified accounts with that email, sets their `emailVerified` flag to `true`, and saves the changes.
* @param email The email address to be verified.
* @param code The verification code sent to the email address.
* @returns A Promise that resolves to `true` if the verification was successful, otherwise `false`.
*/
async verify(email: string, code: string): Promise<boolean> {
const verified = await this.mailVerificationService.verify(email, code);
if (!verified) {
@ -38,6 +51,12 @@ export class RegistrationService {
return verified;
}
/**
* This asynchronous method activates an account.
* It finds the account by its `id`, sets the `active` attribute to `true`, and saves the change.
* @param accountId The unique identifier (`id`) of the account to activate.
* @returns A Promise that resolves to `true` if the activation was successful, otherwise `false`.
*/
async activateAccount(accountId: string): Promise<boolean> {
const accounts = await this.accountService.getBy((x) => x.id === accountId);
if (!accounts?.length) {
@ -48,6 +67,12 @@ export class RegistrationService {
return savedAccount?.active === true;
}
/**
* This asynchronous method deactivates an account.
* It finds the account by its `id`, sets the `active` attribute to `false`, and saves the change.
* @param accountId The unique identifier (`id`) of the account to deactivate.
* @returns A Promise that resolves to `true` if the deactivation was successful, otherwise `false`. Returns `true` if the account is not found, as it is already in a "deactivated" state.
*/
async deactivateAccount(accountId: string): Promise<boolean> {
const accounts = await this.accountService.getBy((x) => x.id === accountId);
if (!accounts?.length) {
@ -58,6 +83,12 @@ export class RegistrationService {
return savedAccount?.active === false;
}
/**
* This asynchronous method unregister (deletes) an account.
* It finds the account by its `id` and triggers its deletion using the `accountService`. The operation is "fire-and-forget" and does not wait for completion.
* @param accountId The unique identifier (`id`) of the account to unregister.
* @returns A Promise that resolves to `void`.
*/
async unregisterAccount(accountId: string): Promise<void> {
const accounts = await this.accountService.getBy((x) => x.id === accountId);
if (!accounts?.length) {

View File

@ -5,6 +5,11 @@ import { Right } from "../models/right";
@Injectable()
export class RightFactoryService {
/**
* creates a Right from a Name
* @param name a Name for a Right
* @returns the new Right
*/
async createRight(name: string): Promise<IRight> {
const right = new Right();
right.name = name;

View File

@ -9,13 +9,30 @@ export class RightService {
private readonly rightRepository: repository.IRepository<IRight>
) {}
/**
* This asynchronous method retrieves rights that match a given filter.
* @param filter A function that takes an `IRight` object and returns a boolean. It serves as the condition for selecting rights.
* @returns A Promise that resolves to an array of `IRight` objects that satisfy the filter condition.
*/
async getBy(filter: (right: IRight) => boolean): Promise<IRight[]> {
return await this.rightRepository.getBy(filter);
}
/**
* This asynchronous method saves a single `IRight` object. It uses the `rightRepository` to persist the right.
* @param right The `IRight` object to be saved.
* @returns A Promise that resolves to the saved `IRight` object, or `null` if the save operation fails.
*/
async save(right: IRight): Promise<IRight | null> {
const rights = await this.rightRepository.save([right]);
return rights?.length ? rights[0] : null;
}
/**
* This asynchronous method deletes rights based on a provided filter.
* @param filter A function that takes an `IRight` object and returns a boolean. It specifies which rights to delete.
* @returns A Promise that resolves to `true` if the deletion is successful, and `false` otherwise.
*/
async delete(filter: (right: IRight) => boolean): Promise<boolean> {
return await this.rightRepository.deleteBy(filter);
}

View File

@ -0,0 +1,35 @@
import { Test, TestingModule } from "@nestjs/testing";
import { AccountRepositoryMockModule } from "../test/account.repository.mock";
import { GroupRepositoryMockModule } from "../test/group.repository.mock";
import { MailServiceMockModule } from "../test/mail.verification.service.mock";
import { OrganizationRepositoryMockModule } from "../test/organization.repository.mock";
import { PasswordServiceMockModule } from "../test/password.service.mock";
import { RightRepositoryMockModule } from "../test/right.repository.mock";
import { SessionServiceMockModule } from "../test/session.service.mock";
import { TokenServiceMockModule } from "../test/token.service.mock";
import { AccountFactoryService, TokenAuthenticationModule } from "../src";
describe("TokenAuthenticationModule 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,
],
}).compile();
});
it("should exports AccountFactoryService", () => {
expect(module).toBeDefined();
expect(module?.get(AccountFactoryService)).toBeDefined();
});
});

View File

@ -1,4 +1,4 @@
import { DynamicModule, Module, Provider } from "@nestjs/common";
import { DynamicModule, Global, Module, Provider } from "@nestjs/common";
import { RightService } from "./services/right.service";
import { GroupService } from "./services/group.service";
import { AccountService } from "./services/account.service";
@ -11,9 +11,10 @@ import { RightFactoryService } from "./services/right.factory.service";
import { OrganizationService } from "./services/organization.service";
import { OrganizationFactoryService } from "./services/organization.factory.service";
@Global()
@Module({})
export class TokenAuthenticationModule {
static forRoot(providers: Provider[]): DynamicModule {
static forRoot(): DynamicModule {
return {
module: TokenAuthenticationModule,
imports: [ConfigModule.forRoot()],
@ -28,7 +29,6 @@ export class TokenAuthenticationModule {
AccountFactoryService,
GroupFactoryService,
RightFactoryService,
...providers,
],
exports: [
RightService,

View File

@ -0,0 +1,33 @@
import { DynamicModule, Global, Module } from "@nestjs/common";
import { IAccount } from "../src";
import { IRepository } from "@apihub24/repository";
@Global()
@Module({})
export class AccountRepositoryMockModule {
static forRoot(): DynamicModule {
const providers = [
{
provide: "@apihub24/account_repository",
useClass: AccountRepositoryMock,
},
];
return {
module: AccountRepositoryMockModule,
providers: [...providers],
exports: [...providers],
};
}
}
class AccountRepositoryMock implements IRepository<IAccount> {
getBy(filter: (model: IAccount) => boolean): Promise<IAccount[]> {
throw new Error("Method not implemented.");
}
save(models: IAccount[]): Promise<IAccount[]> {
throw new Error("Method not implemented.");
}
deleteBy(filter: (model: IAccount) => boolean): Promise<boolean> {
throw new Error("Method not implemented.");
}
}

View File

@ -0,0 +1,33 @@
import { DynamicModule, Global, Module } from "@nestjs/common";
import { IGroup } from "../src";
import { IRepository } from "@apihub24/repository";
@Global()
@Module({})
export class GroupRepositoryMockModule {
static forRoot(): DynamicModule {
const providers = [
{
provide: "@apihub24/group_repository",
useClass: GroupRepositoryMock,
},
];
return {
module: GroupRepositoryMockModule,
providers: [...providers],
exports: [...providers],
};
}
}
class GroupRepositoryMock implements IRepository<IGroup> {
getBy(filter: (model: IGroup) => boolean): Promise<IGroup[]> {
throw new Error("Method not implemented.");
}
save(models: IGroup[]): Promise<IGroup[]> {
throw new Error("Method not implemented.");
}
deleteBy(filter: (model: IGroup) => boolean): Promise<boolean> {
throw new Error("Method not implemented.");
}
}

View File

@ -0,0 +1,29 @@
import { DynamicModule, Global, Module } from "@nestjs/common";
import { IAccount, ISession, IMailVerificationService } from "../src";
@Global()
@Module({})
export class MailServiceMockModule {
static forRoot(): DynamicModule {
const providers = [
{
provide: "@apihub24/mail_verification_service",
useClass: MailServiceMock,
},
];
return {
module: MailServiceMockModule,
providers: [...providers],
exports: [...providers],
};
}
}
class MailServiceMock implements IMailVerificationService {
sendVerificationMail(account: IAccount): Promise<void> {
throw new Error("Method not implemented.");
}
verify(email: string, code: string): Promise<boolean> {
throw new Error("Method not implemented.");
}
}

View File

@ -0,0 +1,33 @@
import { DynamicModule, Global, Module } from "@nestjs/common";
import { IOrganization } from "../src";
import { IRepository } from "@apihub24/repository";
@Global()
@Module({})
export class OrganizationRepositoryMockModule {
static forRoot(): DynamicModule {
const providers = [
{
provide: "@apihub24/organization_repository",
useClass: OrganizationRepositoryMock,
},
];
return {
module: OrganizationRepositoryMockModule,
providers: [...providers],
exports: [...providers],
};
}
}
class OrganizationRepositoryMock implements IRepository<IOrganization> {
getBy(filter: (model: IOrganization) => boolean): Promise<IOrganization[]> {
throw new Error("Method not implemented.");
}
save(models: IOrganization[]): Promise<IOrganization[]> {
throw new Error("Method not implemented.");
}
deleteBy(filter: (model: IOrganization) => boolean): Promise<boolean> {
throw new Error("Method not implemented.");
}
}

View File

@ -0,0 +1,26 @@
import { DynamicModule, Global, Module } from "@nestjs/common";
import { IPasswordService } from "../src";
@Global()
@Module({})
export class PasswordServiceMockModule {
static forRoot(): DynamicModule {
const providers = [
{ provide: "@apihub24/password_service", useClass: PasswordServiceMock },
];
return {
module: PasswordServiceMock,
providers: [...providers],
exports: [...providers],
};
}
}
class PasswordServiceMock implements IPasswordService {
hash(plainTextPassword: string): Promise<string> {
throw new Error("Method not implemented.");
}
verify(plainTextPassword: string, passwordHash: string): Promise<boolean> {
throw new Error("Method not implemented.");
}
}

View File

@ -0,0 +1,33 @@
import { DynamicModule, Global, Module } from "@nestjs/common";
import { IRight } from "../src";
import { IRepository } from "@apihub24/repository";
@Global()
@Module({})
export class RightRepositoryMockModule {
static forRoot(): DynamicModule {
const providers = [
{
provide: "@apihub24/right_repository",
useClass: RightRepositoryMock,
},
];
return {
module: RightRepositoryMockModule,
providers: [...providers],
exports: [...providers],
};
}
}
class RightRepositoryMock implements IRepository<IRight> {
getBy(filter: (model: IRight) => boolean): Promise<IRight[]> {
throw new Error("Method not implemented.");
}
save(models: IRight[]): Promise<IRight[]> {
throw new Error("Method not implemented.");
}
deleteBy(filter: (model: IRight) => boolean): Promise<boolean> {
throw new Error("Method not implemented.");
}
}

View File

@ -0,0 +1,32 @@
import { DynamicModule, Global, Module } from "@nestjs/common";
import { IAccount, ISession, ISessionService } from "../src";
@Global()
@Module({})
export class SessionServiceMockModule {
static forRoot(): DynamicModule {
const providers = [
{ provide: "@apihub24/session_service", useClass: SessionServiceMock },
];
return {
module: SessionServiceMockModule,
providers: [...providers],
exports: [...providers],
};
}
}
class SessionServiceMock implements ISessionService {
create(account: IAccount): Promise<ISession> {
throw new Error("Method not implemented.");
}
getBy(filter: (account: IAccount) => boolean): Promise<ISession[]> {
throw new Error("Method not implemented.");
}
getById(sessionId: string): Promise<ISession | null> {
throw new Error("Method not implemented.");
}
remove(sessionId: string): Promise<void> {
throw new Error("Method not implemented.");
}
}

View File

@ -0,0 +1,34 @@
import { DynamicModule, Global, Module } from "@nestjs/common";
import { Algorithm, IAccount, ISession, ITokenService } from "../src";
@Global()
@Module({})
export class TokenServiceMockModule {
static forRoot(): DynamicModule {
const providers = [
{ provide: "@apihub24/token_service", useClass: TokenServiceMock },
];
return {
module: TokenServiceMockModule,
providers: [...providers],
exports: [...providers],
};
}
}
class TokenServiceMock implements ITokenService {
generate(
session: ISession,
subject: string,
expires?: string,
algorithm?: Algorithm
): Promise<string> {
throw new Error("Method not implemented.");
}
validate(token: string): Promise<boolean> {
throw new Error("Method not implemented.");
}
getAccount(token: string): Promise<IAccount | null> {
throw new Error("Method not implemented.");
}
}