瀏覽代碼

Implement git smart server endpoints

The endpoints for creation, deletion, as well as refs advertisement have
been implemented.
master
TheoryOfNekomata 2 年之前
父節點
當前提交
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"


Loading…
取消
儲存