diff --git a/.gitignore b/.gitignore index 53992de..6ff4ab2 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,6 @@ dist .tern-port .npmrc +.database/ +.idea/ +repos/ \ No newline at end of file diff --git a/package.json b/package.json index 7c8f48c..658676c 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "pridepack" ], "devDependencies": { + "@types/bcrypt": "^5.0.0", "@types/node": "^17.0.25", "eslint": "^8.13.0", "eslint-config-lxsmnsyc": "^0.4.0", @@ -57,9 +58,12 @@ "vitest": "^0.9.4" }, "dependencies": { + "@prisma/client": "^3.14.0", "@theoryofnekomata/uuid-buffer": "^0.1.0", + "bcrypt": "^5.0.1", "fastify": "^3.27.0", - "fastify-plugin": "^3.0.1" + "fastify-plugin": "^3.0.1", + "prisma": "^3.14.0" }, "scripts": { "prepublishOnly": "pridepack clean && pridepack build", diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..a5df17e --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,83 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model User { + id Bytes @id + username String @unique + password String + createdAt DateTime @default(now()) + properties Property[] + userOrgs UserOrg[] + sessions Session[] + createdOrgs Org[] +} + +model Property { + id Bytes @id + userId Bytes + user User @relation(fields: [userId], references: [id]) + name String + value String +} + +model Org { + id Bytes @id + name String @unique + description String + createdAt DateTime @default(now()) + creatorUserId Bytes + creatorUser User @relation(fields: [creatorUserId], references: [id]) + orgUsers UserOrg[] +} + +model UserOrg { + id Int @id @default(autoincrement()) + userId Bytes + user User @relation(fields: [userId], references: [id]) + orgId Bytes + org Org @relation(fields: [orgId], references: [id]) +} + +model Repo { + id Bytes @id + name String + visibility String + ownerId Bytes + ownerType String + createdAt DateTime @default(now()) +} + +model Log { + id Int @id @default(autoincrement()) + subjectUserId Bytes + subjectUsername String + action String + createdAt DateTime @default(now()) + parameters LogParameter[] +} + +model LogParameter { + id Int @id @default(autoincrement()) + logId Int + log Log @relation(fields: [logId], references: [id]) + key String + value String +} + +model Session { + id Bytes @id + createdAt DateTime + validUntil DateTime + + userId Bytes + user User @relation(fields: [userId], references: [id]) +} diff --git a/scripts/create-default-user.ts b/scripts/create-default-user.ts new file mode 100644 index 0000000..3157af6 --- /dev/null +++ b/scripts/create-default-user.ts @@ -0,0 +1,14 @@ +import { Uuid } from '@theoryofnekomata/uuid-buffer'; +import { UserService, UserServiceImpl } from '../src/modules/user' + +const main = async () => { + const userService: UserService = new UserServiceImpl() + const newUser = await userService.create({ + username: 'username', + password: 'password', + }) + + console.log(Uuid.from(newUser.id).toString()); +} + +void main() diff --git a/src/index.ts b/src/index.ts index 006d38b..1679e09 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,9 @@ import { createServer } from './server'; import { addRoutes } from './routes'; -const server = createServer(); +const server = createServer({ + logger: true, +}); addRoutes(server); server.listen(process.env.PORT ?? 8080, '0.0.0.0', (err) => { diff --git a/src/modules/auth/Auth.controller.ts b/src/modules/auth/Auth.controller.ts new file mode 100644 index 0000000..981170c --- /dev/null +++ b/src/modules/auth/Auth.controller.ts @@ -0,0 +1,63 @@ +import { RouteHandlerMethod } from 'fastify' +import { Uuid } from '@theoryofnekomata/uuid-buffer' +import { Controller } from 'src/packages/fastify-utils-theoryofnekomata' +import { UserService, UserServiceImpl } from 'src/modules/user' +import { LoginUserFormData } from 'src/modules/auth/models' +import { SessionService, SessionServiceImpl } from 'src/modules/auth/Session.service' +import { + LoggedOutData, + LoggedInData, + UnableToLogInError, + UnableToLogOutError, +} from 'src/modules/auth/responses' + +export interface AuthController extends Controller<'logIn' | 'logOut'> {} + +export class AuthControllerImpl implements AuthController { + private readonly sessionService: SessionService + private readonly userService: UserService + + constructor() { + this.sessionService = new SessionServiceImpl() + this.userService = new UserServiceImpl() + } + + readonly logIn: RouteHandlerMethod = async (request, reply) => { + const { username, password } = request.body as LoginUserFormData + + try { + const existingUser = await this.userService.getFromCredentials({ + username, + password, + }) + + const newSession = await this.sessionService.create({ + userId: existingUser.id, + }) + + reply.sendData(new LoggedInData(newSession)) + } catch (causeRaw) { + const cause = causeRaw as Error + throw new UnableToLogInError( + 'Authorization could not be performed on the credentials provided. Either try again later or ensure registration using the previous credentials.', + { cause }, + ) + } + } + + readonly logOut: RouteHandlerMethod = async (request, reply) => { + const sessionId = request.session?.id; + if (sessionId) { + try { + await this.sessionService.expire(Uuid.from(sessionId)) + reply.sendData(new LoggedOutData()) + return + } catch { + // noop + } + } + + throw new UnableToLogOutError( + 'De-authorization could not be performed. Ensure session data is present and/or clear local data to carry out the operation.') + } +} diff --git a/src/modules/auth/Password.service.ts b/src/modules/auth/Password.service.ts new file mode 100644 index 0000000..98c91df --- /dev/null +++ b/src/modules/auth/Password.service.ts @@ -0,0 +1,29 @@ +import { hash, genSalt, compare } from 'bcrypt' +import { AppError } from 'src/packages/fastify-compliant-http-errors' + +export class PasswordNotEqualAssertError extends AppError { +} + +export interface PasswordService { + hash(password: string): Promise; + + assertEqual(password: string, hashedPassword: string): Promise; +} + +export class PasswordServiceImpl implements PasswordService { + constructor(private readonly saltRounds = 12) { + } + + async hash(password: string): Promise { + const salt = await genSalt(this.saltRounds) + return hash(password, salt) + } + + async assertEqual(password: string, hashedPassword: string): Promise { + const result = await compare(password, hashedPassword) + if (result) { + return + } + throw new PasswordNotEqualAssertError() + } +} diff --git a/src/modules/auth/Session.service.ts b/src/modules/auth/Session.service.ts new file mode 100644 index 0000000..3e2d638 --- /dev/null +++ b/src/modules/auth/Session.service.ts @@ -0,0 +1,120 @@ +import { PrismaClient } from '@prisma/client' +import { CreateSessionData, Session } from 'src/modules/auth/models' +import { AppError } from 'src/packages/fastify-compliant-http-errors' +import {Uuid} from '@theoryofnekomata/uuid-buffer'; +import {notFoundFactory} from 'src/packages/prisma-error-utils'; +import {User} from '../user'; + +type SessionWithUsername = Session & { user: Pick }; + +export interface SessionService { + create(data: CreateSessionData): Promise + + expire(sessionId: Session['id']): Promise + + isValid(sessionId: Session['id']): Promise + + get(id: Session['id']): Promise +} + +export class SessionNotFoundError extends AppError {} + +export class SessionServiceImpl implements SessionService { + private readonly prismaClient: PrismaClient + + constructor() { + this.prismaClient = new PrismaClient() + } + + private static serialize(raw: T): T { + return { + ...raw, + id: Uuid.from(raw.id), + userId: Uuid.from(raw.userId) + }; + } + + async create(data: CreateSessionData): Promise { + const createdAt = new Date() + const validUntil = new Date(createdAt) + validUntil.setDate(validUntil.getDate() + 1) + const raw = await this.prismaClient.session.create({ + data: { + ...data, + id: Uuid.v4(), + createdAt, + validUntil, + }, + include: { + user: { + select: { + id: false, + username: true, + password: false, + }, + }, + }, + }) + + return SessionServiceImpl.serialize(raw); + } + + async expire(sessionId: Session['id']): Promise { + const existingSession = await this.prismaClient.session.findUnique({ + where: { + id: sessionId, + }, + rejectOnNotFound: notFoundFactory(SessionNotFoundError), + }) + const raw = await this.prismaClient.session.update({ + where: { + id: existingSession.id, + }, + data: { + validUntil: new Date(), + }, + }) + + return SessionServiceImpl.serialize(raw); + } + + async isValid(sessionId: Session['id']): Promise { + const data = await this.prismaClient.session.findUnique({ + where: { + id: sessionId, + }, + }) + + if (!data) { + return false + } + const now = new Date() + return ( + data.createdAt <= now + && now < data.validUntil + ) + } + + async get(id: Session['id']): Promise { + const raw = await this.prismaClient.session.findUnique({ + where: { + id, + }, + include: { + user: { + select: { + id: false, + username: true, + password: false, + } + } + } + }) + + if (raw) { + return SessionServiceImpl.serialize(raw); + } + + return null; + } +} diff --git a/src/modules/auth/index.ts b/src/modules/auth/index.ts new file mode 100644 index 0000000..f6d9d32 --- /dev/null +++ b/src/modules/auth/index.ts @@ -0,0 +1,5 @@ +export * from './Auth.controller' +export * from './models' +export * from './Password.service' +export * from './Session.service' +export * from './responses' diff --git a/src/modules/auth/models.ts b/src/modules/auth/models.ts new file mode 100644 index 0000000..94d21a3 --- /dev/null +++ b/src/modules/auth/models.ts @@ -0,0 +1,14 @@ +export interface Session { + id: Buffer + createdAt: Date + validUntil: Date + userId: Buffer +} + +export interface CreateSessionData extends Omit { +} + +export interface LoginUserFormData { + username: string; + password: string; +} diff --git a/src/modules/auth/responses.ts b/src/modules/auth/responses.ts new file mode 100644 index 0000000..368ff82 --- /dev/null +++ b/src/modules/auth/responses.ts @@ -0,0 +1,16 @@ +import { HttpResponse } from 'src/packages/fastify-send-data' +import { Session } from 'src/modules/auth/models' +import { constants } from 'http2' +import { HttpError } from 'src/packages/fastify-compliant-http-errors' + +export class LoggedInData extends HttpResponse(constants.HTTP_STATUS_OK, 'Logged In') { +} + +export class LoggedOutData extends HttpResponse(constants.HTTP_STATUS_NO_CONTENT, 'Logged Out') { +} + +export class UnableToLogInError extends HttpError(constants.HTTP_STATUS_UNAUTHORIZED, 'Unable to Log In') { +} + +export class UnableToLogOutError extends HttpError(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Unable to Log Out') { +} diff --git a/src/modules/auth/routes.ts b/src/modules/auth/routes.ts new file mode 100644 index 0000000..2cd12d6 --- /dev/null +++ b/src/modules/auth/routes.ts @@ -0,0 +1,18 @@ +import {ServerInstance} from '../../packages/fastify-utils-theoryofnekomata'; +import {AuthController, AuthControllerImpl} from './Auth.controller'; + +export const addRoutes = (server: ServerInstance) => { + const authController: AuthController = new AuthControllerImpl() + + server.route({ + method: 'POST', + url: '/api/auth/log-in', + handler: authController.logIn, + }); + + server.route({ + method: 'POST', + url: '/api/auth/log-out', + handler: authController.logOut, + }); +} diff --git a/src/modules/git/Git.controller.ts b/src/modules/git/Git.controller.ts deleted file mode 100644 index 7d77b78..0000000 --- a/src/modules/git/Git.controller.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {GitService, GitServiceImpl} from './Git.service'; -import {RouteHandlerMethod} from 'fastify'; -import {constants} from 'http2'; -import * as codeCore from '@modal/code-core'; -import {HttpResponse} from '../../packages/fastify-send-data'; -import Controller from '../../utils/types'; -import {Uuid} from '@theoryofnekomata/uuid-buffer'; - -export interface GitController extends Controller< - 'createRepo' - | 'deleteRepo' -> {} - -class RepoCreatedResponse extends HttpResponse(constants.HTTP_STATUS_CREATED, 'Repo Created') {} -class RepoDeletedResponse extends HttpResponse(constants.HTTP_STATUS_NO_CONTENT, 'Repo Deleted') {} - -export class GitControllerImpl implements GitController { - private readonly gitService: GitService; - - constructor() { - this.gitService = new GitServiceImpl() - } - - readonly createRepo: RouteHandlerMethod = async (request, reply) => { - const repo = await this.gitService.createRepo(request.body as codeCore.git.CreateRepoData, request.user); - reply.sendData(new RepoCreatedResponse(repo)); - } - - readonly deleteRepo: RouteHandlerMethod = async (request, reply) => { - const params = request.params as { repoId: string } - await this.gitService.deleteRepo(Uuid.from(params.repoId), request.user) - reply.sendData(new RepoDeletedResponse()); - } -} diff --git a/src/modules/git/Git.service.ts b/src/modules/git/Git.service.ts deleted file mode 100644 index 92725e1..0000000 --- a/src/modules/git/Git.service.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {constants} from 'http2'; -import * as codeCore from '@modal/code-core'; -import {HttpError} from '../../packages/fastify-compliant-http-errors'; - -export interface GitService { - createRepo(options: codeCore.git.CreateRepoData, user?: codeCore.common.User): Promise; - deleteRepo(repoId: codeCore.git.Repo['id'], user?: codeCore.common.User): Promise -} - -export class UnauthorizedToCreateRepoError extends HttpError(constants.HTTP_STATUS_UNAUTHORIZED, 'Unauthorized to Create Repo') {} -export class UnauthorizedToDeleteRepoError extends HttpError(constants.HTTP_STATUS_UNAUTHORIZED, 'Unauthorized to Delete Repo') {} -export class UnableToCreateRepoError extends HttpError(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Unable to Create Repo') {} -export class UnableToDeleteRepoError extends HttpError(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Unable to Delete Repo') {} - -export class GitServiceImpl implements GitService { - private readonly coreGitService: codeCore.git.GitService; - - constructor() { - this.coreGitService = new codeCore.git.GitServiceImpl(); - } - - async createRepo(options: codeCore.git.CreateRepoData, user?: codeCore.common.User): Promise { - if (user) { - let newRepo: codeCore.git.Repo; - try { - newRepo = await this.coreGitService.create(options, user); - } catch (causeRaw) { - const cause = causeRaw as Error; - throw new UnableToCreateRepoError('Something went wrong while creating the repo.', { cause, }); - } - return newRepo; - } - throw new UnauthorizedToCreateRepoError('Could not create repo with insufficient authorization.'); - } - - async deleteRepo(repoId: codeCore.git.Repo['id'], user?: codeCore.common.User): Promise { - if (user) { - try { - await this.coreGitService.delete(repoId, user); - } catch (causeRaw) { - const cause = causeRaw as Error; - throw new UnableToDeleteRepoError('Something went wrong while deleting the repo.', { cause, }); - } - return; - } - throw new UnauthorizedToDeleteRepoError('Could not delete repo with insufficient authorization.'); - } -} diff --git a/src/modules/git/index.ts b/src/modules/git/index.ts deleted file mode 100644 index f18cf7d..0000000 --- a/src/modules/git/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Git.service' -export * from './Git.controller' -export * from './models' diff --git a/src/modules/git/models.ts b/src/modules/git/models.ts deleted file mode 100644 index f16b5a1..0000000 --- a/src/modules/git/models.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as codeCore from '@modal/code-core' - -export type CreateRepoOptions = codeCore.git.CreateOptions diff --git a/src/modules/log/Log.service.ts b/src/modules/log/Log.service.ts new file mode 100644 index 0000000..5d3183c --- /dev/null +++ b/src/modules/log/Log.service.ts @@ -0,0 +1,42 @@ +import { Prisma, PrismaClient } from '@prisma/client'; +import {Uuid} from '@theoryofnekomata/uuid-buffer'; +import {User} from '../user'; +import {Log} from './models'; + +export interface LogService { + create(subject: User, action: string, parameters?: Record): Promise +} + +export class LogServiceImpl implements LogService { + private readonly prismaClient: PrismaClient + + constructor() { + this.prismaClient = new PrismaClient() + } + + async create(subject: User, action: string, parameters?: Record): Promise { + const createData: Prisma.LogCreateInput = { + subjectUsername: subject.username, + subjectUserId: subject.id, + action, + } + + if (parameters) { + createData['parameters'] = { + create: Object.entries(parameters).map(([key, value]) => ({ + key, + value: `${value}`, + })), + } + } + + const raw = await this.prismaClient.log.create({ + data: createData, + }) + + return { + ...raw, + subjectUserId: Uuid.from(raw.subjectUserId), + } + } +} diff --git a/src/modules/log/index.ts b/src/modules/log/index.ts new file mode 100644 index 0000000..297cf31 --- /dev/null +++ b/src/modules/log/index.ts @@ -0,0 +1,2 @@ +export * from './Log.service' +export * from './models' diff --git a/src/modules/log/models.ts b/src/modules/log/models.ts new file mode 100644 index 0000000..c11b664 --- /dev/null +++ b/src/modules/log/models.ts @@ -0,0 +1,7 @@ +export type Log = { + id: number, + subjectUserId: Buffer, + subjectUsername: string, + action: string, + createdAt: Date, +} diff --git a/src/modules/org/Org.service.ts b/src/modules/org/Org.service.ts new file mode 100644 index 0000000..1bc3783 --- /dev/null +++ b/src/modules/org/Org.service.ts @@ -0,0 +1,36 @@ +import {PrismaClient} from '@prisma/client'; +import {Org} from './models'; +import {Uuid} from '@theoryofnekomata/uuid-buffer'; +import {notFoundFactory} from '../../packages/prisma-error-utils'; +import {OrgNotFoundError} from './responses'; + +export interface OrgService { + getFromId(orgId: Org['id']): Promise; +} + +export class OrgServiceImpl { + private readonly prismaClient: PrismaClient; + + constructor() { + this.prismaClient = new PrismaClient(); + } + + private static serialize(org: T): T { + return { + ...org, + id: Uuid.from(org.id), + creatorUserId: Uuid.from(org.creatorUserId), + }; + } + + async getFromId(id: Org['id']): Promise { + const org = await this.prismaClient.org.findUnique({ + where: { + id, + }, + rejectOnNotFound: notFoundFactory(OrgNotFoundError), + }) + + return OrgServiceImpl.serialize(org); + } +} diff --git a/src/modules/org/index.ts b/src/modules/org/index.ts new file mode 100644 index 0000000..0fea659 --- /dev/null +++ b/src/modules/org/index.ts @@ -0,0 +1,3 @@ +export * from './models'; +export * from './Org.service'; +export * from './responses'; diff --git a/src/modules/org/models.ts b/src/modules/org/models.ts new file mode 100644 index 0000000..b7be81d --- /dev/null +++ b/src/modules/org/models.ts @@ -0,0 +1,6 @@ +export type Org = { + id: Buffer, + name: string, + description: string, + creatorUserId: Buffer, +} diff --git a/src/modules/org/responses.ts b/src/modules/org/responses.ts new file mode 100644 index 0000000..60f2b9f --- /dev/null +++ b/src/modules/org/responses.ts @@ -0,0 +1,4 @@ +import { constants } from 'http2' +import {HttpError} from 'src/packages/fastify-compliant-http-errors'; + +export class OrgNotFoundError extends HttpError(constants.HTTP_STATUS_NOT_FOUND, 'Org Not Found') {} diff --git a/src/modules/owner/Owner.service.ts b/src/modules/owner/Owner.service.ts new file mode 100644 index 0000000..19a9fa2 --- /dev/null +++ b/src/modules/owner/Owner.service.ts @@ -0,0 +1,73 @@ +import {Uuid} from '@theoryofnekomata/uuid-buffer'; +import {OwnerType} from './models'; +import {UnknownOwnerTypeError} from './responses'; +import {UserNotFoundError, UserService, UserServiceImpl} from '../user'; +import {OrgNotFoundError, OrgService, OrgServiceImpl} from '../org'; + +export interface OwnerService { + getOwnerName(id: Uuid, type: string): Promise; + getOwnerTypeDirName(type: string): string; +} + +export class OwnerServiceImpl implements OwnerService { + private readonly userService: UserService; + private readonly orgService: OrgService; + private readonly OWNER_TYPE_DIR_NAME: Record; + + constructor() { + this.userService = new UserServiceImpl(); + this.orgService = new OrgServiceImpl(); + this.OWNER_TYPE_DIR_NAME = { + [OwnerType.USER]: 'users', + [OwnerType.ORG]: 'orgs', + }; + } + + private async getOwnerNameFromUser(ownerId: Uuid) { + try { + const user = await this.userService.getFromId(ownerId); + return user.username; + } catch (err) { + if (!(err instanceof UserNotFoundError)) { + throw err; + } + } + + return ownerId.toString(); + } + + private async getOwnerNameFromOrg(ownerId: Uuid) { + try { + const org = await this.orgService.getFromId(ownerId); + return org.name; + } catch (err) { + if (!(err instanceof OrgNotFoundError)) { + throw err; + } + } + + return ownerId.toString(); + } + + async getOwnerName(ownerId: Uuid, ownerType: string) { + if (ownerType === OwnerType.USER) { + return this.getOwnerNameFromUser(ownerId); + } + + if (ownerType === OwnerType.ORG) { + return this.getOwnerNameFromOrg(ownerId); + } + + throw new UnknownOwnerTypeError(`Only valid options are: ${Object.values(OwnerType).join(', ')}`); + } + + getOwnerTypeDirName(type: string): string { + const { [type as OwnerType]: dirName } = this.OWNER_TYPE_DIR_NAME; + + if (!dirName) { + throw new UnknownOwnerTypeError(`Only valid options are: ${Object.values(OwnerType).join(', ')}`); + } + + return dirName; + } +} diff --git a/src/modules/owner/index.ts b/src/modules/owner/index.ts new file mode 100644 index 0000000..ac7b389 --- /dev/null +++ b/src/modules/owner/index.ts @@ -0,0 +1,3 @@ +export * from './models' +export * from './responses' +export * from './Owner.service' diff --git a/src/modules/owner/models.ts b/src/modules/owner/models.ts new file mode 100644 index 0000000..a57ba00 --- /dev/null +++ b/src/modules/owner/models.ts @@ -0,0 +1,4 @@ +export enum OwnerType { + USER = 'USER', + ORG = 'ORG', +} diff --git a/src/modules/owner/responses.ts b/src/modules/owner/responses.ts new file mode 100644 index 0000000..c54e318 --- /dev/null +++ b/src/modules/owner/responses.ts @@ -0,0 +1,4 @@ +import {HttpError} from '../../packages/fastify-compliant-http-errors'; +import {constants} from 'http2'; + +export class UnknownOwnerTypeError extends HttpError(constants.HTTP_STATUS_BAD_REQUEST, 'Unknown Owner Type') {} diff --git a/src/modules/repo-log/RepoLog.service.ts b/src/modules/repo-log/RepoLog.service.ts new file mode 100644 index 0000000..cc4b0c5 --- /dev/null +++ b/src/modules/repo-log/RepoLog.service.ts @@ -0,0 +1,53 @@ +import {User} from '../user'; +import {Repo, RepoAction} from '../repo'; +import {LogService, LogServiceImpl} from '../log'; + +export interface RepoLogService { + logCreateRepo(user: User, repoMetadata: Repo, ownerName: string): Promise; + logDeleteRepo(user: User, repo: Repo): Promise; +} + +export class RepoLogServiceImpl implements RepoLogService { + private readonly logService: LogService; + + constructor() { + this.logService = new LogServiceImpl(); + } + + private async doLogCreateRepo(user: User, repoMetadata: Repo, ownerName: string): Promise { + try { + await this.logService.create( + user, + RepoAction.REPO_CREATED, + { + repoId: repoMetadata.id.toString(), + repoName: repoMetadata.name, + ownerId: repoMetadata.ownerId.toString(), + ownerName, + ownerType: repoMetadata.ownerType, + }, + ); + } catch {} + } + + async logCreateRepo(user: User, repoMetadata: Repo, ownerName: string): Promise { + await this.doLogCreateRepo(user, repoMetadata, ownerName); + } + + private async doLogDeleteRepo(user: User, repo: Repo): Promise { + try { + await this.logService.create( + user, + RepoAction.REPO_REMOVED, + { + repoId: repo.id.toString(), + repoName: repo.name, + }, + ); + } catch {} + } + + async logDeleteRepo(user: User, repo: Repo): Promise { + await this.doLogDeleteRepo(user, repo); + } +} diff --git a/src/modules/repo-log/index.ts b/src/modules/repo-log/index.ts new file mode 100644 index 0000000..1f6b070 --- /dev/null +++ b/src/modules/repo-log/index.ts @@ -0,0 +1 @@ +export * from './RepoLog.service'; diff --git a/src/modules/repo/Repo.controller.ts b/src/modules/repo/Repo.controller.ts new file mode 100644 index 0000000..dc8ef00 --- /dev/null +++ b/src/modules/repo/Repo.controller.ts @@ -0,0 +1,73 @@ +import {RepoService, RepoServiceImpl} from './Repo.service'; +import {RouteHandlerMethod} from 'fastify'; +import {constants} from 'http2'; +import {HttpResponse} from '../../packages/fastify-send-data'; +import { Controller } from 'src/packages/fastify-utils-theoryofnekomata' +import {Uuid} from '@theoryofnekomata/uuid-buffer'; +import {CreateRepoData, Repo} from './models'; + +export interface RepoController extends Controller< + 'createRepo' + | 'deleteRepo' + | 'getRepoRefs' + | 'receivePack' +> {} + +class RepoCreatedResponse extends HttpResponse(constants.HTTP_STATUS_CREATED, 'Repo Created') {} +class RepoDeletedResponse extends HttpResponse(constants.HTTP_STATUS_NO_CONTENT, 'Repo Deleted') {} + +export class RepoControllerImpl implements RepoController { + private readonly repoService: RepoService; + private readonly noCacheHeaders = { + 'Expires': 'Fri, 01 Jan 1980 00:00:00 GMT', + 'Pragma': 'no-cache', + 'Cache-Control': 'no-cache, max-age=0, must-revalidate', + }; + + constructor() { + this.repoService = new RepoServiceImpl(); + } + + readonly createRepo: RouteHandlerMethod = async (request, reply) => { + const repo = await this.repoService.createRepo(request.body as CreateRepoData, request.session?.user); + reply.sendData(new RepoCreatedResponse(repo)); + } + + readonly deleteRepo: RouteHandlerMethod = async (request, reply) => { + const params = request.params as { repoId: string } + await this.repoService.deleteRepo(Uuid.from(params.repoId), request.session?.user) + reply.sendData(new RepoDeletedResponse()); + } + + readonly getRepoRefs: RouteHandlerMethod = async (request, reply) => { + const { service } = request.query as { service: string }; + const params = request.params as { repoName: string, ownerName: string, ownerType: string } + const outputStream = await this.repoService.getRefsFromService( + params.ownerType, + params.ownerName, + params.repoName, + service, + request.session?.user, + ); + + reply + .headers({ + ...this.noCacheHeaders, + 'Content-Type': `application/x-${service}-advertisement`, + }) + .send(outputStream); + } + + readonly receivePack: RouteHandlerMethod = async (request, reply) => { + console.log('content-type', request.headers['content-type']); + console.log('body', typeof request.body, request.body, (request.body as Buffer).toString('utf-8')); + reply + .headers({ + ...this.noCacheHeaders, + // Mark this as service response + 'Content-Type': 'application/x-git-receive-pack-result', + }) + .status(500) + .send(''); + } +} diff --git a/src/modules/repo/Repo.service.ts b/src/modules/repo/Repo.service.ts new file mode 100644 index 0000000..752d4a8 --- /dev/null +++ b/src/modules/repo/Repo.service.ts @@ -0,0 +1,299 @@ +import {PrismaClient} from '@prisma/client'; +import {PassThrough, Stream} from 'stream'; +import {Uuid} from '@theoryofnekomata/uuid-buffer'; +import * as codeCore from '@modal/code-core'; +import {notFoundFactory} from '../../packages/prisma-error-utils'; +import {User} from '../user'; +import {CreateRepoData, Repo, RepoAvailableService} from './models'; +import { + UnauthorizedToCreateRepoError, + UnauthorizedToDeleteRepoError, + UnauthorizedToInvokeRepoServiceError, + UnableToCreateRepoError, + UnableToDeleteRepoError, + UnableToInvokeRepoServiceError, + UnknownServiceError, + RepoNotFoundError, +} from './responses'; +import {ChildProcess} from 'child_process'; +import path from 'path'; +import {OwnerService, OwnerServiceImpl, OwnerType} from '../owner'; +import {RepoLogService, RepoLogServiceImpl} from '../repo-log'; + +type GitServiceMethod = (cwd: string) => Promise; + +export interface RepoService { + createRepo(data: CreateRepoData, user?: User): Promise; + getSingleRepoById(repoId: Repo['id']): Promise; + getSingleRepoByOwnerAndName(ownerType: Repo['ownerType'], ownerId: Repo['ownerId'], repoName: Repo['name']): Promise; + getOwnedRepos(ownerType: Repo['ownerType'], ownerId: Repo['ownerId']): Promise; + deleteRepo(repoId: Repo['id'], user?: User): Promise; + getRefsFromService( + ownerType: Repo['ownerType'], + ownerName: Repo['name'], + repoName: Repo['name'], + service?: string, + user?: User, + ): Promise; +} + +export class RepoServiceImpl implements RepoService { + private readonly gitService: codeCore.git.GitService; + private readonly repoLogService: RepoLogService; + private readonly prismaClient: PrismaClient; + private readonly ownerService: OwnerService; + + constructor() { + this.gitService = new codeCore.git.GitServiceImpl(); + this.repoLogService = new RepoLogServiceImpl(); + this.prismaClient = new PrismaClient(); + this.ownerService = new OwnerServiceImpl(); + } + + private static serialize(repoMetadataRaw: T): T { + return { + ...repoMetadataRaw, + id: Uuid.from(repoMetadataRaw.id), + ownerId: Uuid.from(repoMetadataRaw.ownerId), + }; + } + + private static getRepoBasePath(ownerType: string, ownerName: string, repoName: string) { + return path.join('repos', ownerType, ownerName, repoName.endsWith('.git') ? repoName : `${repoName}.git`); + } + + private static authenticate(_user?: User): User { + return { + id: Uuid.v4(), + username: 'foo', + } as User; + } + + private async doCreateRepoFilesProcess(repoName: string, ownerName: string, ownerType: OwnerType): Promise { + try { + const ownerTypeDirName = this.ownerService.getOwnerTypeDirName(ownerType); + const repoBasePath = RepoServiceImpl.getRepoBasePath(repoName, ownerName, ownerTypeDirName); + return this.gitService.create(repoBasePath); + } catch (causeRaw) { + const cause = causeRaw as Error; + throw new UnableToCreateRepoError('Something went wrong while creating the repo.', { cause, }); + } + } + + private async doCreateRepoFiles(repoName: string, ownerName: string, ownerType: OwnerType): Promise { + const childProcess = await this.doCreateRepoFilesProcess(repoName, ownerName, ownerType); + return new Promise((resolve, reject) => { + childProcess.on('error', (causeRaw) => { + const cause = causeRaw as Error; + reject(new UnableToCreateRepoError('Something went wrong while creating the repo.', { cause, })); + }); + + childProcess.on('close', (code) => { + if (code !== 0) { + reject(new UnableToCreateRepoError('Something went wrong while creating the repo.', { + cause: new Error(`Git process returned ${code}`), + })); + return; + } + resolve(); + }); + }) + } + + private async doCreateRepoMetadata(repoId: Repo['id'], data: CreateRepoData): Promise { + let repoMetadataRaw: Repo; + try { + repoMetadataRaw = await this.prismaClient.repo.create({ + data: { + id: repoId, + name: data.name, + visibility: data.visibility, + ownerId: data.ownerId, + ownerType: data.ownerType, + }, + }); + } catch (causeRaw) { + const cause = causeRaw as Error; + throw new UnableToCreateRepoError('Something went wrong while creating the repo.', { cause, }); + } + + return RepoServiceImpl.serialize(repoMetadataRaw); + } + + async createRepo(data: CreateRepoData, userRaw?: User): Promise { + // TODO put this in ability service + const user = RepoServiceImpl.authenticate(userRaw); + if (!user) { + throw new UnauthorizedToCreateRepoError('Could not create repo with insufficient authorization.'); + } + + const repoId = Uuid.v4(); + const ownerId = Uuid.from(data.ownerId); + const ownerName = await this.ownerService.getOwnerName(ownerId, data.ownerType); + await this.doCreateRepoFiles(data.name, ownerName, data.ownerType as OwnerType); + const repoMetadata = await this.doCreateRepoMetadata(repoId, { + ...data, + ownerId, + }); + + await this.repoLogService.logCreateRepo(user, repoMetadata, ownerName); + return repoMetadata; + } + + private async doGetSingleRepo(repoId: Repo['id']): Promise { + const repo = await this.prismaClient.repo.findUnique({ + where: { + id: repoId, + }, + rejectOnNotFound: notFoundFactory(RepoNotFoundError), + }) + + return RepoServiceImpl.serialize(repo); + } + + async getSingleRepoById(repoId: Repo['id']): Promise { + return this.doGetSingleRepo(repoId); + } + + private async doGetSingleRepoByOwnerAndName( + ownerType: Repo['ownerType'], + ownerId: Repo['ownerId'], + repoName: Repo['name'] + ): Promise { + const repo = await this.prismaClient.repo.findFirst({ + where: { + ownerId: Uuid.from(ownerId), + ownerType, + name: repoName, + }, + rejectOnNotFound: notFoundFactory(RepoNotFoundError), + }) + + return RepoServiceImpl.serialize(repo); + } + + async getSingleRepoByOwnerAndName( + ownerType: Repo['ownerType'], + ownerId: Repo['ownerId'], + repoName: Repo['name'] + ): Promise { + return this.doGetSingleRepoByOwnerAndName(ownerType, ownerId, repoName); + } + + private async doDeleteRepo(repoName: string, ownerName: string, ownerType: OwnerType): Promise { + try { + const ownerTypeDirName = this.ownerService.getOwnerTypeDirName(ownerType); + const repoBasePath = RepoServiceImpl.getRepoBasePath(repoName, ownerName, ownerTypeDirName); + await this.gitService.delete(repoBasePath); + } catch (causeRaw) { + const cause = causeRaw as Error; + throw new UnableToDeleteRepoError('Something went wrong while deleting the repo.', { cause, }); + } + } + + async deleteRepo(repoId: Repo['id'], userRaw?: User): Promise { + const user = RepoServiceImpl.authenticate(userRaw); + if (!user) { + throw new UnauthorizedToDeleteRepoError('Could not delete repo with insufficient authorization.'); + } + + const repo = await this.doGetSingleRepo(repoId); + const ownerName = await this.ownerService.getOwnerName(repo.ownerId, repo.ownerType); + await this.doDeleteRepo(repo.name, ownerName, repo.ownerType as OwnerType); + await this.repoLogService.logDeleteRepo(user, repo); + } + + private async doGetOwnedRepos(ownerType: Repo['ownerType'], ownerId: Repo['ownerId']) { + const repos = await this.prismaClient.repo.findMany({ + where: { + ownerId, + ownerType, + }, + }) + + return repos.map((r) => ({ + ...r, + id: Uuid.from(r.id), + ownerId: Uuid.from(r.ownerId), + })); + } + + async getOwnedRepos(ownerType: Repo['ownerType'], ownerId: Repo['ownerId']): Promise { + return this.doGetOwnedRepos(ownerType, ownerId); + } + + private static gitPackSideband(s: string): string { + return `${(4 + s.length).toString(16).padStart(4, '0')}${s}`; + } + + async doGetRefsFromService( + ownerType: Repo['ownerType'], + ownerName: Repo['name'], + repoName: Repo['name'], + service?: string, + ) { + if (!service) { + throw new UnknownServiceError('Could not return refs for service.'); + } + + const gitServiceMethods: Record = { + [RepoAvailableService.RECEIVE_PACK]: this.gitService.advertiseReceivePackRefs, + [RepoAvailableService.UPLOAD_PACK]: this.gitService.advertiseUploadPackRefs, + } + const { [service as RepoAvailableService]: childProcessMethod } = gitServiceMethods; + + if (!childProcessMethod) { + throw new UnknownServiceError('Could not return refs for service.'); + } + + const repoBasePath = RepoServiceImpl.getRepoBasePath(ownerType, ownerName, repoName); + + return new Promise(async (resolve, reject) => { + const stream = new PassThrough({ + emitClose: true, + autoDestroy: true, + }); + + const childProcess = await childProcessMethod(repoBasePath); + + childProcess.on('close', (code) => { + if (code === 0) { + resolve(stream); + return; + } + + switch (code) { + case 128: + reject(new RepoNotFoundError(`Repo "${repoBasePath}" does not exist. Check that the repo has been created and not deleted previously.`)); + return; + default: + break; + } + + reject(new UnableToInvokeRepoServiceError(`Something went wrong while invoking repo service: Git process received error code ${code}.`)); + }); + + stream.push(`${RepoServiceImpl.gitPackSideband(`# service=${service}\n`)}0000`); + if (childProcess.stdout) { + childProcess.stdout.pipe(stream); + } + }); + } + + async getRefsFromService( + ownerType: Repo['ownerType'], + ownerName: Repo['name'], + repoName: Repo['name'], + service?: string, + userRaw?: User, + ): Promise { + const user = RepoServiceImpl.authenticate(userRaw); + if (!user) { + throw new UnauthorizedToInvokeRepoServiceError('Could not invoke repo service with insufficient authorization.'); + } + + // TODO compute user abilities here + + return this.doGetRefsFromService(ownerType, ownerName, repoName, service); + } +} diff --git a/src/modules/repo/index.ts b/src/modules/repo/index.ts new file mode 100644 index 0000000..a2a7c87 --- /dev/null +++ b/src/modules/repo/index.ts @@ -0,0 +1,4 @@ +export * from './Repo.service' +export * from './Repo.controller' +export * from './models'; +export * from './responses'; diff --git a/src/modules/repo/models.ts b/src/modules/repo/models.ts new file mode 100644 index 0000000..86d4bf8 --- /dev/null +++ b/src/modules/repo/models.ts @@ -0,0 +1,30 @@ +import {Org} from '../org'; +import {User} from '../user'; + +export enum RepoVisibility { + PRIVATE = 'PRIVATE', + INTERNAL = 'INTERNAL', + PUBLIC = 'PUBLIC', +} + +export type Owner = User | Org; + +export enum RepoAction { + REPO_CREATED = 'REPO_CREATED', + REPO_REMOVED = 'REPO_REMOVED', +} + +export enum RepoAvailableService { + UPLOAD_PACK = 'git-upload-pack', + RECEIVE_PACK = 'git-receive-pack', +} + +export type Repo = { + id: Buffer, + name: string, + ownerId: Buffer, + ownerType: string, + visibility: string, +} + +export type CreateRepoData = Omit; diff --git a/src/modules/repo/responses.ts b/src/modules/repo/responses.ts new file mode 100644 index 0000000..db1cbb4 --- /dev/null +++ b/src/modules/repo/responses.ts @@ -0,0 +1,11 @@ +import {constants} from 'http2'; +import {HttpError} from '../../packages/fastify-compliant-http-errors'; + +export class UnauthorizedToCreateRepoError extends HttpError(constants.HTTP_STATUS_UNAUTHORIZED, 'Unauthorized to Create Repo') {} +export class UnauthorizedToDeleteRepoError extends HttpError(constants.HTTP_STATUS_UNAUTHORIZED, 'Unauthorized to Delete Repo') {} +export class UnauthorizedToInvokeRepoServiceError extends HttpError(constants.HTTP_STATUS_UNAUTHORIZED, 'Unauthorized to Invoke Repo Service') {} +export class UnableToCreateRepoError extends HttpError(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Unable to Create Repo') {} +export class UnableToDeleteRepoError extends HttpError(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Unable to Delete Repo') {} +export class UnableToInvokeRepoServiceError extends HttpError(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Unable to Invoke Repo Service') {} +export class UnknownServiceError extends HttpError(constants.HTTP_STATUS_BAD_REQUEST, 'Unknown Service') {} +export class RepoNotFoundError extends HttpError(constants.HTTP_STATUS_NOT_FOUND, 'Repo Not Found') {} diff --git a/src/modules/repo/routes.ts b/src/modules/repo/routes.ts new file mode 100644 index 0000000..457db21 --- /dev/null +++ b/src/modules/repo/routes.ts @@ -0,0 +1,30 @@ +import {ServerInstance} from '../../packages/fastify-utils-theoryofnekomata'; +import {RepoController, RepoControllerImpl} from './Repo.controller'; + +export const addRoutes = (server: ServerInstance) => { + const repoController: RepoController = new RepoControllerImpl() + + server.route({ + method: 'POST', + url: '/api/repos', + handler: repoController.createRepo, + }) + + server.route({ + method: 'DELETE', + url: '/api/repos/:id', + handler: repoController.deleteRepo, + }) + + server.route({ + method: 'GET', + url: '/repos/:ownerType/:ownerName/:repoName/info/refs', + handler: repoController.getRepoRefs, + }) + + server.route({ + method: 'POST', + url: '/repos/:ownerType/:ownerName/:repoName/git-receive-pack', + handler: repoController.receivePack, + }) +} diff --git a/src/modules/user/User.controller.ts b/src/modules/user/User.controller.ts new file mode 100644 index 0000000..7d22f6b --- /dev/null +++ b/src/modules/user/User.controller.ts @@ -0,0 +1,40 @@ +import { RouteHandlerMethod } from 'fastify' +import { Controller } from 'src/packages/fastify-utils-theoryofnekomata' +import { UserService, UserServiceImpl } from 'src/modules/user/User.service' +import { RegisterUserFormData } from 'src/modules/user/models' +import { SessionService, SessionServiceImpl } from 'src/modules/auth' +import { + UserRegisteredResponse, + UnableToRegisterUserError, +} from './responses' + +export interface UserController extends Controller<'register'> { +} + +export class UserControllerImpl implements UserController { + private readonly userService: UserService + private readonly sessionService: SessionService + + constructor() { + this.userService = new UserServiceImpl() + this.sessionService = new SessionServiceImpl() + } + + readonly register: RouteHandlerMethod = async (request, reply) => { + try { + const body = request.body as RegisterUserFormData + const newUser = await this.userService.create({ + username: body.username, + password: body.confirmNewPassword, + }) + const newSession = await this.sessionService.create({ + userId: newUser.id, + }) + + reply.sendData(new UserRegisteredResponse(newSession)) + } catch (causeRaw) { + const cause = causeRaw as Error + throw new UnableToRegisterUserError('The operation could not be performed. Try again later.', { cause }) + } + } +} diff --git a/src/modules/user/User.service.ts b/src/modules/user/User.service.ts new file mode 100644 index 0000000..dcc46ee --- /dev/null +++ b/src/modules/user/User.service.ts @@ -0,0 +1,77 @@ +import { PrismaClient } from '@prisma/client' +import {CreateUserData, PublicUser, User} from 'src/modules/user/models'; +import { PasswordService, PasswordServiceImpl, LoginUserFormData } from 'src/modules/auth' +import { Uuid } from '@theoryofnekomata/uuid-buffer'; +import { notFoundFactory } from '../../packages/prisma-error-utils'; +import {UserNotFoundError} from './responses'; + +export interface UserService { + create(data: CreateUserData): Promise; + + getFromCredentials(args: LoginUserFormData): Promise; + + getFromId(id: User['id']): Promise; +} + +export class UserServiceImpl implements UserService { + private readonly prismaClient: PrismaClient + private readonly passwordService: PasswordService + + constructor() { + this.prismaClient = new PrismaClient() + this.passwordService = new PasswordServiceImpl() + } + + async create(data: CreateUserData): Promise { + const newUser = await this.prismaClient.user.create({ + data: { + ...data, + id: Uuid.v4(), + password: await this.passwordService.hash(data.password), + }, + select: { + id: true, + username: true, + password: false, + }, + }) + + return { + ...newUser, + id: Uuid.from(newUser.id), + } + } + + async getFromCredentials(args: LoginUserFormData): Promise { + const { password: hashedPassword, ...existingUser } = await this.prismaClient.user.findUnique({ + where: { + username: args.username, + }, + rejectOnNotFound: notFoundFactory(UserNotFoundError), + }) + await this.passwordService.assertEqual(args.password, hashedPassword) + + return { + ...existingUser, + id: Uuid.from(existingUser.id), + } + } + + async getFromId(id: User['id']): Promise { + const existingUser = await this.prismaClient.user.findUnique({ + where: { + id, + }, + select: { + id: true, + username: true, + }, + rejectOnNotFound: notFoundFactory(UserNotFoundError), + }); + + return { + ...existingUser, + id: Uuid.from(existingUser.id), + }; + } +} diff --git a/src/modules/user/index.ts b/src/modules/user/index.ts new file mode 100644 index 0000000..e6b4e67 --- /dev/null +++ b/src/modules/user/index.ts @@ -0,0 +1,4 @@ +export * from './models' +export * from './User.controller' +export * from './User.service' +export * from './responses' diff --git a/src/modules/user/models.ts b/src/modules/user/models.ts new file mode 100644 index 0000000..a87a73f --- /dev/null +++ b/src/modules/user/models.ts @@ -0,0 +1,16 @@ +export interface User { + id: Buffer + username: string + password: string +} + +export interface CreateUserData extends Omit { +} + +export interface PublicUser extends Omit { +} + +export interface RegisterUserFormData extends Pick { + newPassword: string; + confirmNewPassword: string; +} diff --git a/src/modules/user/responses.ts b/src/modules/user/responses.ts new file mode 100644 index 0000000..c8a0f7a --- /dev/null +++ b/src/modules/user/responses.ts @@ -0,0 +1,14 @@ +import { constants } from 'http2' +import { HttpResponse } from 'src/packages/fastify-send-data' +import { Session } from 'src/modules/auth' +import {HttpError} from 'src/packages/fastify-compliant-http-errors'; + +export class UserRegisteredResponse extends HttpResponse(constants.HTTP_STATUS_OK, 'User Registered') { +} + +export class UnableToRegisterUserError extends HttpError( + constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, + 'Unable to Register', +) {} + +export class UserNotFoundError extends HttpError(constants.HTTP_STATUS_NOT_FOUND, 'User Not Found') {} diff --git a/src/modules/user/routes.ts b/src/modules/user/routes.ts new file mode 100644 index 0000000..c8e9588 --- /dev/null +++ b/src/modules/user/routes.ts @@ -0,0 +1,11 @@ +import {ServerInstance} from '../../packages/fastify-utils-theoryofnekomata'; +import {UserControllerImpl, UserController} from './User.controller'; + +export const addRoutes = (server: ServerInstance) => { + const userController: UserController = new UserControllerImpl() + server.route({ + method: 'POST', + url: '/api/users/register', + handler: userController.register, + }); +} diff --git a/src/packages/fastify-home-route/index.ts b/src/packages/fastify-home-route/index.ts new file mode 100644 index 0000000..78259bf --- /dev/null +++ b/src/packages/fastify-home-route/index.ts @@ -0,0 +1,44 @@ +import fp from 'fastify-plugin'; +import {FastifyInstance} from 'fastify'; + +declare module 'fastify' { + interface FastifyInstance { + allStaticRoutes: { + method: string, + url: string, + }[] + } +} + +const fastifyHomeRoute = async (app: FastifyInstance) => { + app.decorateRequest('allStaticRoutes', null); + app.addHook('onRoute', (routeData) => { + if (routeData.url.includes('/:')) { + return; + } + + const mutableServer = app as unknown as Record; + mutableServer['allStaticRoutes'] = !Array.isArray(mutableServer['allStaticRoutes']) + ? [ + { + method: routeData.method, + url: routeData.url, + } + ] + : [ + ...mutableServer['allStaticRoutes'], + { + method: routeData.method, + url: routeData.url, + }, + ]; + }); + app.addHook('onReady', () => { + const mutableServer = app as unknown as Record; + if (!mutableServer['allStaticRoutes']) { + mutableServer['allStaticRoutes'] = []; + } + }) +} + +export default fp(fastifyHomeRoute); diff --git a/src/packages/fastify-send-data/index.ts b/src/packages/fastify-send-data/index.ts index 43428e7..bad9da4 100644 --- a/src/packages/fastify-send-data/index.ts +++ b/src/packages/fastify-send-data/index.ts @@ -8,6 +8,11 @@ export interface ResponseDataInterface { body?: T extends undefined ? undefined : { data: T } } +const BLANK_BODY_STATUS_CODES = [ + constants.HTTP_STATUS_NO_CONTENT, + constants.HTTP_STATUS_RESET_CONTENT, +] + const fastifySendData = async (app: FastifyInstance) => { const sendDataKey = 'sendData' as const app.decorateReply(sendDataKey, function reply(this: FastifyReply, data: T) { @@ -15,7 +20,7 @@ const fastifySendData = async (app: FastifyInstance) => { if (data.statusMessage) { this.raw.statusMessage = data.statusMessage } - if (data.statusCode !== constants.HTTP_STATUS_NO_CONTENT) { + if (!BLANK_BODY_STATUS_CODES.includes(data.statusCode)) { this.send(data.body) return } @@ -42,7 +47,7 @@ export const HttpResponse = ( readonly body?: { data: T } constructor(data?: T) { - if (data && statusCode !== constants.HTTP_STATUS_NO_CONTENT) { + if (data && !BLANK_BODY_STATUS_CODES.includes(statusCode)) { this.body = { data, } diff --git a/src/packages/fastify-service-session/index.ts b/src/packages/fastify-service-session/index.ts index 5b45448..726a976 100644 --- a/src/packages/fastify-service-session/index.ts +++ b/src/packages/fastify-service-session/index.ts @@ -1,23 +1,33 @@ import fp from 'fastify-plugin' import { FastifyInstance, FastifyRequest } from 'fastify' -export interface FastifySessionOpts { - sessionRequestKey?: string, +export interface FastifySessionOpts { extractSessionId: (request: RequestType) => SessionId | null | undefined, isSessionValid: (sessionId: SessionId) => Promise, - getSession: (sessionId: SessionId) => Promise + getSession: (sessionId: SessionId) => Promise, + getUserFromSession?: (session: SessionType) => Promise, + sessionRequestKey?: string, + userRequestKey?: string, } const fastifySession = async (app: FastifyInstance, opts: FastifySessionOpts) => { - const { sessionRequestKey = 'session' } = opts - app.decorateRequest(sessionRequestKey, null) + const { + sessionRequestKey = 'session', + userRequestKey = 'user' + } = opts; + app.decorateRequest(sessionRequestKey, null); + app.decorateRequest(userRequestKey, null); app.addHook('onRequest', async (request: FastifyRequest) => { const sessionId = opts.extractSessionId(request) if (typeof sessionId === 'string') { const isSessionValid = await opts.isSessionValid(sessionId) if (isSessionValid) { const mutableRequest = (request as unknown) as Record - mutableRequest[sessionRequestKey] = await opts.getSession(sessionId) + const session = await opts.getSession(sessionId); + mutableRequest[sessionRequestKey] = session; + if (typeof opts.getUserFromSession === 'function') { + mutableRequest[userRequestKey] = await opts.getUserFromSession(session); + } } } }) diff --git a/src/packages/fastify-utils-theoryofnekomata/index.ts b/src/packages/fastify-utils-theoryofnekomata/index.ts new file mode 100644 index 0000000..dcf6b33 --- /dev/null +++ b/src/packages/fastify-utils-theoryofnekomata/index.ts @@ -0,0 +1,7 @@ +import {FastifyInstance, RouteHandlerMethod} from 'fastify'; + +export type ServerInstance = FastifyInstance; + +export type Controller = { + [key in T]: RouteHandlerMethod; +}; diff --git a/src/packages/prisma-error-utils/index.ts b/src/packages/prisma-error-utils/index.ts new file mode 100644 index 0000000..35364ad --- /dev/null +++ b/src/packages/prisma-error-utils/index.ts @@ -0,0 +1 @@ +export const notFoundFactory = (ErrorClass: { new(): T }) => () => new ErrorClass() diff --git a/src/routes.ts b/src/routes.ts index 66fe13b..80a7d9b 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,21 +1,16 @@ -import { FastifyInstance } from 'fastify'; -import * as git from './modules/git' +import {ServerInstance} from './packages/fastify-utils-theoryofnekomata'; +import * as auth from './modules/auth/routes'; +import * as user from './modules/user/routes'; +import * as repo from './modules/repo/routes'; -export const addRoutes = (server: FastifyInstance) => { - server.get('/', async (_, reply) => { - reply.send({hello: 'world'}) - }); - - const gitController = new git.GitControllerImpl() - server.route({ - method: 'POST', - url: '/repos', - handler: gitController.createRepo, - }) +export const addRoutes = (server: ServerInstance) => { + auth.addRoutes(server); + user.addRoutes(server); + repo.addRoutes(server); - server.route({ - method: 'DELETE', - url: '/repos/:id', - handler: gitController.deleteRepo, - }) + server.get('/', async (request, reply) => { + reply.send({ + routes: request.server.allStaticRoutes, + }); + }); } diff --git a/src/server.ts b/src/server.ts index aca8ffb..05fe05b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,20 +1,144 @@ -import fastify, {FastifyServerOptions} from 'fastify'; -import * as codeCore from '@modal/code-core'; +import fastify, {FastifyRequest, FastifyServerOptions} from 'fastify'; import {fastifyErrorHandler} from './packages/fastify-compliant-http-errors'; import fastifySendData from './packages/fastify-send-data'; +import fastifyServiceSession from './packages/fastify-service-session'; +import {Session, SessionService, SessionServiceImpl} from './modules/auth'; +import {Uuid} from '@theoryofnekomata/uuid-buffer'; +import fastifyHomeRoute from './packages/fastify-home-route'; +import {User} from './modules/user'; +import {IncomingMessage} from 'http'; +import {createDeflate, createGunzip} from 'zlib'; +import {PassThrough, Transform} from 'stream'; + +interface RequestLike extends Pick {} declare module 'fastify' { interface FastifyRequest { - user?: codeCore.common.User, + session?: Session & { user: User }, } } +enum AvailableDecoder { + GZIP = 'gzip', + DEFLATE = 'deflate', +} + +type DecoderFunction = () => Transform; + +const DECODERS: Record = { + [AvailableDecoder.GZIP]:() => createGunzip(), + [AvailableDecoder.DEFLATE]: () => createDeflate(), +} + +const DEFAULT_DECODER: DecoderFunction = () => new PassThrough(); + +class PackRequestTransformStream extends Transform { + private data: Record = { + last: null, + commit: null, + ref: null, + } + + constructor() { + super(); + const headerRegExp = /([0-9a-fA-F]+) ([0-9a-fA-F]+) (.+?)( |00|\u0000)|^(0000)$/gi; + let bufferedData = Buffer.from(''); + this.on('data', (chunk) => { + const isHeaderRead = this.data.last && this.data.commit; + if (!isHeaderRead) { + bufferedData = Buffer.concat([bufferedData, chunk]); + const bufferAsString = bufferedData.toString('utf-8'); + const bufferHeaderMatch = bufferAsString.match(headerRegExp); + if (bufferHeaderMatch) { + const [ + _header, + last, + commit, + ref, + ] = Array.from(bufferHeaderMatch); + this.data = { + last, + commit, + ref, + } + } + } + this.push(chunk); + }) + } +} + +const receivePackRequestParser = ( + request: FastifyRequest, + payload: IncomingMessage, + done: Function, +) => { + console.log(request.headers); + const encoding = request.headers['content-encoding']; + let theDecoder: DecoderFunction = DEFAULT_DECODER; + if (encoding) { + const { [encoding as AvailableDecoder]: decoder } = DECODERS; + if (!decoder) { + done(new Error(`Unknown encoding: ${encoding}`)); + return; + } + theDecoder = decoder; + } + + const requestParserStream = new PackRequestTransformStream(); + const pipeline = payload + .pipe(theDecoder()) + .pipe(requestParserStream) + + pipeline.on('end', () => { + done(null, pipeline); + }); +}; + export const createServer = (opts?: FastifyServerOptions) => { - const server = fastify(opts) + const server = fastify(opts); server.setErrorHandler(fastifyErrorHandler) + server.register(fastifyHomeRoute); + server.addContentTypeParser('application/x-git-receive-pack-request', receivePackRequestParser); + server.addContentTypeParser('application/x-git-upload-pack-request', receivePackRequestParser); // TODO + server.register(fastifyServiceSession, { + sessionRequestKey: 'session', + getSession: async (sessionId: string) => { + const sessionService: SessionService = new SessionServiceImpl() + return sessionService.get(Uuid.from(sessionId)) + }, + isSessionValid: async (sessionId: string) => { + const sessionService: SessionService = new SessionServiceImpl() + return sessionService.isValid(Uuid.from(sessionId)) + }, + extractSessionId: (request: RequestLike) => { + const sessionIdKey = 'sessionId' + + const query = request.query as Record + const querySessionId = query[sessionIdKey] + if (typeof querySessionId === 'string') { + return querySessionId + } + + const { authorization } = request.headers + if (typeof authorization === 'string') { + const [authType, headerSessionId] = authorization.split(' ') + if (authType === 'Bearer') { + return headerSessionId + } + } + + const body = request.body as Record ?? {} + const bodySessionId = body[sessionIdKey] + if (typeof bodySessionId === 'string') { + return bodySessionId + } - server.register(fastifySendData) + return undefined + } + }) + server.register(fastifySendData); return server; } diff --git a/src/utils/types.ts b/src/utils/types.ts deleted file mode 100644 index 3542d68..0000000 --- a/src/utils/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { RouteHandlerMethod } from 'fastify' - -type Controller = { - [key in T]: RouteHandlerMethod; -}; - -export default Controller diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index e25b7f5..3d14e86 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -7,7 +7,7 @@ "importHelpers": true, "declaration": true, "sourceMap": true, - "rootDir": "./", + "baseUrl": ".", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, diff --git a/tsconfig.json b/tsconfig.json index ff42f54..61c1843 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "importHelpers": true, "declaration": true, "sourceMap": true, - "rootDir": "./src", + "baseUrl": ".", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, diff --git a/yarn.lock b/yarn.lock index 5a28a7c..09dd830 100644 --- a/yarn.lock +++ b/yarn.lock @@ -316,6 +316,21 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@mapbox/node-pre-gyp@^1.0.0": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc" + integrity sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + "@mdn/browser-compat-data@^3.3.14": version "3.3.14" resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-3.3.14.tgz#b72a37c654e598f9ae6f8335faaee182bebc6b28" @@ -359,6 +374,23 @@ resolved "https://registry.yarnpkg.com/@ovyerus/licenses/-/licenses-6.4.4.tgz#596e3ace46ab7c70bcf0e2b17f259796a4bedf9f" integrity sha512-IHjc31WXciQT3hfvdY+M59jBkQp70Fpr04tNDVO5rez2PNv4u8tE6w//CkU+GeBoO9k2ahneSqzjzvlgjyjkGw== +"@prisma/client@^3.14.0": + version "3.14.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.14.0.tgz#bb90405c012fcca11f4647d91153ed4c58f3bd48" + integrity sha512-atb41UpgTR1MCst0VIbiHTMw8lmXnwUvE1KyUCAkq08+wJyjRE78Due+nSf+7uwqQn+fBFYVmoojtinhlLOSaA== + dependencies: + "@prisma/engines-version" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + +"@prisma/engines-version@3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a": + version "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz#4edae57cf6527f35e22cebe75e49214fc0e99ac9" + integrity sha512-D+yHzq4a2r2Rrd0ZOW/mTZbgDIkUkD8ofKgusEI1xPiZz60Daks+UM7Me2ty5FzH3p/TgyhBpRrfIHx+ha20RQ== + +"@prisma/engines@3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a": + version "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz#7fa11bc26a51d450185c816cc0ab8cac673fb4bf" + integrity sha512-LwZvI3FY6f43xFjQNRuE10JM5R8vJzFTSmbV9X0Wuhv9kscLkjRlZt0BEoiHmO+2HA3B3xxbMfB5du7ZoSFXGg== + "@theoryofnekomata/uuid-buffer@^0.1.0": version "0.1.0" resolved "https://js.pack.modal.sh/@theoryofnekomata%2fuuid-buffer/-/uuid-buffer-0.1.0.tgz#0917314e8230ce1a2047172b3277512bc0fd73a3" @@ -367,6 +399,13 @@ "@types/uuid" "^8.3.4" uuid "^8.3.2" +"@types/bcrypt@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-5.0.0.tgz#a835afa2882d165aff5690893db314eaa98b9f20" + integrity sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw== + dependencies: + "@types/node" "*" + "@types/chai-subset@^1.3.3": version "1.3.3" resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94" @@ -389,7 +428,7 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/node@^17.0.25": +"@types/node@*", "@types/node@^17.0.25": version "17.0.35" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.35.tgz#635b7586086d51fb40de0a2ec9d1014a5283ba4a" integrity sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg== @@ -486,6 +525,11 @@ "@typescript-eslint/types" "5.25.0" eslint-visitor-keys "^3.3.0" +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + abstract-logging@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" @@ -501,6 +545,13 @@ acorn@^8.7.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.4, ajv@^6.12.6: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -545,11 +596,24 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + archy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -651,6 +715,14 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +bcrypt@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71" + integrity sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.0" + node-addon-api "^3.1.0" + bl@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/bl/-/bl-5.0.0.tgz#6928804a41e9da9034868e1c50ca88f21f57aea2" @@ -762,6 +834,11 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + cli-cursor@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" @@ -821,6 +898,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -843,6 +925,11 @@ confusing-browser-globals@^1.0.10: resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== +console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" @@ -889,6 +976,13 @@ damerau-levenshtein@^1.0.7: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -903,13 +997,6 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -952,11 +1039,21 @@ degit@^2.8.4: resolved "https://registry.yarnpkg.com/degit/-/degit-2.8.4.tgz#3bb9c5c00f157c44724dd4a50724e4aa75a54d38" integrity sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng== +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + detect-indent@^6.0.0: version "6.1.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -1656,6 +1753,13 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1696,6 +1800,21 @@ fuzzy-search@^3.2.1: resolved "https://registry.yarnpkg.com/fuzzy-search/-/fuzzy-search-3.2.1.tgz#65d5faad6bc633aee86f1898b7788dfe312ac6c9" integrity sha512-vAcPiyomt1ioKAsAL2uxSABHJ4Ju/e4UeDM+g1OlR0vV4YhLGMNsdLNvZTpEDY4JCSt0E4hASCNM5t2ETtsbyg== +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -1839,6 +1958,11 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -1846,6 +1970,14 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -2222,7 +2354,7 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -make-dir@^3.0.0: +make-dir@^3.0.0, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -2264,6 +2396,26 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minipass@^3.0.0: + version "3.1.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" + integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -2289,11 +2441,30 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +node-addon-api@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + +node-fetch@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + node-releases@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ== +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -2301,6 +2472,16 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + nth-check@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" @@ -2584,6 +2765,13 @@ pridepack@1.1.1: resolve.exports "^1.1.0" yargs "^17.2.1" +prisma@^3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.14.0.tgz#dd67ece37d7b5373e9fd9588971de0024b49be81" + integrity sha512-l9MOgNCn/paDE+i1K2fp9NZ+Du4trzPTJsGkaQHVBufTGqzoYHuNk8JfzXuIn0Gte6/ZjyKj652Jq/Lc1tp2yw== + dependencies: + "@prisma/engines" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + process-warning@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616" @@ -2634,7 +2822,7 @@ react-is@^16.13.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -readable-stream@^3.4.0: +readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -2825,7 +3013,7 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.2, signal-exit@^3.0.3: +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -2858,7 +3046,7 @@ string-similarity@^4.0.1: resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -2954,6 +3142,18 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +tar@^6.1.11: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -2986,6 +3186,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" @@ -3131,6 +3336,19 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -3154,6 +3372,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"