This commit is contained in:
admin 2025-08-21 12:05:19 +02:00
parent 3803d45d14
commit ef16f38a67
38 changed files with 232 additions and 201 deletions

12
package-lock.json generated
View File

@ -1,15 +1,15 @@
{
"name": "@apihub24/token-authentication",
"version": "1.0.0",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@apihub24/token-authentication",
"version": "1.0.0",
"version": "1.0.1",
"license": "MIT",
"dependencies": {
"@apihub24/repository": "^1.0.1",
"@apihub24/repository": "^1.0.2",
"@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.6",
@ -20,9 +20,9 @@
}
},
"node_modules/@apihub24/repository": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@apihub24/repository/-/repository-1.0.1.tgz",
"integrity": "sha512-ex3Z+lxsHtVKDTolJQqLHswq9SKfXzM/hWv17zsrhKqJwuGxO7CeBFM60aiuApZX9NqBhGAkPGGj9jt+F/Y9HQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@apihub24/repository/-/repository-1.0.2.tgz",
"integrity": "sha512-brZkSENCpC1/MJYBSHHeO+8odrKdq/q4U2z7HN5mrAvPl8GNjq0egOCZ22axQKynWuP9yimIIwJxExwp12YBzQ==",
"license": "MIT"
},
"node_modules/@borewit/text-codec": {

View File

@ -11,7 +11,7 @@
"@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.6",
"@apihub24/repository": "^1.0.1",
"@apihub24/repository": "^1.0.2",
"class-validator": "^0.14.2"
},
"devDependencies": {

View File

@ -1,12 +0,0 @@
export * from "./models/account";
export * from "./models/group";
export * from "./models/organization";
export * from "./models/registration";
export * from "./models/right";
export * from "./models/session";
export * from "./models/sign.in";
export * from "./services/mail.verification.service";
export * from "./services/password.service";
export * from "./services/session.service";
export * from "./services/token.service";

View File

@ -1,28 +1,11 @@
import { Group } from './group';
import {
IsEmail,
IsNotEmpty,
MinLength,
ValidateNested,
} from 'class-validator';
import { IGroup } from "./group";
export class Account {
export interface IAccount {
id: string;
@MinLength(3)
accountName: string;
@MinLength(3)
passwordHash: string;
@IsEmail()
email: string;
emailVerified: boolean;
active: boolean;
@IsNotEmpty({ each: true })
@ValidateNested({ each: true })
groups: Group[];
groups: IGroup[];
}

View File

@ -1,18 +1,9 @@
import { Organization } from './organization';
import { Right } from './right';
import { IsNotEmpty, MinLength, ValidateNested } from 'class-validator';
import { IOrganization } from "./organization";
import { IRight } from "./right";
export class Group {
export interface IGroup {
id: string;
@MinLength(3)
name: string;
@IsNotEmpty()
@ValidateNested()
organization: Organization;
@IsNotEmpty({ each: true })
@ValidateNested({ each: true })
rights: Right[];
organization: IOrganization;
rights: IRight[];
}

View File

@ -1,9 +1,4 @@
import { IsNotEmpty, MinLength } from 'class-validator';
export class Organization {
export interface IOrganization {
id: string;
@IsNotEmpty()
@MinLength(3)
name: string;
}

View File

@ -1,22 +1,7 @@
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';
export class Registration {
// @IsNotEmpty()
@MinLength(3)
export interface IRegistration {
accountName: string;
// @IsNotEmpty()
@MinLength(3)
password: string;
// @IsNotEmpty()
@MinLength(3)
passwordComparer: string;
// @IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty({ each: true })
groupIds: string[];
}

View File

@ -1,8 +1,4 @@
import { MinLength } from 'class-validator';
export class Right {
export interface IRight {
id: string;
@MinLength(3)
name: string;
}

View File

@ -1,12 +1,7 @@
import { IsNotEmpty, ValidateNested } from "class-validator";
import { Account } from "./account";
import { IAccount } from "./account";
export class Session {
export interface ISession {
id: string;
@IsNotEmpty()
@ValidateNested()
account: Account;
account: IAccount;
metaData: Record<string, any>;
}

View File

@ -1,8 +1,4 @@
import { MinLength } from 'class-validator';
export class SignIn {
@MinLength(3)
export interface ISignIn {
accountName: string;
@MinLength(3)
password: string;
}

View File

@ -1,6 +1,6 @@
import { Account } from "../models/account";
import { IAccount } from "../models/account";
export interface MailVerificationService {
sendVerificationMail(account: Account): Promise<void>;
export interface IMailVerificationService {
sendVerificationMail(account: IAccount): Promise<void>;
verify(email: string, code: string): Promise<boolean>;
}

View File

@ -1,4 +1,4 @@
export interface PasswordService {
export interface IPasswordService {
hash(plainTextPassword: string): Promise<string>;
verify(plainTextPassword: string, passwordHash: string): Promise<boolean>;
}

View File

@ -1,9 +1,9 @@
import { Account } from "../models/account";
import { Session } from "../models/session";
import { IAccount } from "../models/account";
import { ISession } from "../models/session";
export interface SessionService {
create(account: Account): Promise<Session>;
getBy(filter: (account: Account) => boolean): Promise<Session[]>;
getById(sessionId: string): Promise<Session | null>;
export interface ISessionService {
create(account: IAccount): Promise<ISession>;
getBy(filter: (account: IAccount) => boolean): Promise<ISession[]>;
getById(sessionId: string): Promise<ISession | null>;
remove(sessionId: string): Promise<void>;
}

View File

@ -1,8 +1,8 @@
import { Account } from "../models/account";
import { Session } from "../models/session";
import { IAccount } from "../models/account";
import { ISession } from "../models/session";
export interface TokenService {
generate(session: Session): Promise<string>;
export interface ITokenService {
generate(session: ISession): Promise<string>;
validate(token: string): Promise<boolean>;
getAccount(token: string): Promise<Account | null>;
getAccount(token: string): Promise<IAccount | null>;
}

View File

@ -1,4 +0,0 @@
export * from "./organization.decorator";
export * from "./rights.decorator";
export * from "./roles.decorator";
export * from "./token.guard";

View File

@ -1,5 +1,5 @@
import { SetMetadata } from '@nestjs/common';
import { SetMetadata } from "@nestjs/common";
export const ORGANIZATIONS_KEY = 'organizations';
export const ORGANIZATIONS_KEY = "organizations";
export const Organizations = (...organizations: string[]) =>
SetMetadata(ORGANIZATIONS_KEY, organizations);

View File

@ -1,4 +1,4 @@
import { SetMetadata } from '@nestjs/common';
import { SetMetadata } from "@nestjs/common";
export const RIGHTS_KEY = 'rights';
export const RIGHTS_KEY = "rights";
export const Rights = (...rights: string[]) => SetMetadata(RIGHTS_KEY, rights);

View File

@ -1,4 +1,4 @@
import { SetMetadata } from '@nestjs/common';
import { SetMetadata } from "@nestjs/common";
export const ROLES_KEY = 'roles';
export const ROLES_KEY = "roles";
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

View File

@ -3,33 +3,33 @@ import {
ExecutionContext,
Inject,
Injectable,
} from '@nestjs/common';
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';
} from "@nestjs/common";
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";
@Injectable()
export class TokenGuard implements CanActivate {
constructor(
private reflector: Reflector,
@Inject('@apihub24/token_service')
private readonly tokenService: tokenService.TokenService,
@Inject("@apihub24/token_service")
private readonly tokenService: tokenService.ITokenService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredRights = this.reflector.getAllAndOverride<string[]>(
RIGHTS_KEY,
[context.getHandler(), context.getClass()],
[context.getHandler(), context.getClass()]
);
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
[context.getHandler(), context.getClass()]
);
const requiredOrganizations = this.reflector.getAllAndOverride<string[]>(
ORGANIZATIONS_KEY,
[context.getHandler(), context.getClass()],
[context.getHandler(), context.getClass()]
);
if (!requiredRights && !requiredRoles && !requiredOrganizations) {
return true;
@ -40,8 +40,8 @@ export class TokenGuard implements CanActivate {
} = context.switchToHttp().getRequest();
if (
!authorization ||
typeof authorization !== 'string' ||
!authorization.includes('Bearer ')
typeof authorization !== "string" ||
!authorization.includes("Bearer ")
) {
return false;
}

View File

@ -1,5 +1,3 @@
export * from "./token-authentication.module";
export * from "./contracts/models/account";
export * from "./contracts/models/group";
export * from "./contracts/models/organization";
@ -28,3 +26,5 @@ export * from "./services/organization.service";
export * from "./services/registration.service";
export * from "./services/right.factory.service";
export * from "./services/right.service";
export * from "./token-authentication.module";

29
src/models/account.ts Normal file
View File

@ -0,0 +1,29 @@
import { IAccount } from "../contracts/models/account";
import { Group } from "./group";
import {
IsEmail,
IsNotEmpty,
MinLength,
ValidateNested,
} from "class-validator";
export class Account implements IAccount {
id: string;
@MinLength(3)
accountName: string;
@MinLength(3)
passwordHash: string;
@IsEmail()
email: string;
emailVerified: boolean;
active: boolean;
@IsNotEmpty({ each: true })
@ValidateNested({ each: true })
groups: Group[];
}

19
src/models/group.ts Normal file
View File

@ -0,0 +1,19 @@
import { IGroup } from "../contracts/models/group";
import { Organization } from "./organization";
import { Right } from "./right";
import { IsNotEmpty, MinLength, ValidateNested } from "class-validator";
export class Group implements IGroup {
id: string;
@MinLength(3)
name: string;
@IsNotEmpty()
@ValidateNested()
organization: Organization;
@IsNotEmpty({ each: true })
@ValidateNested({ each: true })
rights: Right[];
}

View File

@ -0,0 +1,10 @@
import { IsNotEmpty, MinLength } from "class-validator";
import { IOrganization } from "../contracts/models/organization";
export class Organization implements IOrganization {
id: string;
@IsNotEmpty()
@MinLength(3)
name: string;
}

View File

@ -0,0 +1,19 @@
import { IsEmail, IsNotEmpty, MinLength } from "class-validator";
import { IRegistration } from "../contracts/models/registration";
export class Registration implements IRegistration {
@MinLength(3)
accountName: string;
@MinLength(3)
password: string;
@MinLength(3)
passwordComparer: string;
@IsEmail()
email: string;
@IsNotEmpty({ each: true })
groupIds: string[];
}

9
src/models/right.ts Normal file
View File

@ -0,0 +1,9 @@
import { MinLength } from "class-validator";
import { IRight } from "../contracts/models/right";
export class Right implements IRight {
id: string;
@MinLength(3)
name: string;
}

13
src/models/session.ts Normal file
View File

@ -0,0 +1,13 @@
import { IsNotEmpty, ValidateNested } from "class-validator";
import { Account } from "./account";
import { ISession } from "../contracts/models/session";
export class Session implements ISession {
id: string;
@IsNotEmpty()
@ValidateNested()
account: Account;
metaData: Record<string, any>;
}

10
src/models/sign.in.ts Normal file
View File

@ -0,0 +1,10 @@
import { MinLength } from "class-validator";
import { ISignIn } from "../contracts/models/sign.in";
export class SignIn implements ISignIn {
@MinLength(3)
accountName: string;
@MinLength(3)
password: string;
}

View File

@ -1,20 +1,21 @@
import { Inject, Injectable } from "@nestjs/common";
import { validate } from "class-validator";
import { GroupService } from "./group.service";
import * as contracts from "../contracts";
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";
@Injectable()
export class AccountFactoryService {
constructor(
@Inject("@apihub24/password_service")
private readonly passwordService: contracts.PasswordService,
private readonly passwordService: passwordService.IPasswordService,
@Inject(GroupService)
private readonly groupService: GroupService
) {}
async createFromRegistration(
registration: contracts.Registration
): Promise<contracts.Account> {
async createFromRegistration(registration: IRegistration): Promise<IAccount> {
let validationErrors = await validate(registration);
if (validationErrors?.length) {
throw new Error(validationErrors[0].toString());
@ -22,7 +23,7 @@ export class AccountFactoryService {
const groups = await this.groupService.getBy((x) =>
registration.groupIds.includes(x.id)
);
const account = new contracts.Account();
const account = new Account();
account.accountName = registration.accountName;
account.email = registration.email;
account.emailVerified = false;

View File

@ -1,34 +1,34 @@
import { Account } from "../contracts/models/account";
import { Inject, Injectable } from "@nestjs/common";
import * as repository from "@apihub24/repository";
import { Group } from "../contracts";
import { Inject, Injectable } from "@nestjs/common";
import { IAccount } from "../contracts/models/account";
import { IGroup } from "../contracts/models/group";
@Injectable()
export class AccountService {
constructor(
@Inject("@apihub24/account_repository")
private readonly accountRepository: repository.Repository<Account>,
private readonly accountRepository: repository.IRepository<IAccount>,
@Inject("@apihub24/group_repository")
private readonly groupRepository: repository.Repository<Group>
private readonly groupRepository: repository.IRepository<IGroup>
) {}
async getBy(filter: (account: Account) => boolean): Promise<Account[]> {
async getBy(filter: (account: IAccount) => boolean): Promise<IAccount[]> {
return await this.accountRepository.getBy(filter);
}
async save(account: Account): Promise<Account | null> {
async save(account: IAccount): Promise<IAccount | null> {
const accounts = await this.accountRepository.save([account]);
return accounts?.length ? accounts[0] : null;
}
async delete(filter: (account: Account) => boolean): Promise<boolean> {
async delete(filter: (account: IAccount) => boolean): Promise<boolean> {
return await this.accountRepository.deleteBy(filter);
}
async addAccountToGroup(
accountId: string,
groupId: string
): Promise<Account> {
): Promise<IAccount> {
const [account, group] = await this.getAccountAndGroup(accountId, groupId);
account.groups = account.groups.filter((x) => x.id !== groupId);
account.groups.push(group);
@ -42,7 +42,7 @@ export class AccountService {
async removeAccountFromGroup(
accountId: string,
groupId: string
): Promise<Account> {
): Promise<IAccount> {
const [account] = await this.getAccountAndGroup(accountId, groupId);
account.groups = account.groups.filter((x) => x.id !== groupId);
const accountsSaved = await this.accountRepository.save([account]);
@ -55,7 +55,7 @@ export class AccountService {
private async getAccountAndGroup(
accountId: string,
groupId: string
): Promise<[Account, Group]> {
): Promise<[IAccount, IGroup]> {
const accounts = await this.accountRepository.getBy(
(x) => x.id === accountId
);

View File

@ -2,7 +2,8 @@ import { Inject, Injectable } from "@nestjs/common";
import { validate } from "class-validator";
import { OrganizationService } from "./organization.service";
import { RightService } from "./right.service";
import { Group } from "../contracts";
import { IGroup } from "../contracts/models/group";
import { Group } from "../models/group";
@Injectable()
export class GroupFactoryService {
@ -17,7 +18,7 @@ export class GroupFactoryService {
name: string,
organizationId: string,
rightIds: string[]
): Promise<Group> {
): Promise<IGroup> {
const rights = await this.rightService.getBy((x) =>
rightIds.includes(x.id)
);

View File

@ -1,27 +1,27 @@
import { Inject, Injectable } from "@nestjs/common";
import { Group } from "../contracts/models/group";
import * as repository from "@apihub24/repository";
import { Right } from "../contracts";
import { IGroup } from "../contracts/models/group";
import { IRight } from "../contracts/models/right";
@Injectable()
export class GroupService {
constructor(
@Inject("@apihub24/group_repository")
private readonly groupRepository: repository.Repository<Group>,
private readonly groupRepository: repository.IRepository<IGroup>,
@Inject("@apihub24/right_repository")
private readonly rightRepository: repository.Repository<Right>
private readonly rightRepository: repository.IRepository<IRight>
) {}
async getBy(filter: (group: Group) => boolean): Promise<Group[]> {
async getBy(filter: (group: IGroup) => boolean): Promise<IGroup[]> {
return await this.groupRepository.getBy(filter);
}
async save(group: Group): Promise<Group | null> {
async save(group: IGroup): Promise<IGroup | null> {
const groups = await this.groupRepository.save([group]);
return groups?.length ? groups[0] : null;
}
async delete(filter: (group: Group) => boolean): Promise<boolean> {
async delete(filter: (group: IGroup) => boolean): Promise<boolean> {
return this.groupRepository.deleteBy(filter);
}
@ -49,7 +49,7 @@ export class GroupService {
private async getGroupAndRight(
groupId: string,
rightId: string
): Promise<[Group, Right]> {
): Promise<[IGroup, IRight]> {
const groups = await this.groupRepository.getBy((x) => x.id === groupId);
if (!groups.length) {
throw new Error(`group with id ${groupId} not found`);

View File

@ -1,10 +0,0 @@
export * from "./account.factory.service";
export * from "./account.service";
export * from "./group.factory.service";
export * from "./group.service";
export * from "./login.service";
export * from "./organization.factory.service";
export * from "./organization.service";
export * from "./registration.service";
export * from "./right.factory.service";
export * from "./right.service";

View File

@ -1,21 +1,24 @@
import { Inject, Injectable } from "@nestjs/common";
import * as contracts from "../contracts";
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";
@Injectable()
export class LoginService {
constructor(
@Inject("@apihub24/token_service")
private readonly tokenService: contracts.TokenService,
private readonly tokenService: tokenService.ITokenService,
@Inject("@apihub24/password_service")
private readonly passwordService: contracts.PasswordService,
private readonly passwordService: passwordService.IPasswordService,
@Inject("@apihub24/session_service")
private readonly sessionService: contracts.SessionService,
private readonly sessionService: sessionService.ISessionService,
@Inject(AccountService)
private readonly accountService: AccountService
) {}
async signIn(signIn: contracts.SignIn): Promise<string> {
async signIn(signIn: ISignIn): Promise<string> {
const accounts = await this.accountService.getBy(
(x) =>
x.accountName === signIn.accountName &&

View File

@ -1,9 +1,10 @@
import { Injectable } from '@nestjs/common';
import { Organization } from '../contracts/models/organization';
import { Injectable } from "@nestjs/common";
import { IOrganization } from "../contracts/models/organization";
import { Organization } from "../models/organization";
@Injectable()
export class OrganizationFactoryService {
createFromName(name: string): Organization {
createFromName(name: string): IOrganization {
const organization = new Organization();
organization.name = name;
return organization;

View File

@ -1,21 +1,21 @@
import { Inject, Injectable } from '@nestjs/common';
import * as repository from '@apihub24/repository';
import { Organization } from '../contracts/models/organization';
import { Inject, Injectable } from "@nestjs/common";
import * as repository from "@apihub24/repository";
import { IOrganization } from "src/contracts/models/organization";
@Injectable()
export class OrganizationService {
constructor(
@Inject('@apihub24/organization_repository')
private readonly organizationRepository: repository.Repository<Organization>,
@Inject("@apihub24/organization_repository")
private readonly organizationRepository: repository.IRepository<IOrganization>
) {}
async getBy(
filter: (organization: Organization) => boolean,
): Promise<Organization[]> {
filter: (organization: IOrganization) => boolean
): Promise<IOrganization[]> {
return await this.organizationRepository.getBy(filter);
}
async save(organization: Organization): Promise<Organization> {
async save(organization: IOrganization): Promise<IOrganization> {
const savedOrganizations = await this.organizationRepository.save([
organization,
]);
@ -26,7 +26,7 @@ export class OrganizationService {
}
async deleteBy(
filter: (organization: Organization) => boolean,
filter: (organization: IOrganization) => boolean
): Promise<boolean> {
return await this.organizationRepository.deleteBy(filter);
}

View File

@ -1,9 +1,9 @@
import { Inject, Injectable } from "@nestjs/common";
import { Account } from "../contracts/models/account";
import { Registration } from "../contracts/models/registration";
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";
@Injectable()
export class RegistrationService {
@ -13,10 +13,10 @@ export class RegistrationService {
@Inject(AccountFactoryService)
private readonly accountFactory: AccountFactoryService,
@Inject("@apihub24/mail_verification_service")
private readonly mailVerificationService: mailVerificationService.MailVerificationService
private readonly mailVerificationService: mailVerificationService.IMailVerificationService
) {}
async registerAccount(registration: Registration): Promise<Account | null> {
async registerAccount(registration: IRegistration): Promise<IAccount | null> {
const newAccount = await this.accountFactory.createFromRegistration(
registration
);

View File

@ -1,10 +1,11 @@
import { Injectable } from "@nestjs/common";
import { Right } from "../contracts/models/right";
import { validate } from "class-validator";
import { IRight } from "../contracts/models/right";
import { Right } from "../models/right";
@Injectable()
export class RightFactoryService {
async createRight(name: string): Promise<Right> {
async createRight(name: string): Promise<IRight> {
const right = new Right();
right.name = name;
const validationErrors = await validate(right);

View File

@ -1,22 +1,22 @@
import { Right } from '../contracts/models/right';
import { Inject, Injectable } from '@nestjs/common';
import * as repository from '@apihub24/repository';
import { Inject, Injectable } from "@nestjs/common";
import * as repository from "@apihub24/repository";
import { IRight } from "../contracts/models/right";
@Injectable()
export class RightService {
constructor(
@Inject('@apihub24/right_repository')
private readonly rightRepository: repository.Repository<Right>,
@Inject("@apihub24/right_repository")
private readonly rightRepository: repository.IRepository<IRight>
) {}
async getBy(filter: (right: Right) => boolean): Promise<Right[]> {
async getBy(filter: (right: IRight) => boolean): Promise<IRight[]> {
return await this.rightRepository.getBy(filter);
}
async save(right: Right): Promise<Right | null> {
async save(right: IRight): Promise<IRight | null> {
const rights = await this.rightRepository.save([right]);
return rights?.length ? rights[0] : null;
}
async delete(filter: (right: Right) => boolean): Promise<boolean> {
async delete(filter: (right: IRight) => boolean): Promise<boolean> {
return await this.rightRepository.deleteBy(filter);
}
}