refactor(server): auth (#5895)

Remove `next-auth` and implement our own Authorization/Authentication system from scratch.

## Server

- [x] tokens
  - [x] function
  - [x] encryption

- [x] AuthController
  - [x] /api/auth/sign-in
  - [x] /api/auth/sign-out
  - [x] /api/auth/session
  - [x] /api/auth/session (WE SUPPORT MULTI-ACCOUNT!)

- [x] OAuthPlugin
  - [x] OAuthController
  - [x] /oauth/login
  - [x] /oauth/callback
  - [x] Providers
    - [x] Google
    - [x] GitHub

## Client

- [x] useSession
- [x] cloudSignIn
- [x] cloudSignOut

## NOTE:

Tests will be adding in the future
This commit is contained in:
liuyi
2024-03-12 10:00:09 +00:00
parent af49e8cc41
commit fb3a0e7b8f
148 changed files with 3407 additions and 2851 deletions

View File

@@ -1,16 +1,8 @@
import { ok } from 'node:assert';
import { randomUUID } from 'node:crypto';
import { Transformer } from '@napi-rs/image';
import type { INestApplication } from '@nestjs/common';
import { hashSync } from '@node-rs/argon2';
import { PrismaClient, type User } from '@prisma/client';
import ava, { type TestFn } from 'ava';
import type { Express } from 'express';
import request from 'supertest';
import { AppModule } from '../src/app.module';
import { FeatureManagementService } from '../src/core/features';
import { createTestingApp } from './utils';
const gql = '/graphql';
@@ -19,43 +11,9 @@ const test = ava as TestFn<{
app: INestApplication;
}>;
class FakePrisma {
fakeUser: User = {
id: randomUUID(),
name: 'Alex Yang',
avatarUrl: '',
email: 'alex.yang@example.org',
password: hashSync('123456'),
emailVerified: new Date(),
createdAt: new Date(),
};
get user() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const prisma = this;
return {
async findFirst() {
return prisma.fakeUser;
},
async findUnique() {
return this.findFirst();
},
async update() {
return this.findFirst();
},
};
}
}
test.beforeEach(async t => {
const { app } = await createTestingApp({
imports: [AppModule],
tapModule(builder) {
builder
.overrideProvider(PrismaClient)
.useClass(FakePrisma)
.overrideProvider(FeatureManagementService)
.useValue({ canEarlyAccess: () => true });
},
});
t.context.app = app;
@@ -66,7 +24,6 @@ test.afterEach.always(async t => {
});
test('should init app', async t => {
t.is(typeof t.context.app, 'object');
await request(t.context.app.getHttpServer())
.post(gql)
.send({
@@ -78,130 +35,22 @@ test('should init app', async t => {
})
.expect(400);
const { token } = await createToken(t.context.app);
await request(t.context.app.getHttpServer())
const response = await request(t.context.app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.send({
query: `
query {
__typename
}
`,
})
.expect(200)
.expect(res => {
t.is(res.body.data.__typename, 'Query');
});
});
test('should find default user', async t => {
const { token } = await createToken(t.context.app);
await request(t.context.app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.send({
query: `
query {
user(email: "alex.yang@example.org") {
... on UserType {
email
}
... on LimitedUserType {
email
}
query: `query {
serverConfig {
name
version
type
features
}
}
`,
}`,
})
.expect(200)
.expect(res => {
t.is(res.body.data.user.email, 'alex.yang@example.org');
});
.expect(200);
const config = response.body.data.serverConfig;
t.is(config.type, 'Affine');
t.true(Array.isArray(config.features));
});
test('should be able to upload avatar and remove it', async t => {
const { token, id } = await createToken(t.context.app);
const png = await Transformer.fromRgbaPixels(
Buffer.alloc(400 * 400 * 4).fill(255),
400,
400
).png();
await request(t.context.app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.field(
'operations',
JSON.stringify({
name: 'uploadAvatar',
query: `mutation uploadAvatar($avatar: Upload!) {
uploadAvatar(avatar: $avatar) {
id
name
avatarUrl
email
}
}
`,
variables: { id, avatar: null },
})
)
.field('map', JSON.stringify({ '0': ['variables.avatar'] }))
.attach('0', png, 'avatar.png')
.expect(200)
.expect(res => {
t.is(res.body.data.uploadAvatar.id, id);
});
await request(t.context.app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation removeAvatar {
removeAvatar {
success
}
}
`,
})
.expect(200)
.expect(res => {
t.is(res.body.data.removeAvatar.success, true);
});
});
async function createToken(app: INestApplication<Express>): Promise<{
id: string;
token: string;
}> {
let token;
let id;
await request(app.getHttpServer())
.post(gql)
.send({
query: `
mutation {
signIn(email: "alex.yang@example.org", password: "123456") {
id
token {
token
}
}
}
`,
})
.expect(200)
.expect(res => {
id = res.body.data.signIn.id;
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;
});
return { token: token!, id: id! };
}