feat(core): support apple sign in (#12424)
This commit is contained in:
@@ -891,13 +891,43 @@
|
|||||||
},
|
},
|
||||||
"providers.oidc": {
|
"providers.oidc": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "OIDC OAuth provider config\n@default {\"clientId\":\"\",\"clientSecret\":\"\",\"issuer\":\"\",\"args\":{}}",
|
"description": "OIDC OAuth provider config\n@default {\"clientId\":\"\",\"clientSecret\":\"\",\"issuer\":\"\",\"args\":{}}\n@link https://openid.net/specs/openid-connect-core-1_0.html",
|
||||||
|
"properties": {
|
||||||
|
"clientId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"clientSecret": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"args": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
"default": {
|
"default": {
|
||||||
"clientId": "",
|
"clientId": "",
|
||||||
"clientSecret": "",
|
"clientSecret": "",
|
||||||
"issuer": "",
|
"issuer": "",
|
||||||
"args": {}
|
"args": {}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"providers.apple": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Apple OAuth provider config\n@default {\"clientId\":\"\",\"clientSecret\":\"\"}\n@link https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/implementing_sign_in_with_apple_in_your_app",
|
||||||
|
"properties": {
|
||||||
|
"clientId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"clientSecret": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"args": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"clientId": "",
|
||||||
|
"clientSecret": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -91,6 +91,7 @@
|
|||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.0",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
"is-mobile": "^5.0.0",
|
"is-mobile": "^5.0.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"keyv": "^5.2.2",
|
"keyv": "^5.2.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mixpanel": "^0.18.0",
|
"mixpanel": "^0.18.0",
|
||||||
@@ -129,6 +130,7 @@
|
|||||||
"@types/express-serve-static-core": "^5.0.6",
|
"@types/express-serve-static-core": "^5.0.6",
|
||||||
"@types/graphql-upload": "^17.0.0",
|
"@types/graphql-upload": "^17.0.0",
|
||||||
"@types/http-errors": "^2.0.4",
|
"@types/http-errors": "^2.0.4",
|
||||||
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/mixpanel": "^2.14.9",
|
"@types/mixpanel": "^2.14.9",
|
||||||
"@types/mustache": "^4.2.5",
|
"@types/mustache": "^4.2.5",
|
||||||
|
|||||||
@@ -350,6 +350,11 @@ export const USER_FRIENDLY_ERRORS = {
|
|||||||
message:
|
message:
|
||||||
'The third-party account has already been connected to another user.',
|
'The third-party account has already been connected to another user.',
|
||||||
},
|
},
|
||||||
|
invalid_oauth_response: {
|
||||||
|
type: 'bad_request',
|
||||||
|
args: { reason: 'string' },
|
||||||
|
message: ({ reason }) => `Invalid OAuth response: ${reason}.`,
|
||||||
|
},
|
||||||
invalid_email: {
|
invalid_email: {
|
||||||
type: 'invalid_input',
|
type: 'invalid_input',
|
||||||
args: { email: 'string' },
|
args: { email: 'string' },
|
||||||
|
|||||||
@@ -170,6 +170,16 @@ export class OauthAccountAlreadyConnected extends UserFriendlyError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
|
class InvalidOauthResponseDataType {
|
||||||
|
@Field() reason!: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidOauthResponse extends UserFriendlyError {
|
||||||
|
constructor(args: InvalidOauthResponseDataType, message?: string | ((args: InvalidOauthResponseDataType) => string)) {
|
||||||
|
super('bad_request', 'invalid_oauth_response', message, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ObjectType()
|
||||||
class InvalidEmailDataType {
|
class InvalidEmailDataType {
|
||||||
@Field() email!: string
|
@Field() email!: string
|
||||||
}
|
}
|
||||||
@@ -1058,6 +1068,7 @@ export enum ErrorNames {
|
|||||||
INVALID_AUTH_STATE,
|
INVALID_AUTH_STATE,
|
||||||
MISSING_OAUTH_QUERY_PARAMETER,
|
MISSING_OAUTH_QUERY_PARAMETER,
|
||||||
OAUTH_ACCOUNT_ALREADY_CONNECTED,
|
OAUTH_ACCOUNT_ALREADY_CONNECTED,
|
||||||
|
INVALID_OAUTH_RESPONSE,
|
||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
INVALID_PASSWORD_LENGTH,
|
INVALID_PASSWORD_LENGTH,
|
||||||
PASSWORD_REQUIRED,
|
PASSWORD_REQUIRED,
|
||||||
@@ -1176,5 +1187,5 @@ registerEnumType(ErrorNames, {
|
|||||||
export const ErrorDataUnionType = createUnionType({
|
export const ErrorDataUnionType = createUnionType({
|
||||||
name: 'ErrorDataUnion',
|
name: 'ErrorDataUnion',
|
||||||
types: () =>
|
types: () =>
|
||||||
[GraphqlBadRequestDataType, HttpRequestErrorDataType, QueryTooLongDataType, ValidationErrorDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, DocUpdateBlockedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, NoMoreSeatDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderNotSupportedDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, CopilotFailedToMatchGlobalContextDataType, CopilotFailedToAddWorkspaceFileEmbeddingDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseToActivateDataType, InvalidLicenseUpdateParamsDataType, UnsupportedClientVersionDataType, MentionUserDocAccessDeniedDataType, InvalidSearchProviderRequestDataType, InvalidIndexerInputDataType] as const,
|
[GraphqlBadRequestDataType, HttpRequestErrorDataType, QueryTooLongDataType, ValidationErrorDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidOauthResponseDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, DocUpdateBlockedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, NoMoreSeatDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderNotSupportedDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, CopilotFailedToMatchGlobalContextDataType, CopilotFailedToAddWorkspaceFileEmbeddingDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseToActivateDataType, InvalidLicenseUpdateParamsDataType, UnsupportedClientVersionDataType, MentionUserDocAccessDeniedDataType, InvalidSearchProviderRequestDataType, InvalidIndexerInputDataType] as const,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { defineModuleConfig, JSONSchema } from '../../base';
|
import { defineModuleConfig, JSONSchema } from '../../base';
|
||||||
|
|
||||||
export interface OAuthProviderConfig {
|
export interface OAuthProviderConfig {
|
||||||
@@ -21,6 +23,7 @@ export interface OAuthOIDCProviderConfig extends OAuthProviderConfig {
|
|||||||
export enum OAuthProviderName {
|
export enum OAuthProviderName {
|
||||||
Google = 'google',
|
Google = 'google',
|
||||||
GitHub = 'github',
|
GitHub = 'github',
|
||||||
|
Apple = 'apple',
|
||||||
OIDC = 'oidc',
|
OIDC = 'oidc',
|
||||||
}
|
}
|
||||||
declare global {
|
declare global {
|
||||||
@@ -29,6 +32,7 @@ declare global {
|
|||||||
providers: {
|
providers: {
|
||||||
[OAuthProviderName.Google]: ConfigItem<OAuthProviderConfig>;
|
[OAuthProviderName.Google]: ConfigItem<OAuthProviderConfig>;
|
||||||
[OAuthProviderName.GitHub]: ConfigItem<OAuthProviderConfig>;
|
[OAuthProviderName.GitHub]: ConfigItem<OAuthProviderConfig>;
|
||||||
|
[OAuthProviderName.Apple]: ConfigItem<OAuthProviderConfig>;
|
||||||
[OAuthProviderName.OIDC]: ConfigItem<OAuthOIDCProviderConfig>;
|
[OAuthProviderName.OIDC]: ConfigItem<OAuthOIDCProviderConfig>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -71,5 +75,29 @@ defineModuleConfig('oauth', {
|
|||||||
issuer: '',
|
issuer: '',
|
||||||
args: {},
|
args: {},
|
||||||
},
|
},
|
||||||
|
schema,
|
||||||
|
link: 'https://openid.net/specs/openid-connect-core-1_0.html',
|
||||||
|
shape: z.object({
|
||||||
|
issuer: z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.regex(/^https?:\/\//, 'issuer must be a valid URL')
|
||||||
|
.or(z.string().length(0)),
|
||||||
|
args: z.object({
|
||||||
|
scope: z.string().optional(),
|
||||||
|
claim_id: z.string().optional(),
|
||||||
|
claim_email: z.string().optional(),
|
||||||
|
claim_name: z.string().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'providers.apple': {
|
||||||
|
desc: 'Apple OAuth provider config',
|
||||||
|
default: {
|
||||||
|
clientId: '',
|
||||||
|
clientSecret: '',
|
||||||
|
},
|
||||||
|
schema,
|
||||||
|
link: 'https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/implementing_sign_in_with_apple_in_your_app',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
OauthAccountAlreadyConnected,
|
OauthAccountAlreadyConnected,
|
||||||
OauthStateExpired,
|
OauthStateExpired,
|
||||||
UnknownOauthProvider,
|
UnknownOauthProvider,
|
||||||
|
URLHelper,
|
||||||
UseNamedGuard,
|
UseNamedGuard,
|
||||||
} from '../../base';
|
} from '../../base';
|
||||||
import { AuthService, Public } from '../../core/auth';
|
import { AuthService, Public } from '../../core/auth';
|
||||||
@@ -36,7 +37,8 @@ export class OAuthController {
|
|||||||
private readonly auth: AuthService,
|
private readonly auth: AuthService,
|
||||||
private readonly oauth: OAuthService,
|
private readonly oauth: OAuthService,
|
||||||
private readonly models: Models,
|
private readonly models: Models,
|
||||||
private readonly providerFactory: OAuthProviderFactory
|
private readonly providerFactory: OAuthProviderFactory,
|
||||||
|
private readonly url: URLHelper
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@@ -67,10 +69,14 @@ export class OAuthController {
|
|||||||
clientNonce,
|
clientNonce,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const stateStr = JSON.stringify({
|
||||||
|
state,
|
||||||
|
client,
|
||||||
|
provider: unknownProviderName,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: provider.getAuthUrl(
|
url: provider.getAuthUrl(stateStr, clientNonce),
|
||||||
JSON.stringify({ state, client, provider: unknownProviderName })
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +91,7 @@ export class OAuthController {
|
|||||||
@Body('state') stateStr?: string,
|
@Body('state') stateStr?: string,
|
||||||
@Body('client_nonce') clientNonce?: string
|
@Body('client_nonce') clientNonce?: string
|
||||||
) {
|
) {
|
||||||
|
// TODO(@forehalo): refactor and remove deprecated code in 0.23
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new MissingOauthQueryParameter({ name: 'code' });
|
throw new MissingOauthQueryParameter({ name: 'code' });
|
||||||
}
|
}
|
||||||
@@ -93,6 +100,17 @@ export class OAuthController {
|
|||||||
throw new MissingOauthQueryParameter({ name: 'state' });
|
throw new MissingOauthQueryParameter({ name: 'state' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE(@forehalo): Apple sign in will directly post /callback, with `state` set at #L73
|
||||||
|
let rawState = null;
|
||||||
|
if (typeof stateStr === 'string' && stateStr.length > 36) {
|
||||||
|
try {
|
||||||
|
rawState = JSON.parse(stateStr);
|
||||||
|
stateStr = rawState.state;
|
||||||
|
} catch {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof stateStr !== 'string' || !this.oauth.isValidState(stateStr)) {
|
if (typeof stateStr !== 'string' || !this.oauth.isValidState(stateStr)) {
|
||||||
throw new InvalidOauthCallbackState();
|
throw new InvalidOauthCallbackState();
|
||||||
}
|
}
|
||||||
@@ -103,8 +121,38 @@ export class OAuthController {
|
|||||||
throw new OauthStateExpired();
|
throw new OauthStateExpired();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
state.provider === OAuthProviderName.Apple &&
|
||||||
|
rawState &&
|
||||||
|
state.client &&
|
||||||
|
state.client !== 'web'
|
||||||
|
) {
|
||||||
|
const clientUrl = new URL(`${state.client}://authentication`);
|
||||||
|
clientUrl.searchParams.set('method', 'oauth');
|
||||||
|
clientUrl.searchParams.set(
|
||||||
|
'payload',
|
||||||
|
JSON.stringify({
|
||||||
|
state: stateStr,
|
||||||
|
code,
|
||||||
|
provider: rawState.provider,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
clientUrl.searchParams.set('server', this.url.origin);
|
||||||
|
|
||||||
|
return res.redirect(
|
||||||
|
this.url.link('/open-app/url?', {
|
||||||
|
url: clientUrl.toString(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(@fengmk2): clientNonce should be required after the client version >= 0.21.0
|
// TODO(@fengmk2): clientNonce should be required after the client version >= 0.21.0
|
||||||
if (state.clientNonce && state.clientNonce !== clientNonce) {
|
if (
|
||||||
|
state.clientNonce &&
|
||||||
|
state.clientNonce !== clientNonce &&
|
||||||
|
// apple sign in with nonce stored in id token
|
||||||
|
state.provider !== OAuthProviderName.Apple
|
||||||
|
) {
|
||||||
throw new InvalidAuthState();
|
throw new InvalidAuthState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +180,8 @@ export class OAuthController {
|
|||||||
);
|
);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
const externAccount = await provider.getUser(tokens.accessToken);
|
|
||||||
|
const externAccount = await provider.getUser(tokens, state);
|
||||||
const user = await this.loginFromOauth(
|
const user = await this.loginFromOauth(
|
||||||
state.provider,
|
state.provider,
|
||||||
externAccount,
|
externAccount,
|
||||||
@@ -140,6 +189,14 @@ export class OAuthController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await this.auth.setCookies(req, res, user.id);
|
await this.auth.setCookies(req, res, user.id);
|
||||||
|
|
||||||
|
if (
|
||||||
|
state.provider === OAuthProviderName.Apple &&
|
||||||
|
(!state.client || state.client === 'web')
|
||||||
|
) {
|
||||||
|
return res.redirect(this.url.link(state.redirectUri ?? '/'));
|
||||||
|
}
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
redirectUri: state.redirectUri,
|
redirectUri: state.redirectUri,
|
||||||
@@ -170,7 +227,9 @@ export class OAuthController {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
provider,
|
provider,
|
||||||
providerAccountId: externalAccount.id,
|
providerAccountId: externalAccount.id,
|
||||||
...tokens,
|
accessToken: tokens.accessToken,
|
||||||
|
refreshToken: tokens.refreshToken,
|
||||||
|
expiresAt: tokens.expiresAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
@@ -180,10 +239,11 @@ export class OAuthController {
|
|||||||
connectedAccount: ConnectedAccount,
|
connectedAccount: ConnectedAccount,
|
||||||
tokens: Tokens
|
tokens: Tokens
|
||||||
) {
|
) {
|
||||||
return await this.models.user.updateConnectedAccount(
|
return await this.models.user.updateConnectedAccount(connectedAccount.id, {
|
||||||
connectedAccount.id,
|
accessToken: tokens.accessToken,
|
||||||
tokens
|
refreshToken: tokens.refreshToken,
|
||||||
);
|
expiresAt: tokens.expiresAt,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -210,7 +270,9 @@ export class OAuthController {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
provider,
|
provider,
|
||||||
providerAccountId: externalAccount.id,
|
providerAccountId: externalAccount.id,
|
||||||
...tokens,
|
accessToken: tokens.accessToken,
|
||||||
|
refreshToken: tokens.refreshToken,
|
||||||
|
expiresAt: tokens.expiresAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
131
packages/backend/server/src/plugins/oauth/providers/apple.ts
Normal file
131
packages/backend/server/src/plugins/oauth/providers/apple.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { JsonWebKey } from 'node:crypto';
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import jwt, { type JwtPayload } from 'jsonwebtoken';
|
||||||
|
|
||||||
|
import {
|
||||||
|
InternalServerError,
|
||||||
|
InvalidOauthCallbackCode,
|
||||||
|
URLHelper,
|
||||||
|
} from '../../../base';
|
||||||
|
import { OAuthProviderName } from '../config';
|
||||||
|
import { OAuthProvider, Tokens } from './def';
|
||||||
|
|
||||||
|
interface AuthTokenResponse {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
id_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppleOAuthProvider extends OAuthProvider {
|
||||||
|
provider = OAuthProviderName.Apple;
|
||||||
|
|
||||||
|
constructor(private readonly url: URLHelper) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthUrl(state: string, clientNonce?: string): string {
|
||||||
|
return `https://appleid.apple.com/auth/authorize?${this.url.stringify({
|
||||||
|
client_id: this.config.clientId,
|
||||||
|
redirect_uri: this.url.link('/api/oauth/callback'),
|
||||||
|
scope: 'name email',
|
||||||
|
response_type: 'code',
|
||||||
|
response_mode: 'form_post',
|
||||||
|
...this.config.args,
|
||||||
|
state,
|
||||||
|
nonce: clientNonce,
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getToken(code: string) {
|
||||||
|
const response = await fetch('https://appleid.apple.com/auth/token', {
|
||||||
|
method: 'POST',
|
||||||
|
body: this.url.stringify({
|
||||||
|
code,
|
||||||
|
client_id: this.config.clientId,
|
||||||
|
client_secret: this.config.clientSecret,
|
||||||
|
redirect_uri: this.url.link('/api/oauth/callback'),
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const appleToken = (await response.json()) as AuthTokenResponse;
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: appleToken.access_token,
|
||||||
|
refreshToken: appleToken.refresh_token,
|
||||||
|
expiresAt: new Date(Date.now() + appleToken.expires_in * 1000),
|
||||||
|
idToken: appleToken.id_token,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const body = await response.text();
|
||||||
|
if (response.status < 500) {
|
||||||
|
throw new InvalidOauthCallbackCode({ status: response.status, body });
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Server responded with non-success status ${response.status}, body: ${body}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(
|
||||||
|
tokens: Tokens & { idToken: string },
|
||||||
|
state: { clientNonce: string }
|
||||||
|
) {
|
||||||
|
const keysReq = await fetch('https://appleid.apple.com/auth/keys', {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
const { keys } = (await keysReq.json()) as { keys: JsonWebKey[] };
|
||||||
|
|
||||||
|
const payload = await new Promise<JwtPayload>((resolve, reject) => {
|
||||||
|
jwt.verify(
|
||||||
|
tokens.idToken,
|
||||||
|
(header, callback) => {
|
||||||
|
const key = keys.find(key => key.kid === header.kid);
|
||||||
|
if (!key) {
|
||||||
|
callback(
|
||||||
|
new InternalServerError(
|
||||||
|
'Cannot find match apple public sign key.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
callback(null, {
|
||||||
|
format: 'jwk',
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
issuer: 'https://appleid.apple.com',
|
||||||
|
audience: this.config.clientId,
|
||||||
|
nonce: state.clientNonce,
|
||||||
|
},
|
||||||
|
(err, payload) => {
|
||||||
|
if (err || !payload || typeof payload === 'string') {
|
||||||
|
reject(err || new InternalServerError('Invalid jwt payload'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(payload);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// see https://developer.apple.com/documentation/signinwithapple/authenticating-users-with-sign-in-with-apple
|
||||||
|
if (!payload.sub || !payload.email) {
|
||||||
|
throw new Error('Invalid jwt payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: payload.sub,
|
||||||
|
email: payload.email,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,12 +17,19 @@ export interface Tokens {
|
|||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuthOptions {
|
||||||
|
client_id: string;
|
||||||
|
redirect_uri: string;
|
||||||
|
scope: string;
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export abstract class OAuthProvider {
|
export abstract class OAuthProvider {
|
||||||
abstract provider: OAuthProviderName;
|
abstract provider: OAuthProviderName;
|
||||||
abstract getAuthUrl(state: string): string;
|
abstract getAuthUrl(state: string, clientNonce?: string): string;
|
||||||
abstract getToken(code: string): Promise<Tokens>;
|
abstract getToken(code: string): Promise<Tokens>;
|
||||||
abstract getUser(token: string): Promise<OAuthAccount>;
|
abstract getUser(tokens: Tokens, state: any): Promise<OAuthAccount>;
|
||||||
|
|
||||||
protected readonly logger = new Logger(this.constructor.name);
|
protected readonly logger = new Logger(this.constructor.name);
|
||||||
@Inject() private readonly factory!: OAuthProviderFactory;
|
@Inject() private readonly factory!: OAuthProviderFactory;
|
||||||
@@ -33,7 +40,9 @@ export abstract class OAuthProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get configured() {
|
get configured() {
|
||||||
return this.config && this.config.clientId && this.config.clientSecret;
|
return (
|
||||||
|
!!this.config && !!this.config.clientId && !!this.config.clientSecret
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('config.init')
|
@OnEvent('config.init')
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { InvalidOauthCallbackCode, URLHelper } from '../../../base';
|
import { InvalidOauthCallbackCode, URLHelper } from '../../../base';
|
||||||
import { OAuthProviderName } from '../config';
|
import { OAuthProviderName } from '../config';
|
||||||
import { OAuthProvider } from './def';
|
import { OAuthProvider, Tokens } from './def';
|
||||||
|
|
||||||
interface AuthTokenResponse {
|
interface AuthTokenResponse {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
@@ -71,11 +71,11 @@ export class GithubOAuthProvider extends OAuthProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser(token: string) {
|
async getUser(tokens: Tokens) {
|
||||||
const response = await fetch('https://api.github.com/user', {
|
const response = await fetch('https://api.github.com/user', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${tokens.accessToken}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { InvalidOauthCallbackCode, URLHelper } from '../../../base';
|
import { InvalidOauthCallbackCode, URLHelper } from '../../../base';
|
||||||
import { OAuthProviderName } from '../config';
|
import { OAuthProviderName } from '../config';
|
||||||
import { OAuthProvider } from './def';
|
import { OAuthProvider, Tokens } from './def';
|
||||||
|
|
||||||
interface GoogleOAuthTokenResponse {
|
interface GoogleOAuthTokenResponse {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
@@ -76,13 +76,13 @@ export class GoogleOAuthProvider extends OAuthProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser(token: string) {
|
async getUser(tokens: Tokens) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
'https://www.googleapis.com/oauth2/v2/userinfo',
|
'https://www.googleapis.com/oauth2/v2/userinfo',
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${tokens.accessToken}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AppleOAuthProvider } from './apple';
|
||||||
import { GithubOAuthProvider } from './github';
|
import { GithubOAuthProvider } from './github';
|
||||||
import { GoogleOAuthProvider } from './google';
|
import { GoogleOAuthProvider } from './google';
|
||||||
import { OIDCProvider } from './oidc';
|
import { OIDCProvider } from './oidc';
|
||||||
@@ -6,4 +7,5 @@ export const OAuthProviders = [
|
|||||||
GoogleOAuthProvider,
|
GoogleOAuthProvider,
|
||||||
GithubOAuthProvider,
|
GithubOAuthProvider,
|
||||||
OIDCProvider,
|
OIDCProvider,
|
||||||
|
AppleOAuthProvider,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { omit } from 'lodash-es';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { URLHelper } from '../../../base';
|
|
||||||
import {
|
import {
|
||||||
OAuthOIDCProviderConfig,
|
InvalidOauthCallbackCode,
|
||||||
OAuthProviderName,
|
InvalidOauthResponse,
|
||||||
OIDCArgs,
|
URLHelper,
|
||||||
} from '../config';
|
} from '../../../base';
|
||||||
|
import { OAuthOIDCProviderConfig, OAuthProviderName } from '../config';
|
||||||
import { OAuthAccount, OAuthProvider, Tokens } from './def';
|
import { OAuthAccount, OAuthProvider, Tokens } from './def';
|
||||||
|
|
||||||
const OIDCTokenSchema = z.object({
|
const OIDCTokenSchema = z.object({
|
||||||
@@ -27,8 +28,6 @@ const OIDCUserInfoSchema = z
|
|||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|
||||||
type OIDCUserInfo = z.infer<typeof OIDCUserInfoSchema>;
|
|
||||||
|
|
||||||
const OIDCConfigurationSchema = z.object({
|
const OIDCConfigurationSchema = z.object({
|
||||||
authorization_endpoint: z.string().url(),
|
authorization_endpoint: z.string().url(),
|
||||||
token_endpoint: z.string().url(),
|
token_endpoint: z.string().url(),
|
||||||
@@ -37,173 +36,142 @@ const OIDCConfigurationSchema = z.object({
|
|||||||
|
|
||||||
type OIDCConfiguration = z.infer<typeof OIDCConfigurationSchema>;
|
type OIDCConfiguration = z.infer<typeof OIDCConfigurationSchema>;
|
||||||
|
|
||||||
const logger = new Logger('OIDCClient');
|
|
||||||
|
|
||||||
class OIDCClient {
|
|
||||||
private static async fetch<T = any>(
|
|
||||||
url: string,
|
|
||||||
options: RequestInit,
|
|
||||||
verifier: z.Schema<T>
|
|
||||||
): Promise<T> {
|
|
||||||
const response = await fetch(url, options);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
logger.error('Failed to fetch OIDC configuration', await response.json());
|
|
||||||
throw new Error(`Failed to configure client`);
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
return verifier.parse(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async create(config: OAuthOIDCProviderConfig, url: URLHelper) {
|
|
||||||
const { args, clientId, clientSecret, issuer } = config;
|
|
||||||
if (!url.verify(issuer)) {
|
|
||||||
throw new Error('OIDC Issuer is invalid.');
|
|
||||||
}
|
|
||||||
const oidcConfig = await OIDCClient.fetch(
|
|
||||||
`${issuer}/.well-known/openid-configuration`,
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
headers: { Accept: 'application/json' },
|
|
||||||
},
|
|
||||||
OIDCConfigurationSchema
|
|
||||||
);
|
|
||||||
|
|
||||||
return new OIDCClient(clientId, clientSecret, args, oidcConfig, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(
|
|
||||||
private readonly clientId: string,
|
|
||||||
private readonly clientSecret: string,
|
|
||||||
private readonly args: OIDCArgs | undefined,
|
|
||||||
private readonly config: OIDCConfiguration,
|
|
||||||
private readonly url: URLHelper
|
|
||||||
) {}
|
|
||||||
|
|
||||||
authorize(state: string): string {
|
|
||||||
const args = Object.assign({}, this.args);
|
|
||||||
if ('claim_id' in args) delete args.claim_id;
|
|
||||||
if ('claim_email' in args) delete args.claim_email;
|
|
||||||
if ('claim_name' in args) delete args.claim_name;
|
|
||||||
|
|
||||||
return `${this.config.authorization_endpoint}?${this.url.stringify({
|
|
||||||
client_id: this.clientId,
|
|
||||||
redirect_uri: this.url.link('/oauth/callback'),
|
|
||||||
response_type: 'code',
|
|
||||||
...args,
|
|
||||||
scope: this.args?.scope || 'openid profile email',
|
|
||||||
state,
|
|
||||||
})}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async token(code: string): Promise<Tokens> {
|
|
||||||
const token = await OIDCClient.fetch(
|
|
||||||
this.config.token_endpoint,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: this.url.stringify({
|
|
||||||
code,
|
|
||||||
client_id: this.clientId,
|
|
||||||
client_secret: this.clientSecret,
|
|
||||||
redirect_uri: this.url.link('/oauth/callback'),
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
OIDCTokenSchema
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
accessToken: token.access_token,
|
|
||||||
refreshToken: token.refresh_token,
|
|
||||||
expiresAt: new Date(Date.now() + token.expires_in * 1000),
|
|
||||||
scope: token.scope,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private mapUserInfo(
|
|
||||||
user: OIDCUserInfo,
|
|
||||||
claimsMap: Record<string, string>
|
|
||||||
): OAuthAccount {
|
|
||||||
const mappedUser: Partial<OAuthAccount> = {};
|
|
||||||
for (const [key, value] of Object.entries(claimsMap)) {
|
|
||||||
const claimValue = user[value];
|
|
||||||
if (claimValue !== undefined) {
|
|
||||||
mappedUser[key as keyof OAuthAccount] = claimValue as string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mappedUser as OAuthAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
async userinfo(token: string) {
|
|
||||||
const user = await OIDCClient.fetch(
|
|
||||||
this.config.userinfo_endpoint,
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
OIDCUserInfoSchema
|
|
||||||
);
|
|
||||||
|
|
||||||
const claimsMap = {
|
|
||||||
id: this.args?.claim_id || 'preferred_username',
|
|
||||||
email: this.args?.claim_email || 'email',
|
|
||||||
name: this.args?.claim_name || 'name',
|
|
||||||
};
|
|
||||||
const userinfo = this.mapUserInfo(user, claimsMap);
|
|
||||||
return { id: userinfo.id, email: userinfo.email };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OIDCProvider extends OAuthProvider {
|
export class OIDCProvider extends OAuthProvider {
|
||||||
override provider = OAuthProviderName.OIDC;
|
override provider = OAuthProviderName.OIDC;
|
||||||
private client: OIDCClient | null = null;
|
#endpoints: OIDCConfiguration | null = null;
|
||||||
|
|
||||||
constructor(private readonly url: URLHelper) {
|
constructor(private readonly url: URLHelper) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override setup() {
|
private get endpoints() {
|
||||||
super.setup();
|
if (!this.#endpoints) {
|
||||||
if (this.configured) {
|
throw new Error('OIDC provider is not configured');
|
||||||
OIDCClient.create(this.config as OAuthOIDCProviderConfig, this.url)
|
|
||||||
.then(client => {
|
|
||||||
this.client = client;
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
this.logger.error('Failed to create OIDC client', e);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.client = null;
|
|
||||||
}
|
}
|
||||||
|
return this.#endpoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkOIDCClient(
|
override get configured() {
|
||||||
client: OIDCClient | null
|
return this.#endpoints !== null;
|
||||||
): asserts client is OIDCClient {
|
}
|
||||||
if (!client) {
|
|
||||||
throw new Error('OIDC client has not been loaded yet.');
|
protected override setup() {
|
||||||
}
|
const validate = async () => {
|
||||||
|
this.#endpoints = null;
|
||||||
|
|
||||||
|
if (this.configured) {
|
||||||
|
const config = this.config as OAuthOIDCProviderConfig;
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${config.issuer}/.well-known/openid-configuration`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.#endpoints = OIDCConfigurationSchema.parse(await res.json());
|
||||||
|
super.setup();
|
||||||
|
} else {
|
||||||
|
this.logger.error(`Invalid OIDC issuer ${config.issuer}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error('Failed to validate OIDC configuration', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
validate().catch(() => {
|
||||||
|
/* noop */
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthUrl(state: string): string {
|
getAuthUrl(state: string): string {
|
||||||
this.checkOIDCClient(this.client);
|
return `${this.endpoints.authorization_endpoint}?${this.url.stringify({
|
||||||
return this.client.authorize(state);
|
client_id: this.config.clientId,
|
||||||
|
redirect_uri: this.url.link('/oauth/callback'),
|
||||||
|
scope: this.config.args?.scope || 'openid profile email',
|
||||||
|
response_type: 'code',
|
||||||
|
...omit(this.config.args, 'claim_id', 'claim_email', 'claim_name'),
|
||||||
|
state,
|
||||||
|
})}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getToken(code: string): Promise<Tokens> {
|
async getToken(code: string): Promise<Tokens> {
|
||||||
this.checkOIDCClient(this.client);
|
const res = await fetch(this.endpoints.token_endpoint, {
|
||||||
return await this.client.token(code);
|
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 (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
const tokens = OIDCTokenSchema.parse(data);
|
||||||
|
return {
|
||||||
|
accessToken: tokens.access_token,
|
||||||
|
refreshToken: tokens.refresh_token,
|
||||||
|
expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
|
||||||
|
scope: tokens.scope,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOauthCallbackCode({
|
||||||
|
status: res.status,
|
||||||
|
body: await res.text(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
async getUser(token: string): Promise<OAuthAccount> {
|
|
||||||
this.checkOIDCClient(this.client);
|
async getUser(tokens: Tokens): Promise<OAuthAccount> {
|
||||||
return await this.client.userinfo(token);
|
const res = await fetch(this.endpoints.userinfo_endpoint, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${tokens.accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const body = await res.json();
|
||||||
|
const user = OIDCUserInfoSchema.parse(body);
|
||||||
|
|
||||||
|
const args = this.config.args ?? {};
|
||||||
|
|
||||||
|
const claimsMap = {
|
||||||
|
id: args.claim_id || 'preferred_username',
|
||||||
|
email: args.claim_email || 'email',
|
||||||
|
name: args.claim_name || 'name',
|
||||||
|
};
|
||||||
|
|
||||||
|
const identities = {
|
||||||
|
id: user[claimsMap.id] as string,
|
||||||
|
email: user[claimsMap.email] as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!identities.id || !identities.email) {
|
||||||
|
throw new InvalidOauthResponse({
|
||||||
|
reason: `Missing required claims: ${Object.keys(identities)
|
||||||
|
.filter(key => !identities[key as keyof typeof identities])
|
||||||
|
.join(', ')}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return identities;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOauthCallbackCode({
|
||||||
|
status: res.status,
|
||||||
|
body: await res.text(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ type EditorType {
|
|||||||
name: String!
|
name: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToAddWorkspaceFileEmbeddingDataType | CopilotFailedToMatchContextDataType | CopilotFailedToMatchGlobalContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderNotSupportedDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | DocUpdateBlockedDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | HttpRequestErrorDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidIndexerInputDataType | InvalidLicenseToActivateDataType | InvalidLicenseUpdateParamsDataType | InvalidOauthCallbackCodeDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | InvalidSearchProviderRequestDataType | MemberNotFoundInSpaceDataType | MentionUserDocAccessDeniedDataType | MissingOauthQueryParameterDataType | NoMoreSeatDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | ValidationErrorDataType | VersionRejectedDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType
|
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToAddWorkspaceFileEmbeddingDataType | CopilotFailedToMatchContextDataType | CopilotFailedToMatchGlobalContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderNotSupportedDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | DocUpdateBlockedDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | HttpRequestErrorDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidIndexerInputDataType | InvalidLicenseToActivateDataType | InvalidLicenseUpdateParamsDataType | InvalidOauthCallbackCodeDataType | InvalidOauthResponseDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | InvalidSearchProviderRequestDataType | MemberNotFoundInSpaceDataType | MentionUserDocAccessDeniedDataType | MissingOauthQueryParameterDataType | NoMoreSeatDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | ValidationErrorDataType | VersionRejectedDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType
|
||||||
|
|
||||||
enum ErrorNames {
|
enum ErrorNames {
|
||||||
ACCESS_DENIED
|
ACCESS_DENIED
|
||||||
@@ -601,6 +601,7 @@ enum ErrorNames {
|
|||||||
INVALID_LICENSE_UPDATE_PARAMS
|
INVALID_LICENSE_UPDATE_PARAMS
|
||||||
INVALID_OAUTH_CALLBACK_CODE
|
INVALID_OAUTH_CALLBACK_CODE
|
||||||
INVALID_OAUTH_CALLBACK_STATE
|
INVALID_OAUTH_CALLBACK_STATE
|
||||||
|
INVALID_OAUTH_RESPONSE
|
||||||
INVALID_PASSWORD_LENGTH
|
INVALID_PASSWORD_LENGTH
|
||||||
INVALID_RUNTIME_CONFIG_TYPE
|
INVALID_RUNTIME_CONFIG_TYPE
|
||||||
INVALID_SEARCH_PROVIDER_REQUEST
|
INVALID_SEARCH_PROVIDER_REQUEST
|
||||||
@@ -753,6 +754,10 @@ type InvalidOauthCallbackCodeDataType {
|
|||||||
status: Int!
|
status: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InvalidOauthResponseDataType {
|
||||||
|
reason: String!
|
||||||
|
}
|
||||||
|
|
||||||
type InvalidPasswordLengthDataType {
|
type InvalidPasswordLengthDataType {
|
||||||
max: Int!
|
max: Int!
|
||||||
min: Int!
|
min: Int!
|
||||||
@@ -1266,6 +1271,7 @@ type NotificationWorkspaceType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum OAuthProviderType {
|
enum OAuthProviderType {
|
||||||
|
Apple
|
||||||
GitHub
|
GitHub
|
||||||
Google
|
Google
|
||||||
OIDC
|
OIDC
|
||||||
|
|||||||
2
packages/common/env/src/ua-helper.ts
vendored
2
packages/common/env/src/ua-helper.ts
vendored
@@ -70,7 +70,7 @@ const getUa = (navigator: Navigator) => {
|
|||||||
const tiktok = mobile && /aweme/i.test(ua);
|
const tiktok = mobile && /aweme/i.test(ua);
|
||||||
const weibo = mobile && /Weibo/i.test(ua);
|
const weibo = mobile && /Weibo/i.test(ua);
|
||||||
const safari =
|
const safari =
|
||||||
ios && !chrome && !wx && !weibo && !tiktok && /Safari|Macintosh/i.test(ua);
|
!chrome && !wx && !weibo && !tiktok && /Safari|Macintosh/i.test(ua);
|
||||||
const firefox = /Firefox/.test(ua);
|
const firefox = /Firefox/.test(ua);
|
||||||
const win = /windows|win32|win64|wow32|wow64/.test(uas);
|
const win = /windows|win32|win64|wow32|wow64/.test(uas);
|
||||||
const linux = /linux/.test(uas);
|
const linux = /linux/.test(uas);
|
||||||
|
|||||||
@@ -671,6 +671,7 @@ export type ErrorDataUnion =
|
|||||||
| InvalidLicenseToActivateDataType
|
| InvalidLicenseToActivateDataType
|
||||||
| InvalidLicenseUpdateParamsDataType
|
| InvalidLicenseUpdateParamsDataType
|
||||||
| InvalidOauthCallbackCodeDataType
|
| InvalidOauthCallbackCodeDataType
|
||||||
|
| InvalidOauthResponseDataType
|
||||||
| InvalidPasswordLengthDataType
|
| InvalidPasswordLengthDataType
|
||||||
| InvalidRuntimeConfigTypeDataType
|
| InvalidRuntimeConfigTypeDataType
|
||||||
| InvalidSearchProviderRequestDataType
|
| InvalidSearchProviderRequestDataType
|
||||||
@@ -773,6 +774,7 @@ export enum ErrorNames {
|
|||||||
INVALID_LICENSE_UPDATE_PARAMS = 'INVALID_LICENSE_UPDATE_PARAMS',
|
INVALID_LICENSE_UPDATE_PARAMS = 'INVALID_LICENSE_UPDATE_PARAMS',
|
||||||
INVALID_OAUTH_CALLBACK_CODE = 'INVALID_OAUTH_CALLBACK_CODE',
|
INVALID_OAUTH_CALLBACK_CODE = 'INVALID_OAUTH_CALLBACK_CODE',
|
||||||
INVALID_OAUTH_CALLBACK_STATE = 'INVALID_OAUTH_CALLBACK_STATE',
|
INVALID_OAUTH_CALLBACK_STATE = 'INVALID_OAUTH_CALLBACK_STATE',
|
||||||
|
INVALID_OAUTH_RESPONSE = 'INVALID_OAUTH_RESPONSE',
|
||||||
INVALID_PASSWORD_LENGTH = 'INVALID_PASSWORD_LENGTH',
|
INVALID_PASSWORD_LENGTH = 'INVALID_PASSWORD_LENGTH',
|
||||||
INVALID_RUNTIME_CONFIG_TYPE = 'INVALID_RUNTIME_CONFIG_TYPE',
|
INVALID_RUNTIME_CONFIG_TYPE = 'INVALID_RUNTIME_CONFIG_TYPE',
|
||||||
INVALID_SEARCH_PROVIDER_REQUEST = 'INVALID_SEARCH_PROVIDER_REQUEST',
|
INVALID_SEARCH_PROVIDER_REQUEST = 'INVALID_SEARCH_PROVIDER_REQUEST',
|
||||||
@@ -935,6 +937,11 @@ export interface InvalidOauthCallbackCodeDataType {
|
|||||||
status: Scalars['Int']['output'];
|
status: Scalars['Int']['output'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InvalidOauthResponseDataType {
|
||||||
|
__typename?: 'InvalidOauthResponseDataType';
|
||||||
|
reason: Scalars['String']['output'];
|
||||||
|
}
|
||||||
|
|
||||||
export interface InvalidPasswordLengthDataType {
|
export interface InvalidPasswordLengthDataType {
|
||||||
__typename?: 'InvalidPasswordLengthDataType';
|
__typename?: 'InvalidPasswordLengthDataType';
|
||||||
max: Scalars['Int']['output'];
|
max: Scalars['Int']['output'];
|
||||||
@@ -1775,6 +1782,7 @@ export interface NotificationWorkspaceType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum OAuthProviderType {
|
export enum OAuthProviderType {
|
||||||
|
Apple = 'Apple',
|
||||||
GitHub = 'GitHub',
|
GitHub = 'GitHub',
|
||||||
Google = 'Google',
|
Google = 'Google',
|
||||||
OIDC = 'OIDC',
|
OIDC = 'OIDC',
|
||||||
|
|||||||
@@ -315,7 +315,13 @@
|
|||||||
},
|
},
|
||||||
"providers.oidc": {
|
"providers.oidc": {
|
||||||
"type": "Object",
|
"type": "Object",
|
||||||
"desc": "OIDC OAuth provider config"
|
"desc": "OIDC OAuth provider config",
|
||||||
|
"link": "https://openid.net/specs/openid-connect-core-1_0.html"
|
||||||
|
},
|
||||||
|
"providers.apple": {
|
||||||
|
"type": "Object",
|
||||||
|
"desc": "Apple OAuth provider config",
|
||||||
|
"link": "https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/implementing_sign_in_with_apple_in_your_app"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"payment": {
|
"payment": {
|
||||||
|
|||||||
@@ -3,13 +3,17 @@ import { notify } from '@affine/component/ui/notification';
|
|||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import { AuthService, ServerService } from '@affine/core/modules/cloud';
|
import { AuthService, ServerService } from '@affine/core/modules/cloud';
|
||||||
import { UrlService } from '@affine/core/modules/url';
|
import { UrlService } from '@affine/core/modules/url';
|
||||||
import { type UserFriendlyError } from '@affine/error';
|
import { UserFriendlyError } from '@affine/error';
|
||||||
import { OAuthProviderType } from '@affine/graphql';
|
import { OAuthProviderType } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import track from '@affine/track';
|
import track from '@affine/track';
|
||||||
import { GithubIcon, GoogleIcon, LockIcon } from '@blocksuite/icons/rc';
|
import {
|
||||||
|
AppleIcon,
|
||||||
|
GithubIcon,
|
||||||
|
GoogleIcon,
|
||||||
|
LockIcon,
|
||||||
|
} from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { type ReactElement, type SVGAttributes } from 'react';
|
import { type ReactElement, type SVGAttributes, useCallback } from 'react';
|
||||||
|
|
||||||
const OAuthProviderMap: Record<
|
const OAuthProviderMap: Record<
|
||||||
OAuthProviderType,
|
OAuthProviderType,
|
||||||
@@ -28,6 +32,10 @@ const OAuthProviderMap: Record<
|
|||||||
[OAuthProviderType.OIDC]: {
|
[OAuthProviderType.OIDC]: {
|
||||||
icon: <LockIcon />,
|
icon: <LockIcon />,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[OAuthProviderType.Apple]: {
|
||||||
|
icon: <AppleIcon />,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function OAuth({ redirectUrl }: { redirectUrl?: string }) {
|
export function OAuth({ redirectUrl }: { redirectUrl?: string }) {
|
||||||
@@ -37,85 +45,80 @@ export function OAuth({ redirectUrl }: { redirectUrl?: string }) {
|
|||||||
const oauthProviders = useLiveData(
|
const oauthProviders = useLiveData(
|
||||||
serverService.server.config$.map(r => r?.oauthProviders)
|
serverService.server.config$.map(r => r?.oauthProviders)
|
||||||
);
|
);
|
||||||
const scheme = urlService.getClientScheme();
|
const auth = useService(AuthService);
|
||||||
|
|
||||||
|
const onContinue = useAsyncCallback(
|
||||||
|
async (provider: OAuthProviderType) => {
|
||||||
|
track.$.$.auth.signIn({ method: 'oauth', provider });
|
||||||
|
|
||||||
|
const open: () => Promise<void> | void = BUILD_CONFIG.isNative
|
||||||
|
? async () => {
|
||||||
|
try {
|
||||||
|
const scheme = urlService.getClientScheme();
|
||||||
|
const options = await auth.oauthPreflight(
|
||||||
|
provider,
|
||||||
|
scheme ?? 'web'
|
||||||
|
);
|
||||||
|
urlService.openPopupWindow(options.url);
|
||||||
|
} catch (e) {
|
||||||
|
notify.error(UserFriendlyError.fromAny(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
params.set('provider', provider);
|
||||||
|
|
||||||
|
if (redirectUrl) {
|
||||||
|
params.set('redirect_uri', redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oauthUrl =
|
||||||
|
serverService.server.baseUrl +
|
||||||
|
`/oauth/login?${params.toString()}`;
|
||||||
|
|
||||||
|
urlService.openPopupWindow(oauthUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ret = open();
|
||||||
|
|
||||||
|
if (ret instanceof Promise) {
|
||||||
|
await ret;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[urlService, redirectUrl, serverService, auth]
|
||||||
|
);
|
||||||
|
|
||||||
if (!oauth) {
|
if (!oauth) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return oauthProviders?.map(provider => (
|
return oauthProviders?.map(provider => {
|
||||||
<OAuthProvider
|
return (
|
||||||
key={provider}
|
<OAuthProvider
|
||||||
provider={provider}
|
key={provider}
|
||||||
redirectUrl={redirectUrl}
|
provider={provider}
|
||||||
scheme={scheme}
|
onContinue={onContinue}
|
||||||
popupWindow={url => {
|
/>
|
||||||
urlService.openPopupWindow(url);
|
);
|
||||||
}}
|
});
|
||||||
/>
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function OAuthProvider({
|
interface OauthProviderProps {
|
||||||
provider,
|
|
||||||
redirectUrl,
|
|
||||||
scheme,
|
|
||||||
popupWindow,
|
|
||||||
}: {
|
|
||||||
provider: OAuthProviderType;
|
provider: OAuthProviderType;
|
||||||
redirectUrl?: string;
|
onContinue: (provider: OAuthProviderType) => void;
|
||||||
scheme?: string;
|
}
|
||||||
popupWindow: (url: string) => void;
|
|
||||||
}) {
|
function OAuthProvider({ onContinue, provider }: OauthProviderProps) {
|
||||||
const serverService = useService(ServerService);
|
|
||||||
const auth = useService(AuthService);
|
|
||||||
const { icon } = OAuthProviderMap[provider];
|
const { icon } = OAuthProviderMap[provider];
|
||||||
const t = useI18n();
|
|
||||||
|
|
||||||
const onClick = useAsyncCallback(async () => {
|
const onClick = useCallback(() => {
|
||||||
if (scheme && BUILD_CONFIG.isNative) {
|
onContinue(provider);
|
||||||
let oauthUrl = '';
|
}, [onContinue, provider]);
|
||||||
try {
|
|
||||||
oauthUrl = await auth.oauthPreflight(provider, scheme);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
const err = e as UserFriendlyError;
|
|
||||||
notify.error({
|
|
||||||
title: t[`error.${err.name}`](err.data),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
popupWindow(oauthUrl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
params.set('provider', provider);
|
|
||||||
|
|
||||||
if (redirectUrl) {
|
|
||||||
params.set('redirect_uri', redirectUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scheme) {
|
|
||||||
params.set('client', scheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Android app scheme not implemented
|
|
||||||
// if (BUILD_CONFIG.isAndroid) {}
|
|
||||||
|
|
||||||
const oauthUrl =
|
|
||||||
serverService.server.baseUrl + `/oauth/login?${params.toString()}`;
|
|
||||||
|
|
||||||
track.$.$.auth.signIn({ method: 'oauth', provider });
|
|
||||||
|
|
||||||
popupWindow(oauthUrl);
|
|
||||||
}, [popupWindow, provider, redirectUrl, scheme, serverService, auth, t]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={provider}
|
variant={provider === OAuthProviderType.Apple ? 'custom' : 'primary'}
|
||||||
variant="primary"
|
|
||||||
block
|
block
|
||||||
size="extraLarge"
|
size="extraLarge"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
type LoaderFunction,
|
type LoaderFunction,
|
||||||
redirect,
|
redirect,
|
||||||
useLoaderData,
|
useLoaderData,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
// oxlint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
useNavigate,
|
useNavigate,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -67,7 +67,7 @@ export const Component = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
auth
|
auth
|
||||||
.oauthPreflight(data.provider, data.client, data.redirectUri)
|
.oauthPreflight(data.provider, data.client, data.redirectUri)
|
||||||
.then(url => {
|
.then(({ url }) => {
|
||||||
// this is the url of oauth provider auth page, can't navigate with react-router
|
// this is the url of oauth provider auth page, can't navigate with react-router
|
||||||
location.href = url;
|
location.href = url;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export class AuthService extends Service {
|
|||||||
provider: OAuthProviderType,
|
provider: OAuthProviderType,
|
||||||
client: string,
|
client: string,
|
||||||
/** @deprecated*/ redirectUrl?: string
|
/** @deprecated*/ redirectUrl?: string
|
||||||
) {
|
): Promise<Record<string, string>> {
|
||||||
this.setClientNonce();
|
this.setClientNonce();
|
||||||
try {
|
try {
|
||||||
const res = await this.fetchService.fetch('/api/oauth/preflight', {
|
const res = await this.fetchService.fetch('/api/oauth/preflight', {
|
||||||
@@ -130,9 +130,7 @@ export class AuthService extends Service {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let { url } = await res.json();
|
return await res.json();
|
||||||
|
|
||||||
return url as string;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
track.$.$.auth.signInFail({
|
track.$.$.auth.signInFail({
|
||||||
method: 'oauth',
|
method: 'oauth',
|
||||||
|
|||||||
@@ -8150,6 +8150,12 @@ export function useAFFiNEI18N(): {
|
|||||||
* `The third-party account has already been connected to another user.`
|
* `The third-party account has already been connected to another user.`
|
||||||
*/
|
*/
|
||||||
["error.OAUTH_ACCOUNT_ALREADY_CONNECTED"](): string;
|
["error.OAUTH_ACCOUNT_ALREADY_CONNECTED"](): string;
|
||||||
|
/**
|
||||||
|
* `Invalid OAuth response: {{reason}}.`
|
||||||
|
*/
|
||||||
|
["error.INVALID_OAUTH_RESPONSE"](options: {
|
||||||
|
readonly reason: string;
|
||||||
|
}): string;
|
||||||
/**
|
/**
|
||||||
* `An invalid email provided: {{email}}`
|
* `An invalid email provided: {{email}}`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2035,6 +2035,7 @@
|
|||||||
"error.INVALID_AUTH_STATE": "Invalid auth state. You might start the auth progress from another device.",
|
"error.INVALID_AUTH_STATE": "Invalid auth state. You might start the auth progress from another device.",
|
||||||
"error.MISSING_OAUTH_QUERY_PARAMETER": "Missing query parameter `{{name}}`.",
|
"error.MISSING_OAUTH_QUERY_PARAMETER": "Missing query parameter `{{name}}`.",
|
||||||
"error.OAUTH_ACCOUNT_ALREADY_CONNECTED": "The third-party account has already been connected to another user.",
|
"error.OAUTH_ACCOUNT_ALREADY_CONNECTED": "The third-party account has already been connected to another user.",
|
||||||
|
"error.INVALID_OAUTH_RESPONSE": "Invalid OAuth response: {{reason}}.",
|
||||||
"error.INVALID_EMAIL": "An invalid email provided: {{email}}",
|
"error.INVALID_EMAIL": "An invalid email provided: {{email}}",
|
||||||
"error.INVALID_PASSWORD_LENGTH": "Password must be between {{min}} and {{max}} characters",
|
"error.INVALID_PASSWORD_LENGTH": "Password must be between {{min}} and {{max}} characters",
|
||||||
"error.PASSWORD_REQUIRED": "Password is required.",
|
"error.PASSWORD_REQUIRED": "Password is required.",
|
||||||
|
|||||||
93
yarn.lock
93
yarn.lock
@@ -958,6 +958,7 @@ __metadata:
|
|||||||
"@types/express-serve-static-core": "npm:^5.0.6"
|
"@types/express-serve-static-core": "npm:^5.0.6"
|
||||||
"@types/graphql-upload": "npm:^17.0.0"
|
"@types/graphql-upload": "npm:^17.0.0"
|
||||||
"@types/http-errors": "npm:^2.0.4"
|
"@types/http-errors": "npm:^2.0.4"
|
||||||
|
"@types/jsonwebtoken": "npm:^9.0.9"
|
||||||
"@types/lodash-es": "npm:^4.17.12"
|
"@types/lodash-es": "npm:^4.17.12"
|
||||||
"@types/mixpanel": "npm:^2.14.9"
|
"@types/mixpanel": "npm:^2.14.9"
|
||||||
"@types/mustache": "npm:^4.2.5"
|
"@types/mustache": "npm:^4.2.5"
|
||||||
@@ -990,6 +991,7 @@ __metadata:
|
|||||||
http-errors: "npm:^2.0.0"
|
http-errors: "npm:^2.0.0"
|
||||||
ioredis: "npm:^5.4.1"
|
ioredis: "npm:^5.4.1"
|
||||||
is-mobile: "npm:^5.0.0"
|
is-mobile: "npm:^5.0.0"
|
||||||
|
jsonwebtoken: "npm:^9.0.2"
|
||||||
keyv: "npm:^5.2.2"
|
keyv: "npm:^5.2.2"
|
||||||
lodash-es: "npm:^4.17.21"
|
lodash-es: "npm:^4.17.21"
|
||||||
mixpanel: "npm:^0.18.0"
|
mixpanel: "npm:^0.18.0"
|
||||||
@@ -15199,6 +15201,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/jsonwebtoken@npm:^9.0.9":
|
||||||
|
version: 9.0.9
|
||||||
|
resolution: "@types/jsonwebtoken@npm:9.0.9"
|
||||||
|
dependencies:
|
||||||
|
"@types/ms": "npm:*"
|
||||||
|
"@types/node": "npm:*"
|
||||||
|
checksum: 10/ef4dc05ae5ae78e3d2e20c364437e4afb788017cc80dd8a23a3eb17a3fcecb41e6abba254aba974d45a71307dd375aba4fda73cec358923aaaf8dff4667bea09
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/katex@npm:^0.16.0, @types/katex@npm:^0.16.7":
|
"@types/katex@npm:^0.16.0, @types/katex@npm:^0.16.7":
|
||||||
version: 0.16.7
|
version: 0.16.7
|
||||||
resolution: "@types/katex@npm:0.16.7"
|
resolution: "@types/katex@npm:0.16.7"
|
||||||
@@ -24802,6 +24814,24 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"jsonwebtoken@npm:^9.0.2":
|
||||||
|
version: 9.0.2
|
||||||
|
resolution: "jsonwebtoken@npm:9.0.2"
|
||||||
|
dependencies:
|
||||||
|
jws: "npm:^3.2.2"
|
||||||
|
lodash.includes: "npm:^4.3.0"
|
||||||
|
lodash.isboolean: "npm:^3.0.3"
|
||||||
|
lodash.isinteger: "npm:^4.0.4"
|
||||||
|
lodash.isnumber: "npm:^3.0.3"
|
||||||
|
lodash.isplainobject: "npm:^4.0.6"
|
||||||
|
lodash.isstring: "npm:^4.0.1"
|
||||||
|
lodash.once: "npm:^4.0.0"
|
||||||
|
ms: "npm:^2.1.1"
|
||||||
|
semver: "npm:^7.5.4"
|
||||||
|
checksum: 10/6e9b6d879cec2b27f2f3a88a0c0973edc7ba956a5d9356b2626c4fddfda969e34a3832deaf79c3e1c6c9a525bc2c4f2c2447fa477f8ac660f0017c31a59ae96b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"jsx-ast-utils@npm:3.3.5, jsx-ast-utils@npm:^2.4.1 || ^3.0.0":
|
"jsx-ast-utils@npm:3.3.5, jsx-ast-utils@npm:^2.4.1 || ^3.0.0":
|
||||||
version: 3.3.5
|
version: 3.3.5
|
||||||
resolution: "jsx-ast-utils@npm:3.3.5"
|
resolution: "jsx-ast-utils@npm:3.3.5"
|
||||||
@@ -24842,6 +24872,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"jwa@npm:^1.4.1":
|
||||||
|
version: 1.4.2
|
||||||
|
resolution: "jwa@npm:1.4.2"
|
||||||
|
dependencies:
|
||||||
|
buffer-equal-constant-time: "npm:^1.0.1"
|
||||||
|
ecdsa-sig-formatter: "npm:1.0.11"
|
||||||
|
safe-buffer: "npm:^5.0.1"
|
||||||
|
checksum: 10/a46c9ddbcc226d9e85e13ef96328c7d331abddd66b5a55ec44bcf4350464a6125385ac9c1e64faa0fae8d586d90a14d6b5e96c73f0388970a3918d5252efb0f3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"jwa@npm:^2.0.0":
|
"jwa@npm:^2.0.0":
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
resolution: "jwa@npm:2.0.1"
|
resolution: "jwa@npm:2.0.1"
|
||||||
@@ -24853,6 +24894,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"jws@npm:^3.2.2":
|
||||||
|
version: 3.2.2
|
||||||
|
resolution: "jws@npm:3.2.2"
|
||||||
|
dependencies:
|
||||||
|
jwa: "npm:^1.4.1"
|
||||||
|
safe-buffer: "npm:^5.0.1"
|
||||||
|
checksum: 10/70b016974af8a76d25030c80a0097b24ed5b17a9cf10f43b163c11cb4eb248d5d04a3fe48c0d724d2884c32879d878ccad7be0663720f46b464f662f7ed778fe
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"jws@npm:^4.0.0":
|
"jws@npm:^4.0.0":
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
resolution: "jws@npm:4.0.0"
|
resolution: "jws@npm:4.0.0"
|
||||||
@@ -25380,6 +25431,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.includes@npm:^4.3.0":
|
||||||
|
version: 4.3.0
|
||||||
|
resolution: "lodash.includes@npm:4.3.0"
|
||||||
|
checksum: 10/45e0a7c7838c931732cbfede6327da321b2b10482d5063ed21c020fa72b09ca3a4aa3bda4073906ab3f436cf36eb85a52ea3f08b7bab1e0baca8235b0e08fe51
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash.isarguments@npm:^3.1.0":
|
"lodash.isarguments@npm:^3.1.0":
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
resolution: "lodash.isarguments@npm:3.1.0"
|
resolution: "lodash.isarguments@npm:3.1.0"
|
||||||
@@ -25387,6 +25445,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isboolean@npm:^3.0.3":
|
||||||
|
version: 3.0.3
|
||||||
|
resolution: "lodash.isboolean@npm:3.0.3"
|
||||||
|
checksum: 10/b70068b4a8b8837912b54052557b21fc4774174e3512ed3c5b94621e5aff5eb6c68089d0a386b7e801d679cd105d2e35417978a5e99071750aa2ed90bffd0250
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash.isequal@npm:^4.5.0":
|
"lodash.isequal@npm:^4.5.0":
|
||||||
version: 4.5.0
|
version: 4.5.0
|
||||||
resolution: "lodash.isequal@npm:4.5.0"
|
resolution: "lodash.isequal@npm:4.5.0"
|
||||||
@@ -25394,6 +25459,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isinteger@npm:^4.0.4":
|
||||||
|
version: 4.0.4
|
||||||
|
resolution: "lodash.isinteger@npm:4.0.4"
|
||||||
|
checksum: 10/c971f5a2d67384f429892715550c67bac9f285604a0dd79275fd19fef7717aec7f2a6a33d60769686e436ceb9771fd95fe7fcb68ad030fc907d568d5a3b65f70
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash.ismatch@npm:^4.4.0":
|
"lodash.ismatch@npm:^4.4.0":
|
||||||
version: 4.4.0
|
version: 4.4.0
|
||||||
resolution: "lodash.ismatch@npm:4.4.0"
|
resolution: "lodash.ismatch@npm:4.4.0"
|
||||||
@@ -25401,6 +25473,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isnumber@npm:^3.0.3":
|
||||||
|
version: 3.0.3
|
||||||
|
resolution: "lodash.isnumber@npm:3.0.3"
|
||||||
|
checksum: 10/913784275b565346255e6ae6a6e30b760a0da70abc29f3e1f409081585875105138cda4a429ff02577e1bc0a7ae2a90e0a3079a37f3a04c3d6c5aaa532f4cab2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash.isplainobject@npm:^4.0.6":
|
"lodash.isplainobject@npm:^4.0.6":
|
||||||
version: 4.0.6
|
version: 4.0.6
|
||||||
resolution: "lodash.isplainobject@npm:4.0.6"
|
resolution: "lodash.isplainobject@npm:4.0.6"
|
||||||
@@ -25408,6 +25487,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isstring@npm:^4.0.1":
|
||||||
|
version: 4.0.1
|
||||||
|
resolution: "lodash.isstring@npm:4.0.1"
|
||||||
|
checksum: 10/eaac87ae9636848af08021083d796e2eea3d02e80082ab8a9955309569cb3a463ce97fd281d7dc119e402b2e7d8c54a23914b15d2fc7fff56461511dc8937ba0
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash.kebabcase@npm:^4.1.1":
|
"lodash.kebabcase@npm:^4.1.1":
|
||||||
version: 4.1.1
|
version: 4.1.1
|
||||||
resolution: "lodash.kebabcase@npm:4.1.1"
|
resolution: "lodash.kebabcase@npm:4.1.1"
|
||||||
@@ -25443,6 +25529,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.once@npm:^4.0.0":
|
||||||
|
version: 4.1.1
|
||||||
|
resolution: "lodash.once@npm:4.1.1"
|
||||||
|
checksum: 10/202f2c8c3d45e401b148a96de228e50ea6951ee5a9315ca5e15733d5a07a6b1a02d9da1e7fdf6950679e17e8ca8f7190ec33cae47beb249b0c50019d753f38f3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash.snakecase@npm:^4.1.1":
|
"lodash.snakecase@npm:^4.1.1":
|
||||||
version: 4.1.1
|
version: 4.1.1
|
||||||
resolution: "lodash.snakecase@npm:4.1.1"
|
resolution: "lodash.snakecase@npm:4.1.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user