mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 12:06:35 +08:00
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:
@@ -0,0 +1,21 @@
|
||||
import { OAuthProviderName } from '../types';
|
||||
|
||||
export interface OAuthAccount {
|
||||
id: string;
|
||||
email: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export interface Tokens {
|
||||
accessToken: string;
|
||||
scope?: string;
|
||||
refreshToken?: string;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
|
||||
export abstract class OAuthProvider {
|
||||
abstract provider: OAuthProviderName;
|
||||
abstract getAuthUrl(state?: string): string;
|
||||
abstract getToken(code: string): Promise<Tokens>;
|
||||
abstract getUser(token: string): Promise<OAuthAccount>;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Config, URLHelper } from '../../../fundamentals';
|
||||
import { AutoRegisteredOAuthProvider } from '../register';
|
||||
import { OAuthProviderName } from '../types';
|
||||
|
||||
interface AuthTokenResponse {
|
||||
access_token: string;
|
||||
scope: string;
|
||||
token_type: string;
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
login: string;
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GithubOAuthProvider extends AutoRegisteredOAuthProvider {
|
||||
provider = OAuthProviderName.GitHub;
|
||||
|
||||
constructor(
|
||||
protected readonly AFFiNEConfig: Config,
|
||||
private readonly url: URLHelper
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getAuthUrl(state: string) {
|
||||
return `https://github.com/login/oauth/authorize?${this.url.stringify({
|
||||
client_id: this.config.clientId,
|
||||
redirect_uri: this.url.link('/oauth/callback'),
|
||||
scope: 'user',
|
||||
...this.config.args,
|
||||
state,
|
||||
})}`;
|
||||
}
|
||||
|
||||
async getToken(code: string) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://github.com/login/oauth/access_token',
|
||||
{
|
||||
method: 'POST',
|
||||
body: this.url.stringify({
|
||||
code,
|
||||
client_id: this.config.clientId,
|
||||
client_secret: this.config.clientSecret,
|
||||
redirect_uri: this.url.link('/oauth/callback'),
|
||||
}),
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const ghToken = (await response.json()) as AuthTokenResponse;
|
||||
|
||||
return {
|
||||
accessToken: ghToken.access_token,
|
||||
scope: ghToken.scope,
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
`Server responded with non-success code ${
|
||||
response.status
|
||||
}, ${JSON.stringify(await response.json())}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new HttpException(
|
||||
`Failed to get access_token, err: ${(e as Error).message}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getUser(token: string) {
|
||||
try {
|
||||
const response = await fetch('https://api.github.com/user', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const user = (await response.json()) as UserInfo;
|
||||
|
||||
return {
|
||||
id: user.login,
|
||||
avatarUrl: user.avatar_url,
|
||||
email: user.email,
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
`Server responded with non-success code ${
|
||||
response.status
|
||||
} ${await response.text()}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new HttpException(
|
||||
`Failed to get user information, err: ${(e as Error).stack}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Config, URLHelper } from '../../../fundamentals';
|
||||
import { AutoRegisteredOAuthProvider } from '../register';
|
||||
import { OAuthProviderName } from '../types';
|
||||
|
||||
interface GoogleOAuthTokenResponse {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
token_type: string;
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
id: string;
|
||||
email: string;
|
||||
picture: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GoogleOAuthProvider extends AutoRegisteredOAuthProvider {
|
||||
override provider = OAuthProviderName.Google;
|
||||
|
||||
constructor(
|
||||
protected readonly AFFiNEConfig: Config,
|
||||
private readonly url: URLHelper
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getAuthUrl(state: string) {
|
||||
return `https://accounts.google.com/o/oauth2/v2/auth?${this.url.stringify({
|
||||
client_id: this.config.clientId,
|
||||
redirect_uri: this.url.link('/oauth/callback'),
|
||||
response_type: 'code',
|
||||
scope: 'openid email profile',
|
||||
promot: 'select_account',
|
||||
access_type: 'offline',
|
||||
...this.config.args,
|
||||
state,
|
||||
})}`;
|
||||
}
|
||||
|
||||
async getToken(code: string) {
|
||||
try {
|
||||
const response = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
body: this.url.stringify({
|
||||
code,
|
||||
client_id: this.config.clientId,
|
||||
client_secret: this.config.clientSecret,
|
||||
redirect_uri: this.url.link('/oauth/callback'),
|
||||
grant_type: 'authorization_code',
|
||||
}),
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const ghToken = (await response.json()) as GoogleOAuthTokenResponse;
|
||||
|
||||
return {
|
||||
accessToken: ghToken.access_token,
|
||||
refreshToken: ghToken.refresh_token,
|
||||
expiresAt: new Date(Date.now() + ghToken.expires_in * 1000),
|
||||
scope: ghToken.scope,
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
`Server responded with non-success code ${
|
||||
response.status
|
||||
}, ${JSON.stringify(await response.json())}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new HttpException(
|
||||
`Failed to get access_token, err: ${(e as Error).message}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getUser(token: string) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://www.googleapis.com/oauth2/v2/userinfo',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const user = (await response.json()) as UserInfo;
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
avatarUrl: user.picture,
|
||||
email: user.email,
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
`Server responded with non-success code ${
|
||||
response.status
|
||||
} ${await response.text()}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new HttpException(
|
||||
`Failed to get user information, err: ${(e as Error).stack}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { GithubOAuthProvider } from './github';
|
||||
import { GoogleOAuthProvider } from './google';
|
||||
|
||||
export const OAuthProviders = [GoogleOAuthProvider, GithubOAuthProvider];
|
||||
Reference in New Issue
Block a user