소스 검색

Implement git smart server endpoints

The endpoints for creation, deletion, as well as refs advertisement have
been implemented.
master
부모
커밋
78805e1641
52개의 변경된 파일1676개의 추가작업 그리고 142개의 파일을 삭제
  1. +3
    -0
      .gitignore
  2. +5
    -1
      package.json
  3. +83
    -0
      prisma/schema.prisma
  4. +14
    -0
      scripts/create-default-user.ts
  5. +3
    -1
      src/index.ts
  6. +63
    -0
      src/modules/auth/Auth.controller.ts
  7. +29
    -0
      src/modules/auth/Password.service.ts
  8. +120
    -0
      src/modules/auth/Session.service.ts
  9. +5
    -0
      src/modules/auth/index.ts
  10. +14
    -0
      src/modules/auth/models.ts
  11. +16
    -0
      src/modules/auth/responses.ts
  12. +18
    -0
      src/modules/auth/routes.ts
  13. +0
    -34
      src/modules/git/Git.controller.ts
  14. +0
    -48
      src/modules/git/Git.service.ts
  15. +0
    -3
      src/modules/git/index.ts
  16. +0
    -3
      src/modules/git/models.ts
  17. +42
    -0
      src/modules/log/Log.service.ts
  18. +2
    -0
      src/modules/log/index.ts
  19. +7
    -0
      src/modules/log/models.ts
  20. +36
    -0
      src/modules/org/Org.service.ts
  21. +3
    -0
      src/modules/org/index.ts
  22. +6
    -0
      src/modules/org/models.ts
  23. +4
    -0
      src/modules/org/responses.ts
  24. +73
    -0
      src/modules/owner/Owner.service.ts
  25. +3
    -0
      src/modules/owner/index.ts
  26. +4
    -0
      src/modules/owner/models.ts
  27. +4
    -0
      src/modules/owner/responses.ts
  28. +53
    -0
      src/modules/repo-log/RepoLog.service.ts
  29. +1
    -0
      src/modules/repo-log/index.ts
  30. +73
    -0
      src/modules/repo/Repo.controller.ts
  31. +299
    -0
      src/modules/repo/Repo.service.ts
  32. +4
    -0
      src/modules/repo/index.ts
  33. +30
    -0
      src/modules/repo/models.ts
  34. +11
    -0
      src/modules/repo/responses.ts
  35. +30
    -0
      src/modules/repo/routes.ts
  36. +40
    -0
      src/modules/user/User.controller.ts
  37. +77
    -0
      src/modules/user/User.service.ts
  38. +4
    -0
      src/modules/user/index.ts
  39. +16
    -0
      src/modules/user/models.ts
  40. +14
    -0
      src/modules/user/responses.ts
  41. +11
    -0
      src/modules/user/routes.ts
  42. +44
    -0
      src/packages/fastify-home-route/index.ts
  43. +7
    -2
      src/packages/fastify-send-data/index.ts
  44. +16
    -6
      src/packages/fastify-service-session/index.ts
  45. +7
    -0
      src/packages/fastify-utils-theoryofnekomata/index.ts
  46. +1
    -0
      src/packages/prisma-error-utils/index.ts
  47. +13
    -18
      src/routes.ts
  48. +129
    -5
      src/server.ts
  49. +0
    -7
      src/utils/types.ts
  50. +1
    -1
      tsconfig.eslint.json
  51. +1
    -1
      tsconfig.json
  52. +237
    -12
      yarn.lock

+ 3
- 0
.gitignore 파일 보기

@@ -105,3 +105,6 @@ dist
.tern-port

.npmrc
.database/
.idea/
repos/

+ 5
- 1
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",


+ 83
- 0
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])
}

+ 14
- 0
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()

+ 3
- 1
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) => {


+ 63
- 0
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.')
}
}

+ 29
- 0
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<string>;

assertEqual(password: string, hashedPassword: string): Promise<void>;
}

export class PasswordServiceImpl implements PasswordService {
constructor(private readonly saltRounds = 12) {
}

async hash(password: string): Promise<string> {
const salt = await genSalt(this.saltRounds)
return hash(password, salt)
}

async assertEqual(password: string, hashedPassword: string): Promise<void> {
const result = await compare(password, hashedPassword)
if (result) {
return
}
throw new PasswordNotEqualAssertError()
}
}

+ 120
- 0
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<User, 'username'> };

export interface SessionService {
create(data: CreateSessionData): Promise<SessionWithUsername>

expire(sessionId: Session['id']): Promise<Session>

isValid(sessionId: Session['id']): Promise<boolean>

get(id: Session['id']): Promise<SessionWithUsername | null>
}

export class SessionNotFoundError extends AppError {}

export class SessionServiceImpl implements SessionService {
private readonly prismaClient: PrismaClient

constructor() {
this.prismaClient = new PrismaClient()
}

private static serialize<T extends Session>(raw: T): T {
return {
...raw,
id: Uuid.from(raw.id),
userId: Uuid.from(raw.userId)
};
}

async create(data: CreateSessionData): Promise<SessionWithUsername> {
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<Session> {
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<boolean> {
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<SessionWithUsername | null> {
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;
}
}

+ 5
- 0
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'

+ 14
- 0
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<Session, 'id' | 'createdAt' | 'validUntil'> {
}

export interface LoginUserFormData {
username: string;
password: string;
}

+ 16
- 0
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<Session>(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') {
}

+ 18
- 0
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,
});
}

+ 0
- 34
src/modules/git/Git.controller.ts 파일 보기

@@ -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<codeCore.git.Repo>(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());
}
}

+ 0
- 48
src/modules/git/Git.service.ts 파일 보기

@@ -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<codeCore.git.Repo>;
deleteRepo(repoId: codeCore.git.Repo['id'], user?: codeCore.common.User): Promise<void>
}

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<codeCore.git.Repo> {
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<void> {
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.');
}
}

+ 0
- 3
src/modules/git/index.ts 파일 보기

@@ -1,3 +0,0 @@
export * from './Git.service'
export * from './Git.controller'
export * from './models'

+ 0
- 3
src/modules/git/models.ts 파일 보기

@@ -1,3 +0,0 @@
import * as codeCore from '@modal/code-core'

export type CreateRepoOptions = codeCore.git.CreateOptions

+ 42
- 0
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<string, unknown>): Promise<Log>
}

export class LogServiceImpl implements LogService {
private readonly prismaClient: PrismaClient

constructor() {
this.prismaClient = new PrismaClient()
}

async create(subject: User, action: string, parameters?: Record<string, unknown>): Promise<Log> {
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),
}
}
}

+ 2
- 0
src/modules/log/index.ts 파일 보기

@@ -0,0 +1,2 @@
export * from './Log.service'
export * from './models'

+ 7
- 0
src/modules/log/models.ts 파일 보기

@@ -0,0 +1,7 @@
export type Log = {
id: number,
subjectUserId: Buffer,
subjectUsername: string,
action: string,
createdAt: Date,
}

+ 36
- 0
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<Org>;
}

export class OrgServiceImpl {
private readonly prismaClient: PrismaClient;

constructor() {
this.prismaClient = new PrismaClient();
}

private static serialize<T extends Org>(org: T): T {
return {
...org,
id: Uuid.from(org.id),
creatorUserId: Uuid.from(org.creatorUserId),
};
}

async getFromId(id: Org['id']): Promise<Org> {
const org = await this.prismaClient.org.findUnique({
where: {
id,
},
rejectOnNotFound: notFoundFactory(OrgNotFoundError),
})

return OrgServiceImpl.serialize(org);
}
}

+ 3
- 0
src/modules/org/index.ts 파일 보기

@@ -0,0 +1,3 @@
export * from './models';
export * from './Org.service';
export * from './responses';

+ 6
- 0
src/modules/org/models.ts 파일 보기

@@ -0,0 +1,6 @@
export type Org = {
id: Buffer,
name: string,
description: string,
creatorUserId: Buffer,
}

+ 4
- 0
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') {}

+ 73
- 0
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<string>;
getOwnerTypeDirName(type: string): string;
}

export class OwnerServiceImpl implements OwnerService {
private readonly userService: UserService;
private readonly orgService: OrgService;
private readonly OWNER_TYPE_DIR_NAME: Record<OwnerType, string>;

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;
}
}

+ 3
- 0
src/modules/owner/index.ts 파일 보기

@@ -0,0 +1,3 @@
export * from './models'
export * from './responses'
export * from './Owner.service'

+ 4
- 0
src/modules/owner/models.ts 파일 보기

@@ -0,0 +1,4 @@
export enum OwnerType {
USER = 'USER',
ORG = 'ORG',
}

+ 4
- 0
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') {}

+ 53
- 0
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<void>;
logDeleteRepo(user: User, repo: Repo): Promise<void>;
}

export class RepoLogServiceImpl implements RepoLogService {
private readonly logService: LogService;

constructor() {
this.logService = new LogServiceImpl();
}

private async doLogCreateRepo(user: User, repoMetadata: Repo, ownerName: string): Promise<void> {
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<void> {
await this.doLogCreateRepo(user, repoMetadata, ownerName);
}

private async doLogDeleteRepo(user: User, repo: Repo): Promise<void> {
try {
await this.logService.create(
user,
RepoAction.REPO_REMOVED,
{
repoId: repo.id.toString(),
repoName: repo.name,
},
);
} catch {}
}

async logDeleteRepo(user: User, repo: Repo): Promise<void> {
await this.doLogDeleteRepo(user, repo);
}
}

+ 1
- 0
src/modules/repo-log/index.ts 파일 보기

@@ -0,0 +1 @@
export * from './RepoLog.service';

+ 73
- 0
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<Repo>(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('');
}
}

+ 299
- 0
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<ChildProcess>;

export interface RepoService {
createRepo(data: CreateRepoData, user?: User): Promise<Repo>;
getSingleRepoById(repoId: Repo['id']): Promise<Repo>;
getSingleRepoByOwnerAndName(ownerType: Repo['ownerType'], ownerId: Repo['ownerId'], repoName: Repo['name']): Promise<Repo>;
getOwnedRepos(ownerType: Repo['ownerType'], ownerId: Repo['ownerId']): Promise<Repo[]>;
deleteRepo(repoId: Repo['id'], user?: User): Promise<void>;
getRefsFromService(
ownerType: Repo['ownerType'],
ownerName: Repo['name'],
repoName: Repo['name'],
service?: string,
user?: User,
): Promise<Stream>;
}

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<T extends Repo>(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<ChildProcess> {
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<void> {
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<Repo> {
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<Repo> {
// 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<Repo> {
const repo = await this.prismaClient.repo.findUnique({
where: {
id: repoId,
},
rejectOnNotFound: notFoundFactory(RepoNotFoundError),
})

return RepoServiceImpl.serialize(repo);
}

async getSingleRepoById(repoId: Repo['id']): Promise<Repo> {
return this.doGetSingleRepo(repoId);
}

private async doGetSingleRepoByOwnerAndName(
ownerType: Repo['ownerType'],
ownerId: Repo['ownerId'],
repoName: Repo['name']
): Promise<Repo> {
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<Repo> {
return this.doGetSingleRepoByOwnerAndName(ownerType, ownerId, repoName);
}

private async doDeleteRepo(repoName: string, ownerName: string, ownerType: OwnerType): Promise<void> {
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<void> {
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<Repo[]> {
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, GitServiceMethod> = {
[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<Stream>(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<Stream> {
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);
}
}

+ 4
- 0
src/modules/repo/index.ts 파일 보기

@@ -0,0 +1,4 @@
export * from './Repo.service'
export * from './Repo.controller'
export * from './models';
export * from './responses';

+ 30
- 0
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<Repo, 'id'>;

+ 11
- 0
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') {}

+ 30
- 0
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,
})
}

+ 40
- 0
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 })
}
}
}

+ 77
- 0
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<PublicUser>;

getFromCredentials(args: LoginUserFormData): Promise<PublicUser>;

getFromId(id: User['id']): Promise<PublicUser>;
}

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<PublicUser> {
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<PublicUser> {
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<PublicUser> {
const existingUser = await this.prismaClient.user.findUnique({
where: {
id,
},
select: {
id: true,
username: true,
},
rejectOnNotFound: notFoundFactory(UserNotFoundError),
});

return {
...existingUser,
id: Uuid.from(existingUser.id),
};
}
}

+ 4
- 0
src/modules/user/index.ts 파일 보기

@@ -0,0 +1,4 @@
export * from './models'
export * from './User.controller'
export * from './User.service'
export * from './responses'

+ 16
- 0
src/modules/user/models.ts 파일 보기

@@ -0,0 +1,16 @@
export interface User {
id: Buffer
username: string
password: string
}

export interface CreateUserData extends Omit<User, 'id'> {
}

export interface PublicUser extends Omit<User, 'password'> {
}

export interface RegisterUserFormData extends Pick<User, 'username'> {
newPassword: string;
confirmNewPassword: string;
}

+ 14
- 0
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<Session>(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') {}

+ 11
- 0
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,
});
}

+ 44
- 0
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<string, unknown>;
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<string, unknown>;
if (!mutableServer['allStaticRoutes']) {
mutableServer['allStaticRoutes'] = [];
}
})
}

export default fp(fastifyHomeRoute);

+ 7
- 2
src/packages/fastify-send-data/index.ts 파일 보기

@@ -8,6 +8,11 @@ export interface ResponseDataInterface<T extends unknown = undefined> {
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<T extends ResponseDataInterface>(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 = <T extends unknown = undefined>(
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,
}


+ 16
- 6
src/packages/fastify-service-session/index.ts 파일 보기

@@ -1,23 +1,33 @@
import fp from 'fastify-plugin'
import { FastifyInstance, FastifyRequest } from 'fastify'

export interface FastifySessionOpts<SessionType = unknown, SessionId = string, RequestType extends FastifyRequest = FastifyRequest> {
sessionRequestKey?: string,
export interface FastifySessionOpts<SessionType = unknown, SessionId = string, UserType = unknown, RequestType extends FastifyRequest = FastifyRequest> {
extractSessionId: (request: RequestType) => SessionId | null | undefined,
isSessionValid: (sessionId: SessionId) => Promise<boolean>,
getSession: (sessionId: SessionId) => Promise<SessionType>
getSession: (sessionId: SessionId) => Promise<SessionType>,
getUserFromSession?: (session: SessionType) => Promise<UserType>,
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<string, unknown>
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);
}
}
}
})


+ 7
- 0
src/packages/fastify-utils-theoryofnekomata/index.ts 파일 보기

@@ -0,0 +1,7 @@
import {FastifyInstance, RouteHandlerMethod} from 'fastify';

export type ServerInstance = FastifyInstance;

export type Controller<T extends string> = {
[key in T]: RouteHandlerMethod;
};

+ 1
- 0
src/packages/prisma-error-utils/index.ts 파일 보기

@@ -0,0 +1 @@
export const notFoundFactory = <T extends Error>(ErrorClass: { new(): T }) => () => new ErrorClass()

+ 13
- 18
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,
});
});
}

+ 129
- 5
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<FastifyRequest, 'query' | 'headers' | 'body'> {}

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, DecoderFunction> = {
[AvailableDecoder.GZIP]:() => createGunzip(),
[AvailableDecoder.DEFLATE]: () => createDeflate(),
}

const DEFAULT_DECODER: DecoderFunction = () => new PassThrough();

class PackRequestTransformStream extends Transform {
private data: Record<string, unknown> = {
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<string, unknown>
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<string, unknown> ?? {}
const bodySessionId = body[sessionIdKey]
if (typeof bodySessionId === 'string') {
return bodySessionId
}

server.register(fastifySendData)
return undefined
}
})
server.register(fastifySendData);

return server;
}

+ 0
- 7
src/utils/types.ts 파일 보기

@@ -1,7 +0,0 @@
import { RouteHandlerMethod } from 'fastify'

type Controller<T extends string> = {
[key in T]: RouteHandlerMethod;
};

export default Controller

+ 1
- 1
tsconfig.eslint.json 파일 보기

@@ -7,7 +7,7 @@
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./",
"baseUrl": ".",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,


+ 1
- 1
tsconfig.json 파일 보기

@@ -7,7 +7,7 @@
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./src",
"baseUrl": ".",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,


+ 237
- 12
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"


불러오는 중...
취소
저장