Skip to content

Commit

Permalink
feat(backend): init users endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Mati365 committed Sep 28, 2024
1 parent 8255436 commit f64a9ce
Show file tree
Hide file tree
Showing 24 changed files with 153 additions and 39 deletions.
10 changes: 5 additions & 5 deletions apps/admin/src/i18n/packs/i18n-lang-en.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type {
DecodeTokenFormatError,
NoTokensInStorageError,
SdkDecodeTokenFormatError,
SdkIncorrectUsernameOrPasswordError,
SdkInvalidJwtTokenError,
SdkNoTokensInStorageError,
SdkPayloadValidationError,
SdkRequestError,
SdkServerError,
Expand All @@ -11,8 +11,8 @@ import type {

export type SdkTranslatedErrors =
| SdkIncorrectUsernameOrPasswordError
| DecodeTokenFormatError
| NoTokensInStorageError
| SdkDecodeTokenFormatError
| SdkNoTokensInStorageError
| SdkPayloadValidationError
| SdkRequestError
| SdkServerError
Expand All @@ -21,7 +21,7 @@ export type SdkTranslatedErrors =

const I18N_SDK_ERRORS_EN: Record<SdkTranslatedErrors['tag'], string> = {
SdkIncorrectUsernameOrPasswordError: 'Incorrect email or password',
DecodeTokenFormatError: 'Token format is incorrect',
SdkDecodeTokenFormatError: 'Token format is incorrect',
SdkPayloadValidationError: 'Payload validation error',
SdkRequestError: 'Request error',
SdkServerError: 'Server error',
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/src/i18n/packs/i18n-lang-pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { I18nLangPack } from './i18n-packs';

const I18N_SDK_ERRORS_PL: Record<SdkTranslatedErrors['tag'], string> = {
SdkIncorrectUsernameOrPasswordError: 'Nieprawidłowy adres e-mail lub hasło',
DecodeTokenFormatError: 'Nieprawidłowy format tokenu',
SdkDecodeTokenFormatError: 'Nieprawidłowy format tokenu',
SdkPayloadValidationError: 'Błąd walidacji danych',
SdkRequestError: 'Błąd żądania',
SdkServerError: 'Błąd serwera',
Expand Down
8 changes: 7 additions & 1 deletion apps/backend/src/modules/api/controllers/root.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AuthController } from './auth.controller';
import { HealthCheckController } from './health-check.controller';
import { OrganizationsController } from './organizations.controller';
import { BaseController } from './shared';
import { UsersController } from './users.controller';

@injectable()
export class RootApiController extends BaseController {
Expand All @@ -17,14 +18,18 @@ export class RootApiController extends BaseController {
@inject(HealthCheckController) healthCheck: HealthCheckController,
@inject(AuthController) auth: AuthController,
@inject(OrganizationsController) organizations: OrganizationsController,
@inject(UsersController) users: UsersController,
) {
super();

const { config, isEnv } = configService;
const corsOrigins = (
isEnv('dev')
? '*'
: [`https://www.${config.endUserDomain}`, `https://${config.endUserDomain}`]
: [
`https://www.${config.endUserDomain}`,
`https://${config.endUserDomain}`,
]
);

this.router
Expand All @@ -42,6 +47,7 @@ export class RootApiController extends BaseController {
.route('/health-check', healthCheck.router)
.route('/auth', auth.router)
.route('/organizations', organizations.router)
.route('/users', users.router)
.all('*', notFoundMiddleware);
}
}
41 changes: 41 additions & 0 deletions apps/backend/src/modules/api/controllers/users.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { pipe } from 'fp-ts/lib/function';
import { inject, injectable } from 'tsyringe';

import {
SdkCreateUserInputV,
type UsersSdk,
} from '@llm/sdk';
import { ConfigService } from '~/modules/config';
import { UsersService } from '~/modules/users';

import {
mapDbRecordAlreadyExistsToSdkError,
rejectUnsafeSdkErrors,
sdkSchemaValidator,
serializeSdkResponseTE,
} from '../helpers';
import { AuthorizedController } from './shared/authorized.controller';

@injectable()
export class UsersController extends AuthorizedController {
constructor(
@inject(ConfigService) configService: ConfigService,
@inject(UsersService) usersService: UsersService,
) {
super(configService);

this.router
.post(
'/',
sdkSchemaValidator('json', SdkCreateUserInputV),
async context => pipe(
usersService.asUser(context.var.jwt).create({
value: context.req.valid('json'),
}),
mapDbRecordAlreadyExistsToSdkError,
rejectUnsafeSdkErrors,
serializeSdkResponseTE<ReturnType<UsersSdk['create']>>(context),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export function mapTagToSdkError<const ET extends string, RET extends TaggedErro
) {
const logger = LoggerService.of('mapTagToSdkError');

return <T, E extends TaggedError<string, any>>(task: TE.TaskEither<E, T>) => pipe(
return <T, E extends TaggedError<string, any>>(
task: Extract<E, TaggedError<ET>> extends never ? never : TE.TaskEither<E, T>,
) => pipe(
task,
TE.mapLeft((error) => {
if (isSdkTaggedError(error) || error.tag !== catchTag) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import type { TaggedError } from '@llm/commons';

export function respondWithTaggedError(
context: Context,
result: TaggedError<string>,
fallbackCode: number = 500,
) {
return context.json(
return (result: TaggedError<string>) => context.json(
{
error: result.serialize(),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import type { Env, MiddlewareHandler, ValidationTargets } from 'hono';
import type { z } from 'zod';

import { either as E } from 'fp-ts';
import { pipe } from 'fp-ts/function';
import { validator } from 'hono/validator';

import type { SdkErrorResponseT } from '@llm/sdk';

import { tryParseUsingZodSchema } from '@llm/commons';
import { SdkInvalidRequestError } from '@llm/sdk';

import { respondWithTaggedError } from './respond-with-tagged-error';

export function sdkSchemaValidator<
T extends z.ZodFirstPartySchemaTypes,
Expand All @@ -24,15 +26,14 @@ export function sdkSchemaValidator<
>(target: Target, schema: T): MiddlewareHandler<E, P, V> {
const parser = tryParseUsingZodSchema(schema);

return validator(target, (value, c) => {
return validator(target, (value, context) => {
const result = parser(value);

if (E.isLeft(result)) {
const response: SdkErrorResponseT = {
error: result.left.serialize(),
};

return c.json(response, 400);
return pipe(
new SdkInvalidRequestError(result.left.context),
respondWithTaggedError(context),
);
}

return result.right as any;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function serializeSdkResponseTE<T extends TE.TaskEither<TaggedError<`Sdk$
const result = await task();

if (E.isLeft(result)) {
return respondWithTaggedError(context, result.left);
return respondWithTaggedError(context)(result.left);
}

return context.json({
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/modules/api/middlewares/jwt.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function jwtMiddleware(jwtSecret: string) {
const decodeResult = tryVerifyAndDecodeToken(jwtSecret, token ?? '');

if (E.isLeft(decodeResult)) {
return respondWithTaggedError(context, decodeResult.left, 401);
return respondWithTaggedError(context, 401)(decodeResult.left);
}

context.set('jwt', decodeResult.right);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TaggedError } from '@llm/commons';

export class DatabaseRecordAlreadyExists extends TaggedError.ofLiteral()('DatabaseRecordAlreadyExists') {
readonly httpCode = 403;
readonly httpCode = 409;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export class OrganizationsService implements WithAuthFirewall<OrganizationsFirew
@inject(OrganizationsEsIndexRepo) private readonly esIndexRepo: OrganizationsEsIndexRepo,
) {}

asUser = (jwt: SdkJwtTokenT) => new OrganizationsFirewall(jwt, this);

search = this.esSearchRepo.search;

create = (value: SdkCreateOrganizationInputT) => pipe(
Expand All @@ -35,6 +37,4 @@ export class OrganizationsService implements WithAuthFirewall<OrganizationsFirew
this.repo.update({ id, value }),
TE.tap(() => this.esIndexRepo.findAndIndexDocumentById(id)),
);

asUser = (jwt: SdkJwtTokenT) => new OrganizationsFirewall(jwt, this);
}
26 changes: 26 additions & 0 deletions apps/backend/src/modules/users/users.firewall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { flow } from 'fp-ts/function';

import type { SdkJwtTokenT } from '@llm/sdk';

import { AuthFirewallService } from '~/modules/auth/firewall';

import type { UsersService } from './users.service';

export class UsersFirewall extends AuthFirewallService {
constructor(
jwt: SdkJwtTokenT,
private readonly usersService: UsersService,
) {
super(jwt);
}

create = flow(
this.usersService.create,
this.tryTEIfUser.is.root,
);

createIfNotExists = flow(
this.usersService.createIfNotExists,
this.tryTEIfUser.is.root,
);
}
14 changes: 13 additions & 1 deletion apps/backend/src/modules/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,28 @@ import { taskEither as TE } from 'fp-ts';
import { flow } from 'fp-ts/function';
import { inject, injectable } from 'tsyringe';

import type { SdkJwtTokenT } from '@llm/sdk';

import type { WithAuthFirewall } from '../auth';

import { UsersEsIndexRepo } from './elasticsearch/users-es-index.repo';
import { UsersFirewall } from './users.firewall';
import { UsersRepo } from './users.repo';

@injectable()
export class UsersService {
export class UsersService implements WithAuthFirewall<UsersFirewall> {
constructor(
@inject(UsersRepo) private readonly usersRepo: UsersRepo,
@inject(UsersEsIndexRepo) private readonly usersEsIndexRepo: UsersEsIndexRepo,
) {}

asUser = (jwt: SdkJwtTokenT) => new UsersFirewall(jwt, this);

create = flow(
this.usersRepo.create,
TE.tap(({ id }) => this.usersEsIndexRepo.findAndIndexDocumentById(id)),
);

createIfNotExists = flow(
this.usersRepo.createIfNotExists,
TE.tap(({ id, created }) =>
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/src/modules/abstract-nested-sdk-with-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export abstract class AbstractNestedSdkWithAuth<
get authAsyncFetcher() {
return this.config.authAsyncFetcher;
}

fetch = this.authAsyncFetcher.fetchWithAuthTokenTE;
}
7 changes: 3 additions & 4 deletions packages/sdk/src/modules/auth/auth-async-fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { type either as E, taskEither as TE } from 'fp-ts';
import { pipe } from 'fp-ts/function';

import { type TaggedError, tapTaskEither, type ValidationError } from '@llm/commons';
import { type TaggedError, tapTaskEither } from '@llm/commons';
import { type APIRequestAttrs, performApiRequest, type SdkApiRequestErrors } from '~/shared';

import type { AuthSdk } from './auth.sdk';
import type { SdkJwtTokensPairT } from './dto';
import type { DecodeTokenFormatError } from './helpers';
import type { NoTokensInStorageError } from './storage';
import type { SdkNoTokensInStorageError } from './storage';

import { shouldRefreshToken } from './helpers';

Expand Down Expand Up @@ -40,7 +39,7 @@ export class AuthAsyncFetcher {
TE.bindW('shouldRefresh', ({ tokens }) =>
TE.fromEither(shouldRefreshToken(tokens.token))),
TE.chain(({ tokens, shouldRefresh }): TE.TaskEither<
NoTokensInStorageError | DecodeTokenFormatError | ValidationError | SdkApiRequestErrors,
SdkNoTokensInStorageError | SdkApiRequestErrors,
SdkJwtTokensPairT
> => {
if (!shouldRefresh) {
Expand Down
5 changes: 3 additions & 2 deletions packages/sdk/src/modules/auth/helpers/try-decode-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ export function tryDecodeToken(token: string) {
return pipe(
E.tryCatch(
() => jwtDecode(token),
(err: any) => new DecodeTokenFormatError(err),
(err: any) => new SdkDecodeTokenFormatError(err),
),
E.chainW(tryParseUsingZodSchema(SdkJwtTokenV)),
E.mapLeft(err => new SdkDecodeTokenFormatError(err)),
);
}

export class DecodeTokenFormatError extends TaggedError.ofLiteral<Error>()('DecodeTokenFormatError') {}
export class SdkDecodeTokenFormatError extends TaggedError.ofLiteral<Error>()('SdkDecodeTokenFormatError') {}
6 changes: 3 additions & 3 deletions packages/sdk/src/modules/auth/storage/tokens-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ export abstract class TokensStorage {
getSessionTokensTE() {
return pipe(
this.getSessionTokens(),
TE.fromOption(() => new NoTokensInStorageError({})),
TE.fromOption(() => new SdkNoTokensInStorageError({})),
);
}

getDecodedToken() {
return pipe(
this.getSessionTokens(),
E.fromOption(() => new NoTokensInStorageError({})),
E.fromOption(() => new SdkNoTokensInStorageError({})),
E.chain(({ token }) => tryDecodeToken(token)),
);
}
Expand All @@ -49,4 +49,4 @@ export abstract class TokensStorage {
}
}

export class NoTokensInStorageError extends TaggedError.ofLiteral<any>()('DecodeTokenFormatError') {}
export class SdkNoTokensInStorageError extends TaggedError.ofLiteral<any>()('SdkDecodeTokenFormatError') {}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { AbstractNestedSdkWithAuth } from '~/modules/abstract-nested-sdk-with-auth';
import {
getPayload,
performApiRequest,
postPayload,
putPayload,
type SdkRecordAlreadyExistsError,
Expand All @@ -22,20 +21,20 @@ export class OrganizationsSdk extends AbstractNestedSdkWithAuth {
protected endpointPrefix = '/dashboard/organizations';

search = (data: SdKSearchOrganizationsInputT) =>
performApiRequest<SdKSearchOrganizationsOutputT>({
this.fetch<SdKSearchOrganizationsOutputT>({
url: this.endpoint('/search'),
query: data,
options: getPayload(),
});

create = (data: SdkCreateOrganizationInputT) =>
performApiRequest<SdkCreateOrganizationOutputT, SdkRecordAlreadyExistsError>({
this.fetch<SdkCreateOrganizationOutputT, SdkRecordAlreadyExistsError>({
url: this.endpoint('/'),
options: postPayload(data),
});

update = ({ id, ...data }: SdkUpdateOrganizationInputT & SdkTableRowWithIdT) =>
performApiRequest<
this.fetch<
SdkUpdateOrganizationOutputT,
SdkRecordAlreadyExistsError | SdkRecordNotFoundError
>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ export const SdkCreateUserInputV = z.object({
);

export type SdkCreateUserInputT = z.infer<typeof SdkCreateUserInputV>;

export const SdkCreateUserOutputV = SdkTableRowWithIdV;

export type SdkCreateUserOutputT = z.infer<typeof SdkCreateUserOutputV>;
9 changes: 9 additions & 0 deletions packages/sdk/src/modules/dashboard/users/users.sdk.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { AbstractNestedSdkWithAuth } from '~/modules/abstract-nested-sdk-with-auth';
import { postPayload, type SdkRecordAlreadyExistsError } from '~/shared';

import type { SdkCreateUserInputT, SdkCreateUserOutputT } from './dto';

export class UsersSdk extends AbstractNestedSdkWithAuth {
protected endpointPrefix = '/dashboard/users';

create = (data: SdkCreateUserInputT) =>
this.fetch<SdkCreateUserOutputT, SdkRecordAlreadyExistsError>({
url: this.endpoint('/'),
options: postPayload(data),
});
};
Loading

0 comments on commit f64a9ce

Please sign in to comment.