From 9ed6c497c029080970413dc1d1695a7182c9cf08 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 22 Aug 2025 17:35:47 +0200 Subject: [PATCH] add Documentation and start with tests --- README.md | 52 +++++++++---------- src/contracts/models/account.ts | 21 ++++++++ src/contracts/models/group.ts | 12 +++++ src/contracts/models/organization.ts | 6 +++ src/contracts/models/registration.ts | 18 +++++++ src/contracts/models/right.ts | 6 +++ src/contracts/models/session.ts | 9 ++++ src/contracts/models/sign.in.ts | 9 ++++ .../services/mail.verification.service.ts | 19 +++++++ src/contracts/services/password.service.ts | 20 +++++++ src/contracts/services/session.service.ts | 28 ++++++++++ src/contracts/services/token.service.ts | 25 +++++++++ src/guards/organization.decorator.ts | 4 ++ src/guards/rights.decorator.ts | 4 ++ src/guards/roles.decorator.ts | 4 ++ src/guards/token.guard.ts | 15 ++++++ src/index.ts | 2 +- src/models/account.ts | 39 ++++++++++++-- src/models/group.ts | 25 +++++++-- src/models/organization.ts | 10 +++- src/models/registration.ts | 37 +++++++++++-- src/models/right.ts | 8 ++- src/models/session.ts | 13 ++++- src/models/sign.in.ts | 13 ++++- src/services/account.factory.service.ts | 5 ++ src/services/account.service.ts | 38 +++++++++++++- src/services/group.factory.service.ts | 7 +++ src/services/group.service.ts | 29 +++++++++++ src/services/login.service.ts | 14 +++++ src/services/organization.factory.service.ts | 5 ++ src/services/organization.service.ts | 16 ++++++ src/services/registration.service.ts | 31 +++++++++++ src/services/right.factory.service.ts | 5 ++ src/services/right.service.ts | 17 ++++++ src/token.authentication.module.spec.ts | 35 +++++++++++++ ...dule.ts => token.authentication.module.ts} | 6 +-- test/account.repository.mock.ts | 33 ++++++++++++ test/group.repository.mock.ts | 33 ++++++++++++ test/mail.verification.service.mock.ts | 29 +++++++++++ test/organization.repository.mock.ts | 33 ++++++++++++ test/password.service.mock.ts | 26 ++++++++++ test/right.repository.mock.ts | 33 ++++++++++++ test/session.service.mock.ts | 32 ++++++++++++ test/token.service.mock.ts | 34 ++++++++++++ 44 files changed, 804 insertions(+), 56 deletions(-) create mode 100644 src/token.authentication.module.spec.ts rename src/{token-authentication.module.ts => token.authentication.module.ts} (90%) create mode 100644 test/account.repository.mock.ts create mode 100644 test/group.repository.mock.ts create mode 100644 test/mail.verification.service.mock.ts create mode 100644 test/organization.repository.mock.ts create mode 100644 test/password.service.mock.ts create mode 100644 test/right.repository.mock.ts create mode 100644 test/session.service.mock.ts create mode 100644 test/token.service.mock.ts diff --git a/README.md b/README.md index 51ef8b6..97a0baa 100644 --- a/README.md +++ b/README.md @@ -57,36 +57,32 @@ const passwordModule = PasswordHasherModule.forRoot(); @Module({ imports: [ ConfigModule.forRoot(), - emailVerificationModule, - sessionModule, - jwtModule, - passwordModule, - TokenAuthenticationModule.forRoot([ - ...takeExportedProviders(emailVerificationModule), - ...takeExportedProviders(sessionModule), - ...takeExportedProviders(jwtModule), - ...takeExportedProviders(passwordModule), - // register the Repositories with InMemoryRepository - { - provide: "@apihub24/organization_repository", - useClass: InMemoryRepository, - }, - { - provide: "@apihub24/account_repository", - useClass: InMemoryRepository, - }, - { - provide: "@apihub24/group_repository", - useClass: InMemoryRepository, - }, - { - provide: "@apihub24/right_repository", - useClass: InMemoryRepository, - }, - ]), + EmailVerificationModule.forRoot(), + InMemorySessionsModule.forRoot(), + JwtGeneratorModule.forRoot(), + PasswordHasherModule.forRoot(), + TokenAuthenticationModule.forRoot(), ], controllers: [LoginController, LogoutController], - providers: [], + providers: [ + // register the Repositories with InMemoryRepository + { + provide: "@apihub24/organization_repository", + useClass: InMemoryRepository, + }, + { + provide: "@apihub24/account_repository", + useClass: InMemoryRepository, + }, + { + provide: "@apihub24/group_repository", + useClass: InMemoryRepository, + }, + { + provide: "@apihub24/right_repository", + useClass: InMemoryRepository, + }, + ], // required exports for the TokenGuard exports: [TokenAuthenticationModule, jwtModule], }) diff --git a/src/contracts/models/account.ts b/src/contracts/models/account.ts index 5cd007a..a8a0e1b 100644 --- a/src/contracts/models/account.ts +++ b/src/contracts/models/account.ts @@ -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[]; } diff --git a/src/contracts/models/group.ts b/src/contracts/models/group.ts index 37bf32a..5eb0bc6 100644 --- a/src/contracts/models/group.ts +++ b/src/contracts/models/group.ts @@ -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[]; } diff --git a/src/contracts/models/organization.ts b/src/contracts/models/organization.ts index d91d0c6..0bca9e7 100644 --- a/src/contracts/models/organization.ts +++ b/src/contracts/models/organization.ts @@ -1,4 +1,10 @@ +/** + * represent a Organization Unit + */ export interface IOrganization { id: string; + /** + * the Name of the Organization + */ name: string; } diff --git a/src/contracts/models/registration.ts b/src/contracts/models/registration.ts index f185e38..21a5838 100644 --- a/src/contracts/models/registration.ts +++ b/src/contracts/models/registration.ts @@ -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[]; } diff --git a/src/contracts/models/right.ts b/src/contracts/models/right.ts index 59bbb69..540d45b 100644 --- a/src/contracts/models/right.ts +++ b/src/contracts/models/right.ts @@ -1,4 +1,10 @@ +/** + * represent a Application Right + */ export interface IRight { id: string; + /** + * the Right Name + */ name: string; } diff --git a/src/contracts/models/session.ts b/src/contracts/models/session.ts index 5ba12e4..c9b67b6 100644 --- a/src/contracts/models/session.ts +++ b/src/contracts/models/session.ts @@ -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; } diff --git a/src/contracts/models/sign.in.ts b/src/contracts/models/sign.in.ts index 3461bea..d242ebc 100644 --- a/src/contracts/models/sign.in.ts +++ b/src/contracts/models/sign.in.ts @@ -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; } diff --git a/src/contracts/services/mail.verification.service.ts b/src/contracts/services/mail.verification.service.ts index 004f2e1..75743a3 100644 --- a/src/contracts/services/mail.verification.service.ts +++ b/src/contracts/services/mail.verification.service.ts @@ -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; + + /** + * 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; } diff --git a/src/contracts/services/password.service.ts b/src/contracts/services/password.service.ts index 79545e9..7678151 100644 --- a/src/contracts/services/password.service.ts +++ b/src/contracts/services/password.service.ts @@ -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; + + /** + * 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; } diff --git a/src/contracts/services/session.service.ts b/src/contracts/services/session.service.ts index 33a986d..5f46a93 100644 --- a/src/contracts/services/session.service.ts +++ b/src/contracts/services/session.service.ts @@ -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; + + /** + * 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; + + /** + * 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; + + /** + * 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; } diff --git a/src/contracts/services/token.service.ts b/src/contracts/services/token.service.ts index 22e821c..f2f6a85 100644 --- a/src/contracts/services/token.service.ts +++ b/src/contracts/services/token.service.ts @@ -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; + + /** + * 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; + + /** + * 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; } diff --git a/src/guards/organization.decorator.ts b/src/guards/organization.decorator.ts index ccae042..f460c3a 100644 --- a/src/guards/organization.decorator.ts +++ b/src/guards/organization.decorator.ts @@ -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); diff --git a/src/guards/rights.decorator.ts b/src/guards/rights.decorator.ts index 4604968..eb11079 100644 --- a/src/guards/rights.decorator.ts +++ b/src/guards/rights.decorator.ts @@ -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); diff --git a/src/guards/roles.decorator.ts b/src/guards/roles.decorator.ts index 3d51da8..61f9574 100644 --- a/src/guards/roles.decorator.ts +++ b/src/guards/roles.decorator.ts @@ -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); diff --git a/src/guards/token.guard.ts b/src/guards/token.guard.ts index 5282bed..797c546 100644 --- a/src/guards/token.guard.ts +++ b/src/guards/token.guard.ts @@ -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 { const requiredRights = this.reflector.getAllAndOverride( RIGHTS_KEY, diff --git a/src/index.ts b/src/index.ts index 5931fa6..7afa80c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"; diff --git a/src/models/account.ts b/src/models/account.ts index b166bce..b28441d 100644 --- a/src/models/account.ts +++ b/src/models/account.ts @@ -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[]; } diff --git a/src/models/group.ts b/src/models/group.ts index 68575cc..1bd4986 100644 --- a/src/models/group.ts +++ b/src/models/group.ts @@ -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[]; } diff --git a/src/models/organization.ts b/src/models/organization.ts index c652934..634fbed 100644 --- a/src/models/organization.ts +++ b/src/models/organization.ts @@ -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; } diff --git a/src/models/registration.ts b/src/models/registration.ts index a3dbed6..23c9d80 100644 --- a/src/models/registration.ts +++ b/src/models/registration.ts @@ -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[]; } diff --git a/src/models/right.ts b/src/models/right.ts index 40929f2..08b392f 100644 --- a/src/models/right.ts +++ b/src/models/right.ts @@ -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; } diff --git a/src/models/session.ts b/src/models/session.ts index 9b87552..7368bac 100644 --- a/src/models/session.ts +++ b/src/models/session.ts @@ -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; } diff --git a/src/models/sign.in.ts b/src/models/sign.in.ts index 817b70c..808e5e1 100644 --- a/src/models/sign.in.ts +++ b/src/models/sign.in.ts @@ -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; } diff --git a/src/services/account.factory.service.ts b/src/services/account.factory.service.ts index a29b790..6e75e60 100644 --- a/src/services/account.factory.service.ts +++ b/src/services/account.factory.service.ts @@ -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 { let validationErrors = await validate( plainToClass(Registration, registration) diff --git a/src/services/account.service.ts b/src/services/account.service.ts index 551e6b8..1fae6d1 100644 --- a/src/services/account.service.ts +++ b/src/services/account.service.ts @@ -12,19 +12,46 @@ export class AccountService { private readonly groupRepository: repository.IRepository ) {} + /** + * 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 { 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 { - const accounts = await this.accountRepository.save([account]); - return accounts?.length ? accounts[0] : 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 { 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 diff --git a/src/services/group.factory.service.ts b/src/services/group.factory.service.ts index 4f92c60..e0f488e 100644 --- a/src/services/group.factory.service.ts +++ b/src/services/group.factory.service.ts @@ -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, diff --git a/src/services/group.service.ts b/src/services/group.service.ts index 718522c..14ebf37 100644 --- a/src/services/group.service.ts +++ b/src/services/group.service.ts @@ -12,19 +12,41 @@ export class GroupService { private readonly rightRepository: repository.IRepository ) {} + /** + * 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 { 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 { 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 { 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); diff --git a/src/services/login.service.ts b/src/services/login.service.ts index aa5974a..fe4711b 100644 --- a/src/services/login.service.ts +++ b/src/services/login.service.ts @@ -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 { const accounts = await this.accountService.getBy((x) => x.id === accountId); if (!accounts.length) { diff --git a/src/services/organization.factory.service.ts b/src/services/organization.factory.service.ts index 3cf3ca0..6af96a1 100644 --- a/src/services/organization.factory.service.ts +++ b/src/services/organization.factory.service.ts @@ -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; diff --git a/src/services/organization.service.ts b/src/services/organization.service.ts index 95ee3a2..9b45f3e 100644 --- a/src/services/organization.service.ts +++ b/src/services/organization.service.ts @@ -9,12 +9,23 @@ export class OrganizationService { private readonly organizationRepository: repository.IRepository ) {} + /** + * 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 { 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 { 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 { diff --git a/src/services/registration.service.ts b/src/services/registration.service.ts index 75ebda8..dedae03 100644 --- a/src/services/registration.service.ts +++ b/src/services/registration.service.ts @@ -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 { 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 { 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 { 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 { 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 { const accounts = await this.accountService.getBy((x) => x.id === accountId); if (!accounts?.length) { diff --git a/src/services/right.factory.service.ts b/src/services/right.factory.service.ts index 08d9e29..8d4dbef 100644 --- a/src/services/right.factory.service.ts +++ b/src/services/right.factory.service.ts @@ -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 { const right = new Right(); right.name = name; diff --git a/src/services/right.service.ts b/src/services/right.service.ts index 2fd9335..8234796 100644 --- a/src/services/right.service.ts +++ b/src/services/right.service.ts @@ -9,13 +9,30 @@ export class RightService { private readonly rightRepository: repository.IRepository ) {} + /** + * 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 { 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 { 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 { return await this.rightRepository.deleteBy(filter); } diff --git a/src/token.authentication.module.spec.ts b/src/token.authentication.module.spec.ts new file mode 100644 index 0000000..8f64489 --- /dev/null +++ b/src/token.authentication.module.spec.ts @@ -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(); + }); +}); diff --git a/src/token-authentication.module.ts b/src/token.authentication.module.ts similarity index 90% rename from src/token-authentication.module.ts rename to src/token.authentication.module.ts index 6865ba3..cb5fed8 100644 --- a/src/token-authentication.module.ts +++ b/src/token.authentication.module.ts @@ -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, diff --git a/test/account.repository.mock.ts b/test/account.repository.mock.ts new file mode 100644 index 0000000..e565a09 --- /dev/null +++ b/test/account.repository.mock.ts @@ -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 { + getBy(filter: (model: IAccount) => boolean): Promise { + throw new Error("Method not implemented."); + } + save(models: IAccount[]): Promise { + throw new Error("Method not implemented."); + } + deleteBy(filter: (model: IAccount) => boolean): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/test/group.repository.mock.ts b/test/group.repository.mock.ts new file mode 100644 index 0000000..16fd411 --- /dev/null +++ b/test/group.repository.mock.ts @@ -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 { + getBy(filter: (model: IGroup) => boolean): Promise { + throw new Error("Method not implemented."); + } + save(models: IGroup[]): Promise { + throw new Error("Method not implemented."); + } + deleteBy(filter: (model: IGroup) => boolean): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/test/mail.verification.service.mock.ts b/test/mail.verification.service.mock.ts new file mode 100644 index 0000000..0824360 --- /dev/null +++ b/test/mail.verification.service.mock.ts @@ -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 { + throw new Error("Method not implemented."); + } + verify(email: string, code: string): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/test/organization.repository.mock.ts b/test/organization.repository.mock.ts new file mode 100644 index 0000000..8d69f06 --- /dev/null +++ b/test/organization.repository.mock.ts @@ -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 { + getBy(filter: (model: IOrganization) => boolean): Promise { + throw new Error("Method not implemented."); + } + save(models: IOrganization[]): Promise { + throw new Error("Method not implemented."); + } + deleteBy(filter: (model: IOrganization) => boolean): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/test/password.service.mock.ts b/test/password.service.mock.ts new file mode 100644 index 0000000..3514890 --- /dev/null +++ b/test/password.service.mock.ts @@ -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 { + throw new Error("Method not implemented."); + } + verify(plainTextPassword: string, passwordHash: string): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/test/right.repository.mock.ts b/test/right.repository.mock.ts new file mode 100644 index 0000000..199b7e7 --- /dev/null +++ b/test/right.repository.mock.ts @@ -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 { + getBy(filter: (model: IRight) => boolean): Promise { + throw new Error("Method not implemented."); + } + save(models: IRight[]): Promise { + throw new Error("Method not implemented."); + } + deleteBy(filter: (model: IRight) => boolean): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/test/session.service.mock.ts b/test/session.service.mock.ts new file mode 100644 index 0000000..94890e8 --- /dev/null +++ b/test/session.service.mock.ts @@ -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 { + throw new Error("Method not implemented."); + } + getBy(filter: (account: IAccount) => boolean): Promise { + throw new Error("Method not implemented."); + } + getById(sessionId: string): Promise { + throw new Error("Method not implemented."); + } + remove(sessionId: string): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/test/token.service.mock.ts b/test/token.service.mock.ts new file mode 100644 index 0000000..397a223 --- /dev/null +++ b/test/token.service.mock.ts @@ -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 { + throw new Error("Method not implemented."); + } + validate(token: string): Promise { + throw new Error("Method not implemented."); + } + getAccount(token: string): Promise { + throw new Error("Method not implemented."); + } +}