mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 22:38:56 +08:00
feat(server): port resolvers to node server (#2026)
Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
|
SECRET_KEY="secret"
|
||||||
DATABASE_URL="postgresql://affine@localhost:5432/affine"
|
DATABASE_URL="postgresql://affine@localhost:5432/affine"
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "google_users" (
|
|
||||||
"id" VARCHAR NOT NULL,
|
|
||||||
"user_id" VARCHAR NOT NULL,
|
|
||||||
"google_id" VARCHAR NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "google_users_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "permissions" (
|
|
||||||
"id" VARCHAR NOT NULL,
|
|
||||||
"workspace_id" VARCHAR NOT NULL,
|
|
||||||
"user_id" VARCHAR,
|
|
||||||
"user_email" TEXT,
|
|
||||||
"type" SMALLINT NOT NULL,
|
|
||||||
"accepted" BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "seaql_migrations" (
|
|
||||||
"version" VARCHAR NOT NULL,
|
|
||||||
"applied_at" BIGINT NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "seaql_migrations_pkey" PRIMARY KEY ("version")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "users" (
|
|
||||||
"id" VARCHAR NOT NULL,
|
|
||||||
"name" VARCHAR NOT NULL,
|
|
||||||
"email" VARCHAR NOT NULL,
|
|
||||||
"avatar_url" VARCHAR,
|
|
||||||
"token_nonce" SMALLINT DEFAULT 0,
|
|
||||||
"password" VARCHAR,
|
|
||||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "workspaces" (
|
|
||||||
"id" VARCHAR NOT NULL,
|
|
||||||
"public" BOOLEAN NOT NULL,
|
|
||||||
"type" SMALLINT NOT NULL,
|
|
||||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
CONSTRAINT "workspaces_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "google_users_google_id_key" ON "google_users"("google_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "google_users" ADD CONSTRAINT "google_users_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "permissions" ADD CONSTRAINT "permissions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "permissions" ADD CONSTRAINT "permissions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"id" VARCHAR NOT NULL,
|
||||||
|
"name" VARCHAR NOT NULL,
|
||||||
|
"email" VARCHAR NOT NULL,
|
||||||
|
"token_nonce" SMALLINT NOT NULL DEFAULT 0,
|
||||||
|
"avatar_url" VARCHAR,
|
||||||
|
"password" VARCHAR,
|
||||||
|
"fulfilled" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "workspaces" (
|
||||||
|
"id" VARCHAR NOT NULL,
|
||||||
|
"public" BOOLEAN NOT NULL,
|
||||||
|
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "workspaces_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "connected_accounts" (
|
||||||
|
"id" VARCHAR NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
"provider" VARCHAR NOT NULL,
|
||||||
|
"provider_user_id" VARCHAR NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "connected_accounts_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "user_workspace_permissions" (
|
||||||
|
"id" VARCHAR NOT NULL,
|
||||||
|
"workspace_id" VARCHAR NOT NULL,
|
||||||
|
"entity_id" VARCHAR NOT NULL,
|
||||||
|
"type" SMALLINT NOT NULL,
|
||||||
|
"accepted" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "user_workspace_permissions_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "connected_accounts_provider_user_id_key" ON "connected_accounts"("provider_user_id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "connected_accounts" ADD CONSTRAINT "connected_accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "user_workspace_permissions" ADD CONSTRAINT "user_workspace_permissions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "user_workspace_permissions" ADD CONSTRAINT "user_workspace_permissions_entity_id_fkey" FOREIGN KEY ("entity_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon ./src/index.ts",
|
"dev": "nodemon ./src/index.ts",
|
||||||
"test": "yarn exec ts-node-esm ./scripts/run-test.ts all",
|
"test": "yarn exec ts-node-esm ./scripts/run-test.ts all",
|
||||||
"test:coverage": "c8 yarn exec ts-node-esm ./scripts/run-test.ts all"
|
"test:coverage": "c8 yarn exec ts-node-esm ./scripts/run-test.ts all",
|
||||||
|
"postinstall": "prisma generate"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/server": "^4.7.0",
|
"@apollo/server": "^4.7.0",
|
||||||
@@ -21,8 +22,10 @@
|
|||||||
"@nestjs/platform-express": "^9.4.0",
|
"@nestjs/platform-express": "^9.4.0",
|
||||||
"@prisma/client": "^4.13.0",
|
"@prisma/client": "^4.13.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
|
"express": "^4.18.2",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"graphql-type-json": "^0.3.2",
|
"graphql-type-json": "^0.3.2",
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"prisma": "^4.13.0",
|
"prisma": "^4.13.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
@@ -30,6 +33,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/testing": "^9.4.0",
|
"@nestjs/testing": "^9.4.0",
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
"@types/lodash-es": "^4.17.7",
|
"@types/lodash-es": "^4.17.7",
|
||||||
"@types/node": "^18.16.2",
|
"@types/node": "^18.16.2",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
|
|||||||
+46
-35
@@ -7,46 +7,57 @@ generator client {
|
|||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
|
|
||||||
model google_users {
|
model User {
|
||||||
id String @id @db.VarChar
|
id String @id @default(uuid()) @db.VarChar
|
||||||
user_id String @db.VarChar
|
name String @db.VarChar
|
||||||
google_id String @unique @db.VarChar
|
email String @unique @db.VarChar
|
||||||
users users @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
tokenNonce Int @default(0) @map("token_nonce") @db.SmallInt
|
||||||
|
avatarUrl String? @map("avatar_url") @db.VarChar
|
||||||
|
/// Available if user signed up through OAuth providers
|
||||||
|
password String? @db.VarChar
|
||||||
|
/// User may created by email collobration invitation before signup.
|
||||||
|
/// We will precreate a user entity in such senarios but leave fulfilled as false until they signed up
|
||||||
|
/// This implementation is convenient for handing unregistered user permissoin
|
||||||
|
fulfilled Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
connectedAccounts ConnectedAccount[]
|
||||||
|
workspaces UserWorkspacePermission[]
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
||||||
model permissions {
|
model Workspace {
|
||||||
id String @id @db.VarChar
|
id String @id @default(uuid()) @db.VarChar
|
||||||
workspace_id String @db.VarChar
|
public Boolean
|
||||||
user_id String? @db.VarChar
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
user_email String?
|
users UserWorkspacePermission[]
|
||||||
type Int @db.SmallInt
|
|
||||||
accepted Boolean @default(false)
|
@@map("workspaces")
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
|
||||||
users users? @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
||||||
workspaces workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model seaql_migrations {
|
model ConnectedAccount {
|
||||||
version String @id @db.VarChar
|
id String @id @default(uuid()) @db.VarChar
|
||||||
applied_at BigInt
|
userId String @map("user_id")
|
||||||
|
/// the general provider name, e.g. google, github, facebook
|
||||||
|
provider String @db.VarChar
|
||||||
|
/// the user id provided by OAuth providers, or other user identitive credential like `username` provided by GitHub
|
||||||
|
providerUserId String @unique @map("provider_user_id") @db.VarChar
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("connected_accounts")
|
||||||
}
|
}
|
||||||
|
|
||||||
model users {
|
model UserWorkspacePermission {
|
||||||
id String @id @db.VarChar
|
id String @id @default(uuid()) @db.VarChar
|
||||||
name String @db.VarChar
|
workspaceId String @map("workspace_id") @db.VarChar
|
||||||
email String @unique @db.VarChar
|
userId String @map("entity_id") @db.VarChar
|
||||||
avatar_url String? @db.VarChar
|
/// Read/Write/Admin/Owner
|
||||||
token_nonce Int? @default(0) @db.SmallInt
|
type Int @db.SmallInt
|
||||||
password String? @db.VarChar
|
/// Whether the permission invitation is accepted by the user
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
accepted Boolean @default(false)
|
||||||
google_users google_users[]
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
permissions permissions[]
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
}
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
model workspaces {
|
@@map("user_workspace_permissions")
|
||||||
id String @id @db.VarChar
|
|
||||||
public Boolean
|
|
||||||
type Int @db.SmallInt
|
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
|
||||||
permissions permissions[]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
|
||||||
|
|
||||||
import userA from '@affine-test/fixtures/userA.json' assert { type: 'json' };
|
import userA from '@affine-test/fixtures/userA.json' assert { type: 'json' };
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
await prisma.users.create({
|
await prisma.user.create({
|
||||||
data: {
|
data: userA,
|
||||||
id: randomUUID(),
|
|
||||||
...userA,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/// <reference types="./global.d.ts" />
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ConfigModule } from './config';
|
import { ConfigModule } from './config';
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ export function parseEnvValue(value: string | undefined, type?: EnvConfigType) {
|
|||||||
*/
|
*/
|
||||||
export interface AFFiNEConfig {
|
export interface AFFiNEConfig {
|
||||||
ENV_MAP: Record<string, ConfigPaths | [ConfigPaths, EnvConfigType?]>;
|
ENV_MAP: Record<string, ConfigPaths | [ConfigPaths, EnvConfigType?]>;
|
||||||
|
/**
|
||||||
|
* Application sign key secret
|
||||||
|
*/
|
||||||
|
readonly secret: string;
|
||||||
/**
|
/**
|
||||||
* System version
|
* System version
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import pkg from '../../package.json' assert { type: 'json' };
|
|||||||
import type { AFFiNEConfig } from './def';
|
import type { AFFiNEConfig } from './def';
|
||||||
|
|
||||||
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
|
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
|
||||||
|
secret: 'secret',
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
ENV_MAP: {},
|
ENV_MAP: {},
|
||||||
env: process.env.NODE_ENV ?? 'development',
|
env: process.env.NODE_ENV ?? 'development',
|
||||||
|
|||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
declare namespace Express {
|
||||||
|
interface Request {
|
||||||
|
user?: import('@prisma/client').User | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
createParamDecorator,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PrismaService } from '../../prisma';
|
||||||
|
import { getRequestResponseFromContext } from '../../utils/nestjs';
|
||||||
|
import { AuthService } from './service';
|
||||||
|
|
||||||
|
export function getUserFromContext(context: ExecutionContext) {
|
||||||
|
const req = getRequestResponseFromContext(context).req;
|
||||||
|
return req.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to fetch current user from the request context.
|
||||||
|
*
|
||||||
|
* > The user may be undefined if authorization token is not provided.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* // Graphql Query
|
||||||
|
* \@Query(() => UserType)
|
||||||
|
* user(@CurrentUser() user?: User) {
|
||||||
|
* return user;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* // HTTP Controller
|
||||||
|
* \@Get('/user)
|
||||||
|
* user(@CurrentUser() user?: User) {
|
||||||
|
* return user;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const CurrentUser = createParamDecorator(
|
||||||
|
(_: unknown, context: ExecutionContext) => {
|
||||||
|
return getUserFromContext(context);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
class AuthGuard implements CanActivate {
|
||||||
|
constructor(private auth: AuthService, private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext) {
|
||||||
|
const { req } = getRequestResponseFromContext(context);
|
||||||
|
const token = req.headers.authorization;
|
||||||
|
if (!token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const claims = this.auth.verify(token);
|
||||||
|
req.user = await this.prisma.user.findUnique({ where: { id: claims.id } });
|
||||||
|
return !!req.user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This guard is used to protect routes/queries/mutations that require a user to be logged in.
|
||||||
|
*
|
||||||
|
* The `@CurrentUser()` parameter decorator used in a `Auth` guarded queries would always give us the user because the `Auth` guard will
|
||||||
|
* fast throw if user is not logged in.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* \@Auth()
|
||||||
|
* \@Query(() => UserType)
|
||||||
|
* user(@CurrentUser() user: User) {
|
||||||
|
* return user;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const Auth = () => {
|
||||||
|
return UseGuards(AuthGuard);
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { AuthResolver } from './resolver';
|
||||||
|
import { AuthService } from './service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [AuthService, AuthResolver],
|
||||||
|
exports: [AuthService],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
|
export * from './guard';
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { ForbiddenException } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
Args,
|
||||||
|
Context,
|
||||||
|
Field,
|
||||||
|
Mutation,
|
||||||
|
ObjectType,
|
||||||
|
Parent,
|
||||||
|
ResolveField,
|
||||||
|
Resolver,
|
||||||
|
} from '@nestjs/graphql';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
import { UserType } from '../users/resolver';
|
||||||
|
import { CurrentUser } from './guard';
|
||||||
|
import { AuthService } from './service';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class TokenType {
|
||||||
|
@Field()
|
||||||
|
token!: string;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
refresh!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Resolver(() => UserType)
|
||||||
|
export class AuthResolver {
|
||||||
|
constructor(private auth: AuthService) {}
|
||||||
|
|
||||||
|
@ResolveField(() => TokenType)
|
||||||
|
token(@CurrentUser() currentUser: UserType, @Parent() user: UserType) {
|
||||||
|
if (user !== currentUser) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: this.auth.sign(user),
|
||||||
|
// TODO: impl
|
||||||
|
refresh: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => UserType)
|
||||||
|
async signIn(
|
||||||
|
@Context() ctx: { req: Request },
|
||||||
|
@Args('email') email: string,
|
||||||
|
@Args('password') password: string
|
||||||
|
) {
|
||||||
|
const user = await this.auth.signIn(email, password);
|
||||||
|
ctx.req.user = user;
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { User } from '@prisma/client';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
import { Config } from '../../config';
|
||||||
|
import { PrismaService } from '../../prisma';
|
||||||
|
|
||||||
|
type UserClaim = Pick<User, 'id' | 'name' | 'email'>;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(private config: Config, private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
sign(user: UserClaim) {
|
||||||
|
return jwt.sign(user, this.config.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(token: string) {
|
||||||
|
try {
|
||||||
|
const claims = jwt.verify(token, this.config.secret) as UserClaim;
|
||||||
|
return claims;
|
||||||
|
} catch (e) {
|
||||||
|
throw new UnauthorizedException('Invalid token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async signIn(email: string, password: string) {
|
||||||
|
const user = await this.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new BadRequestException('Invalid email or password');
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
|
import { AuthModule } from './auth';
|
||||||
import { UsersModule } from './users';
|
import { UsersModule } from './users';
|
||||||
import { WorkspaceModule } from './workspaces';
|
import { WorkspaceModule } from './workspaces';
|
||||||
|
|
||||||
export const BusinessModules = [WorkspaceModule, UsersModule];
|
export const BusinessModules = [AuthModule, WorkspaceModule, UsersModule];
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
import { Args, Field, ID, ObjectType, Query, Resolver } from '@nestjs/graphql';
|
import { Args, Field, ID, ObjectType, Query, Resolver } from '@nestjs/graphql';
|
||||||
import type { users } from '@prisma/client';
|
import type { User } from '@prisma/client';
|
||||||
|
|
||||||
import { PrismaService } from '../../prisma/service';
|
import { PrismaService } from '../../prisma/service';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class User implements users {
|
export class UserType implements Partial<User> {
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Field({ description: 'User name' })
|
@Field({ description: 'User name' })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@Field({ description: 'User email' })
|
@Field({ description: 'User email' })
|
||||||
email!: string;
|
email!: string;
|
||||||
@Field({ description: 'User password', nullable: true })
|
|
||||||
password!: string;
|
|
||||||
@Field({ description: 'User avatar url', nullable: true })
|
@Field({ description: 'User avatar url', nullable: true })
|
||||||
avatar_url!: string;
|
avatarUrl!: string;
|
||||||
@Field({ description: 'User token nonce', nullable: true })
|
|
||||||
token_nonce!: number;
|
|
||||||
@Field({ description: 'User created date', nullable: true })
|
@Field({ description: 'User created date', nullable: true })
|
||||||
created_at!: Date;
|
createdAt!: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Resolver(() => User)
|
@Resolver(() => UserType)
|
||||||
export class UserResolver {
|
export class UserResolver {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
@Query(() => User, {
|
@Query(() => UserType, {
|
||||||
name: 'user',
|
name: 'user',
|
||||||
description: 'Get user by email',
|
description: 'Get user by email',
|
||||||
})
|
})
|
||||||
async user(@Args('email') email: string) {
|
async user(@Args('email') email: string) {
|
||||||
return this.prisma.users.findUnique({
|
return this.prisma.user.findUnique({
|
||||||
where: { email },
|
where: { email },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PermissionService } from './permission';
|
||||||
import { WorkspaceResolver } from './resolver';
|
import { WorkspaceResolver } from './resolver';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [WorkspaceResolver],
|
providers: [WorkspaceResolver, PermissionService],
|
||||||
|
exports: [PermissionService],
|
||||||
})
|
})
|
||||||
export class WorkspaceModule {}
|
export class WorkspaceModule {}
|
||||||
|
export { WorkspaceType } from './resolver';
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
import { PrismaService } from '../../prisma';
|
||||||
|
import { Permission } from './types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PermissionService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async get(ws: string, user: string) {
|
||||||
|
const data = await this.prisma.userWorkspacePermission.findFirst({
|
||||||
|
where: {
|
||||||
|
workspaceId: ws,
|
||||||
|
userId: user,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return data?.type as Permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
async check(
|
||||||
|
ws: string,
|
||||||
|
user: string,
|
||||||
|
permission: Permission = Permission.Read
|
||||||
|
) {
|
||||||
|
if (!(await this.tryCheck(ws, user, permission))) {
|
||||||
|
throw new ForbiddenException('Permission denied');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async tryCheck(
|
||||||
|
ws: string,
|
||||||
|
user: string,
|
||||||
|
permission: Permission = Permission.Read
|
||||||
|
) {
|
||||||
|
const data = await this.prisma.userWorkspacePermission.count({
|
||||||
|
where: {
|
||||||
|
workspaceId: ws,
|
||||||
|
userId: user,
|
||||||
|
accepted: true,
|
||||||
|
type: {
|
||||||
|
gte: permission,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the permission is read, we should check if the workspace is public
|
||||||
|
if (permission === Permission.Read) {
|
||||||
|
const data = await this.prisma.workspace.count({
|
||||||
|
where: { id: ws, public: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return data > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async grant(
|
||||||
|
ws: string,
|
||||||
|
user: string,
|
||||||
|
permission: Permission = Permission.Read
|
||||||
|
) {
|
||||||
|
const data = await this.prisma.userWorkspacePermission.findFirst({
|
||||||
|
where: {
|
||||||
|
workspaceId: ws,
|
||||||
|
userId: user,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const [p] = await this.prisma.$transaction(
|
||||||
|
[
|
||||||
|
this.prisma.userWorkspacePermission.update({
|
||||||
|
where: {
|
||||||
|
id: data.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: permission,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// If the new permission is owner, we need to revoke old owner
|
||||||
|
permission === Permission.Owner
|
||||||
|
? this.prisma.userWorkspacePermission.updateMany({
|
||||||
|
where: {
|
||||||
|
workspaceId: ws,
|
||||||
|
type: Permission.Owner,
|
||||||
|
userId: {
|
||||||
|
not: user,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Permission.Admin,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
].filter(Boolean) as Prisma.PrismaPromise<any>[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.userWorkspacePermission.create({
|
||||||
|
data: {
|
||||||
|
workspaceId: ws,
|
||||||
|
userId: user,
|
||||||
|
type: permission,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async revoke(ws: string, user: string) {
|
||||||
|
const result = await this.prisma.userWorkspacePermission.deleteMany({
|
||||||
|
where: {
|
||||||
|
workspaceId: ws,
|
||||||
|
userId: user,
|
||||||
|
type: {
|
||||||
|
// We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading
|
||||||
|
not: Permission.Owner,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,85 +1,206 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Args,
|
Args,
|
||||||
Field,
|
Field,
|
||||||
ID,
|
ID,
|
||||||
|
InputType,
|
||||||
|
Int,
|
||||||
Mutation,
|
Mutation,
|
||||||
ObjectType,
|
ObjectType,
|
||||||
|
Parent,
|
||||||
|
PartialType,
|
||||||
|
PickType,
|
||||||
Query,
|
Query,
|
||||||
registerEnumType,
|
registerEnumType,
|
||||||
|
ResolveField,
|
||||||
Resolver,
|
Resolver,
|
||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
import type { workspaces } from '@prisma/client';
|
import type { User, Workspace } from '@prisma/client';
|
||||||
|
|
||||||
import { PrismaService } from '../../prisma/service';
|
import { PrismaService } from '../../prisma';
|
||||||
|
import { Auth, CurrentUser } from '../auth';
|
||||||
|
import { UserType } from '../users/resolver';
|
||||||
|
import { PermissionService } from './permission';
|
||||||
|
import { Permission } from './types';
|
||||||
|
|
||||||
export enum WorkspaceType {
|
registerEnumType(Permission, {
|
||||||
Private = 0,
|
name: 'Permission',
|
||||||
Normal = 1,
|
description: 'User permission in workspace',
|
||||||
}
|
|
||||||
|
|
||||||
registerEnumType(WorkspaceType, {
|
|
||||||
name: 'WorkspaceType',
|
|
||||||
description: 'Workspace type',
|
|
||||||
valuesMap: {
|
|
||||||
Normal: {
|
|
||||||
description: 'Normal workspace',
|
|
||||||
},
|
|
||||||
Private: {
|
|
||||||
description: 'Private workspace',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class Workspace implements workspaces {
|
export class WorkspaceType implements Partial<Workspace> {
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Field({ description: 'is Public workspace' })
|
@Field({ description: 'is Public workspace' })
|
||||||
public!: boolean;
|
public!: boolean;
|
||||||
@Field(() => WorkspaceType, { description: 'Workspace type' })
|
|
||||||
type!: WorkspaceType;
|
|
||||||
@Field({ description: 'Workspace created date' })
|
@Field({ description: 'Workspace created date' })
|
||||||
created_at!: Date;
|
createdAt!: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Resolver(() => Workspace)
|
@InputType()
|
||||||
|
export class UpdateWorkspaceInput extends PickType(
|
||||||
|
PartialType(WorkspaceType),
|
||||||
|
['public'],
|
||||||
|
InputType
|
||||||
|
) {
|
||||||
|
@Field(() => ID)
|
||||||
|
id!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Auth()
|
||||||
|
@Resolver(() => WorkspaceType)
|
||||||
export class WorkspaceResolver {
|
export class WorkspaceResolver {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly permissionProvider: PermissionService
|
||||||
|
) {}
|
||||||
|
|
||||||
// debug only query should be removed
|
@ResolveField(() => Permission, {
|
||||||
@Query(() => [Workspace], {
|
description: 'Permission of current signed in user in workspace',
|
||||||
name: 'workspaces',
|
complexity: 2,
|
||||||
description: 'Get all workspaces',
|
|
||||||
})
|
})
|
||||||
async workspaces() {
|
async permission(
|
||||||
return this.prisma.workspaces.findMany();
|
@CurrentUser() user: User,
|
||||||
|
@Parent() workspace: WorkspaceType
|
||||||
|
) {
|
||||||
|
// may applied in workspaces query
|
||||||
|
if ('permission' in workspace) {
|
||||||
|
return workspace.permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = this.permissionProvider.get(workspace.id, user.id);
|
||||||
|
|
||||||
|
if (!permission) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => Workspace, {
|
@ResolveField(() => Int, {
|
||||||
name: 'workspace',
|
description: 'member count of workspace',
|
||||||
description: 'Get workspace by id',
|
complexity: 2,
|
||||||
})
|
})
|
||||||
async workspace(@Args('id') id: string) {
|
memberCount(@Parent() workspace: WorkspaceType) {
|
||||||
return this.prisma.workspaces.findUnique({
|
return this.prisma.userWorkspacePermission.count({
|
||||||
where: { id },
|
where: {
|
||||||
});
|
workspaceId: workspace.id,
|
||||||
}
|
accepted: true,
|
||||||
|
|
||||||
// create workspace
|
|
||||||
@Mutation(() => Workspace, {
|
|
||||||
name: 'createWorkspace',
|
|
||||||
description: 'Create workspace',
|
|
||||||
})
|
|
||||||
async createWorkspace() {
|
|
||||||
return this.prisma.workspaces.create({
|
|
||||||
data: {
|
|
||||||
id: randomUUID(),
|
|
||||||
type: WorkspaceType.Private,
|
|
||||||
public: false,
|
|
||||||
created_at: new Date(),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => UserType, {
|
||||||
|
description: 'Owner of workspace',
|
||||||
|
complexity: 2,
|
||||||
|
})
|
||||||
|
async owner(@Parent() workspace: WorkspaceType) {
|
||||||
|
const data = await this.prisma.userWorkspacePermission.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
type: Permission.Owner,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(() => [WorkspaceType], {
|
||||||
|
description: 'Get all accessible workspaces for current user',
|
||||||
|
complexity: 2,
|
||||||
|
})
|
||||||
|
async workspaces(@CurrentUser() user: User) {
|
||||||
|
const data = await this.prisma.userWorkspacePermission.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
workspace: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.map(({ workspace, type }) => {
|
||||||
|
return {
|
||||||
|
...workspace,
|
||||||
|
permission: type,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(() => WorkspaceType, {
|
||||||
|
description: 'Get workspace by id',
|
||||||
|
})
|
||||||
|
async workspace(@CurrentUser() user: UserType, @Args('id') id: string) {
|
||||||
|
await this.permissionProvider.check(id, user.id);
|
||||||
|
const workspace = await this.prisma.workspace.findUnique({ where: { id } });
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new NotFoundException("Workspace doesn't exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return workspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => WorkspaceType, {
|
||||||
|
description: 'Create a new workspace',
|
||||||
|
})
|
||||||
|
async createWorkspace(@CurrentUser() user: User) {
|
||||||
|
return this.prisma.workspace.create({
|
||||||
|
data: {
|
||||||
|
public: false,
|
||||||
|
users: {
|
||||||
|
create: {
|
||||||
|
type: Permission.Owner,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => WorkspaceType, {
|
||||||
|
description: 'Update workspace',
|
||||||
|
})
|
||||||
|
async updateWorkspace(
|
||||||
|
@CurrentUser() user: User,
|
||||||
|
@Args({ name: 'input', type: () => UpdateWorkspaceInput })
|
||||||
|
{ id, ...updates }: UpdateWorkspaceInput
|
||||||
|
) {
|
||||||
|
await this.permissionProvider.check('id', user.id, Permission.Admin);
|
||||||
|
|
||||||
|
return this.prisma.workspace.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
data: updates,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => Boolean)
|
||||||
|
async deleteWorkspace(@CurrentUser() user: User, @Args('id') id: string) {
|
||||||
|
await this.permissionProvider.check(id, user.id, Permission.Owner);
|
||||||
|
|
||||||
|
await this.prisma.workspace.delete({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// delete all related data, like websocket connections, blobs, etc.
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export enum Permission {
|
||||||
|
Read = 0,
|
||||||
|
Write = 1,
|
||||||
|
Admin = 10,
|
||||||
|
Owner = 99,
|
||||||
|
}
|
||||||
@@ -8,3 +8,4 @@ import { PrismaService } from './service';
|
|||||||
exports: [PrismaService],
|
exports: [PrismaService],
|
||||||
})
|
})
|
||||||
export class PrismaModule {}
|
export class PrismaModule {}
|
||||||
|
export { PrismaService } from './service';
|
||||||
|
|||||||
@@ -34,22 +34,46 @@ describe('AppModule', () => {
|
|||||||
.post(gql)
|
.post(gql)
|
||||||
.send({
|
.send({
|
||||||
query: `
|
query: `
|
||||||
query {
|
query {
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
.expect(400);
|
.expect(400);
|
||||||
|
|
||||||
|
let token;
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.post(gql)
|
.post(gql)
|
||||||
|
.send({
|
||||||
|
query: `
|
||||||
|
mutation {
|
||||||
|
signIn(email: "alex.yang@example.org", password: "123456") {
|
||||||
|
token {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
ok(
|
||||||
|
typeof res.body.data.signIn.token.token === 'string',
|
||||||
|
'res.body.data.signIn.token.token is not a string'
|
||||||
|
);
|
||||||
|
token = res.body.data.signIn.token.token;
|
||||||
|
});
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(gql)
|
||||||
|
.set({ Authorization: token })
|
||||||
.send({
|
.send({
|
||||||
query: `
|
query: `
|
||||||
mutation {
|
mutation {
|
||||||
createWorkspace {
|
createWorkspace {
|
||||||
id
|
id
|
||||||
type
|
|
||||||
public
|
public
|
||||||
created_at
|
createdAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -64,16 +88,12 @@ describe('AppModule', () => {
|
|||||||
typeof res.body.data.createWorkspace.id === 'string',
|
typeof res.body.data.createWorkspace.id === 'string',
|
||||||
'res.body.data.createWorkspace.id is not a string'
|
'res.body.data.createWorkspace.id is not a string'
|
||||||
);
|
);
|
||||||
ok(
|
|
||||||
typeof res.body.data.createWorkspace.type === 'string',
|
|
||||||
'res.body.data.createWorkspace.type is not a string'
|
|
||||||
);
|
|
||||||
ok(
|
ok(
|
||||||
typeof res.body.data.createWorkspace.public === 'boolean',
|
typeof res.body.data.createWorkspace.public === 'boolean',
|
||||||
'res.body.data.createWorkspace.public is not a boolean'
|
'res.body.data.createWorkspace.public is not a boolean'
|
||||||
);
|
);
|
||||||
ok(
|
ok(
|
||||||
typeof res.body.data.createWorkspace.created_at === 'string',
|
typeof res.body.data.createWorkspace.createdAt === 'string',
|
||||||
'res.body.data.createWorkspace.created_at is not a string'
|
'res.body.data.createWorkspace.created_at is not a string'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -87,7 +107,7 @@ describe('AppModule', () => {
|
|||||||
query {
|
query {
|
||||||
user(email: "alex.yang@example.org") {
|
user(email: "alex.yang@example.org") {
|
||||||
email
|
email
|
||||||
avatar_url
|
avatarUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { ArgumentsHost, ExecutionContext } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
GqlArgumentsHost,
|
||||||
|
GqlContextType,
|
||||||
|
GqlExecutionContext,
|
||||||
|
} from '@nestjs/graphql';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
export function getRequestResponseFromContext(context: ExecutionContext) {
|
||||||
|
switch (context.getType<GqlContextType>()) {
|
||||||
|
case 'graphql': {
|
||||||
|
const gqlContext = GqlExecutionContext.create(context).getContext<{
|
||||||
|
req: Request;
|
||||||
|
}>();
|
||||||
|
return {
|
||||||
|
req: gqlContext.req,
|
||||||
|
res: gqlContext.req.res!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'http': {
|
||||||
|
const http = context.switchToHttp();
|
||||||
|
return {
|
||||||
|
req: http.getRequest<Request>(),
|
||||||
|
res: http.getResponse<Response>(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown context type for getting request and response');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRequestResponseFromHost(host: ArgumentsHost) {
|
||||||
|
switch (host.getType<GqlContextType>()) {
|
||||||
|
case 'graphql': {
|
||||||
|
const gqlContext = GqlArgumentsHost.create(host).getContext<{
|
||||||
|
req: Request;
|
||||||
|
}>();
|
||||||
|
return {
|
||||||
|
req: gqlContext.req,
|
||||||
|
res: gqlContext.req.res!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'http': {
|
||||||
|
const http = host.switchToHttp();
|
||||||
|
return {
|
||||||
|
req: http.getRequest<Request>(),
|
||||||
|
res: http.getResponse<Response>(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown host type for getting request and response');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRequestFromHost(host: ArgumentsHost) {
|
||||||
|
return getRequestResponseFromHost(host).req;
|
||||||
|
}
|
||||||
@@ -7,5 +7,5 @@
|
|||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true
|
||||||
},
|
},
|
||||||
"include": ["./scripts", "package.json"]
|
"include": ["scripts", "package.json"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,9 @@
|
|||||||
{
|
{
|
||||||
"path": "./apps/web"
|
"path": "./apps/web"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "./apps/server"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "./packages/component"
|
"path": "./packages/component"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -248,13 +248,17 @@ __metadata:
|
|||||||
"@nestjs/platform-express": ^9.4.0
|
"@nestjs/platform-express": ^9.4.0
|
||||||
"@nestjs/testing": ^9.4.0
|
"@nestjs/testing": ^9.4.0
|
||||||
"@prisma/client": ^4.13.0
|
"@prisma/client": ^4.13.0
|
||||||
|
"@types/express": ^4.17.17
|
||||||
|
"@types/jsonwebtoken": ^9.0.1
|
||||||
"@types/lodash-es": ^4.17.7
|
"@types/lodash-es": ^4.17.7
|
||||||
"@types/node": ^18.16.2
|
"@types/node": ^18.16.2
|
||||||
"@types/supertest": ^2.0.12
|
"@types/supertest": ^2.0.12
|
||||||
c8: ^7.13.0
|
c8: ^7.13.0
|
||||||
dotenv: ^16.0.3
|
dotenv: ^16.0.3
|
||||||
|
express: ^4.18.2
|
||||||
graphql: ^16.6.0
|
graphql: ^16.6.0
|
||||||
graphql-type-json: ^0.3.2
|
graphql-type-json: ^0.3.2
|
||||||
|
jsonwebtoken: ^9.0.0
|
||||||
lodash-es: ^4.17.21
|
lodash-es: ^4.17.21
|
||||||
nodemon: ^2.0.22
|
nodemon: ^2.0.22
|
||||||
prisma: ^4.13.0
|
prisma: ^4.13.0
|
||||||
@@ -8269,7 +8273,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/express@npm:^4.17.13, @types/express@npm:^4.7.0":
|
"@types/express@npm:^4.17.13, @types/express@npm:^4.17.17, @types/express@npm:^4.7.0":
|
||||||
version: 4.17.17
|
version: 4.17.17
|
||||||
resolution: "@types/express@npm:4.17.17"
|
resolution: "@types/express@npm:4.17.17"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8438,6 +8442,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/jsonwebtoken@npm:^9.0.1":
|
||||||
|
version: 9.0.1
|
||||||
|
resolution: "@types/jsonwebtoken@npm:9.0.1"
|
||||||
|
dependencies:
|
||||||
|
"@types/node": "*"
|
||||||
|
checksum: a7f0925e9a42ad3ae970364c63c5986d40da5c83d51d3f4e624eb0f064a380376f9e3fb3f2f837390a9ab80143f5d75fd51866da30e110f6b486a3379e1c768f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/keyv@npm:^3.1.4":
|
"@types/keyv@npm:^3.1.4":
|
||||||
version: 3.1.4
|
version: 3.1.4
|
||||||
resolution: "@types/keyv@npm:3.1.4"
|
resolution: "@types/keyv@npm:3.1.4"
|
||||||
@@ -10609,6 +10622,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"buffer-equal-constant-time@npm:1.0.1":
|
||||||
|
version: 1.0.1
|
||||||
|
resolution: "buffer-equal-constant-time@npm:1.0.1"
|
||||||
|
checksum: 80bb945f5d782a56f374b292770901065bad21420e34936ecbe949e57724b4a13874f735850dd1cc61f078773c4fb5493a41391e7bda40d1fa388d6bd80daaab
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"buffer-equal@npm:^1.0.0":
|
"buffer-equal@npm:^1.0.0":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "buffer-equal@npm:1.0.1"
|
resolution: "buffer-equal@npm:1.0.1"
|
||||||
@@ -12575,6 +12595,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ecdsa-sig-formatter@npm:1.0.11":
|
||||||
|
version: 1.0.11
|
||||||
|
resolution: "ecdsa-sig-formatter@npm:1.0.11"
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: ^5.0.1
|
||||||
|
checksum: 207f9ab1c2669b8e65540bce29506134613dd5f122cccf1e6a560f4d63f2732d427d938f8481df175505aad94583bcb32c688737bb39a6df0625f903d6d93c03
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"ee-first@npm:1.1.1":
|
"ee-first@npm:1.1.1":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "ee-first@npm:1.1.1"
|
resolution: "ee-first@npm:1.1.1"
|
||||||
@@ -13749,7 +13778,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"express@npm:4.18.2, express@npm:^4.17.1, express@npm:^4.17.3":
|
"express@npm:4.18.2, express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.2":
|
||||||
version: 4.18.2
|
version: 4.18.2
|
||||||
resolution: "express@npm:4.18.2"
|
resolution: "express@npm:4.18.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17588,6 +17617,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"jsonwebtoken@npm:^9.0.0":
|
||||||
|
version: 9.0.0
|
||||||
|
resolution: "jsonwebtoken@npm:9.0.0"
|
||||||
|
dependencies:
|
||||||
|
jws: ^3.2.2
|
||||||
|
lodash: ^4.17.21
|
||||||
|
ms: ^2.1.1
|
||||||
|
semver: ^7.3.8
|
||||||
|
checksum: b9181cecf9df99f1dc0253f91ba000a1aa4d91f5816d1608c0dba61a5623726a0bfe200b51df25de18c1a6000825d231ad7ce2788aa54fd48dcb760ad9eb9514
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.3.3":
|
"jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.3.3":
|
||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
resolution: "jsx-ast-utils@npm:3.3.3"
|
resolution: "jsx-ast-utils@npm:3.3.3"
|
||||||
@@ -17605,6 +17646,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"jwa@npm:^1.4.1":
|
||||||
|
version: 1.4.1
|
||||||
|
resolution: "jwa@npm:1.4.1"
|
||||||
|
dependencies:
|
||||||
|
buffer-equal-constant-time: 1.0.1
|
||||||
|
ecdsa-sig-formatter: 1.0.11
|
||||||
|
safe-buffer: ^5.0.1
|
||||||
|
checksum: ff30ea7c2dcc61f3ed2098d868bf89d43701605090c5b21b5544b512843ec6fd9e028381a4dda466cbcdb885c2d1150f7c62e7168394ee07941b4098e1035e2f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"jws@npm:^3.2.2":
|
||||||
|
version: 3.2.2
|
||||||
|
resolution: "jws@npm:3.2.2"
|
||||||
|
dependencies:
|
||||||
|
jwa: ^1.4.1
|
||||||
|
safe-buffer: ^5.0.1
|
||||||
|
checksum: f0213fe5b79344c56cd443428d8f65c16bf842dc8cb8f5aed693e1e91d79c20741663ad6eff07a6d2c433d1831acc9814e8d7bada6a0471fbb91d09ceb2bf5c2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"keyv@npm:^4.0.0, keyv@npm:^4.5.2":
|
"keyv@npm:^4.0.0, keyv@npm:^4.5.2":
|
||||||
version: 4.5.2
|
version: 4.5.2
|
||||||
resolution: "keyv@npm:4.5.2"
|
resolution: "keyv@npm:4.5.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user