D gamer007/add microsoft oauth (#5103)

Need to create a new branch because original branch name is `main` and
we cannot push additional commits
Linked to https://github.com/twentyhq/twenty/pull/4718


![image](https://github.com/twentyhq/twenty/assets/29927851/52b220e7-770a-4ffe-b6e9-468605c2b8fa)

![image](https://github.com/twentyhq/twenty/assets/29927851/7a7a4737-f09f-4d9b-8962-5a9b8c71edc1)

---------

Co-authored-by: DGamer007 <prajapatidhruv266@gmail.com>
This commit is contained in:
martmull 2024-04-24 14:56:02 +02:00 committed by GitHub
parent b3e1d6becf
commit 87a9ecee28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 458 additions and 129 deletions

View File

@ -61,6 +61,7 @@
"@types/lodash.pick": "^4.3.7",
"@types/mailparser": "^3.4.4",
"@types/nodemailer": "^6.4.14",
"@types/passport-microsoft": "^1.0.3",
"add": "^2.0.6",
"afterframe": "^1.0.2",
"apollo-server-express": "^3.12.0",
@ -136,6 +137,7 @@
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"passport-microsoft": "^2.0.0",
"patch-package": "^8.0.0",
"pg": "^8.11.3",
"pg-boss": "^9.0.3",

View File

@ -70,10 +70,16 @@ import TabItem from '@theme/TabItem';
['MESSAGING_PROVIDER_GMAIL_ENABLED', 'false', 'Enable Gmail API connection'],
['CALENDAR_PROVIDER_GMAIL_ENABLED', 'false', 'Enable Google Calendar API connection'],
['AUTH_GOOGLE_APIS_CALLBACK_URL', '', 'Google APIs auth callback'],
['AUTH_GOOGLE_ENABLED', 'false', 'Enable Goole SSO login'],
['AUTH_PASSWORD_ENABLED', 'false', 'Enable Email/Password login'],
['AUTH_GOOGLE_ENABLED', 'false', 'Enable Google SSO login'],
['AUTH_GOOGLE_CLIENT_ID', '', 'Google client ID'],
['AUTH_GOOGLE_CLIENT_SECRET', '', 'Google client secret'],
['AUTH_GOOGLE_CALLBACK_URL', '', 'Google auth callback'],
['AUTH_MICROSOFT_ENABLED', 'false', 'Enable Microsoft SSO login'],
['AUTH_MICROSOFT_CLIENT_ID', '', 'Microsoft client ID'],
['AUTH_MICROSOFT_TENANT_ID', '', 'Microsoft tenant ID'],
['AUTH_MICROSOFT_CLIENT_SECRET', '', 'Microsoft client secret'],
['AUTH_MICROSOFT_CALLBACK_URL', '', 'Microsoft auth callback'],
['FRONT_AUTH_CALLBACK_URL', 'http://localhost:3001/verify ', 'Callback used for Login page'],
['IS_SIGN_UP_DISABLED', 'false', 'Disable sign-up'],
['PASSWORD_RESET_TOKEN_EXPIRES_IN', '5m', 'Password reset token expiration time'],

View File

@ -56,6 +56,7 @@ export type AuthProviders = {
__typename?: 'AuthProviders';
google: Scalars['Boolean'];
magicLink: Scalars['Boolean'];
microsoft: Scalars['Boolean'];
password: Scalars['Boolean'];
};
@ -562,6 +563,7 @@ export type RemoteServer = {
export type RemoteTable = {
__typename?: 'RemoteTable';
id?: Maybe<Scalars['UUID']>;
name: Scalars['String'];
schema: Scalars['String'];
status: RemoteTableStatus;
@ -1111,7 +1113,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null } } };
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null } } };
export type UploadFileMutationVariables = Exact<{
file: Scalars['Upload'];
@ -2140,6 +2142,7 @@ export const GetClientConfigDocument = gql`
authProviders {
google
password
microsoft
}
billing {
isBillingEnabled

View File

@ -116,8 +116,9 @@ describe('useAuth', () => {
expect(state.icons).toEqual({});
expect(state.authProviders).toEqual({
google: false,
microsoft: false,
magicLink: false,
password: true,
password: false,
});
expect(state.billing).toBeNull();
expect(state.isSignInPrefilled).toBe(false);

View File

@ -234,6 +234,14 @@ export const useAuth = () => {
}` || '';
}, []);
const handleMicrosoftLogin = useCallback((workspaceInviteHash?: string) => {
const authServerUrl = REACT_APP_SERVER_BASE_URL;
window.location.href =
`${authServerUrl}/auth/microsoft/${
workspaceInviteHash ? '?inviteHash=' + workspaceInviteHash : ''
}` || '';
}, []);
return {
challenge: handleChallenge,
verify: handleVerify,
@ -244,5 +252,6 @@ export const useAuth = () => {
signUpWithCredentials: handleCredentialsSignUp,
signInWithCredentials: handleCrendentialsSignIn,
signInWithGoogle: handleGoogleLogin,
signInWithMicrosoft: handleMicrosoftLogin,
};
};

View File

@ -1,12 +1,19 @@
import { JSX } from 'react';
import styled from '@emotion/styled';
const StyledSeparator = styled.div`
type HorizontalSeparatorProps = {
visible?: boolean;
};
const StyledSeparator = styled.div<HorizontalSeparatorProps>`
background-color: ${({ theme }) => theme.border.color.medium};
height: 1px;
height: ${({ visible }) => (visible ? '1px' : 0)};
margin-bottom: ${({ theme }) => theme.spacing(3)};
margin-top: ${({ theme }) => theme.spacing(3)};
width: 100%;
`;
export const HorizontalSeparator = (): JSX.Element => <StyledSeparator />;
export const HorizontalSeparator = ({
visible = true,
}: HorizontalSeparatorProps): JSX.Element => (
<StyledSeparator visible={visible} />
);

View File

@ -4,11 +4,12 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useRecoilState } from 'recoil';
import { IconGoogle } from 'twenty-ui';
import { IconGoogle, IconMicrosoft } from 'twenty-ui';
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { Loader } from '@/ui/feedback/loader/components/Loader';
@ -27,7 +28,6 @@ import { HorizontalSeparator } from './HorizontalSeparator';
const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
margin-top: ${({ theme }) => theme.spacing(4)};
width: 200px;
`;
const StyledForm = styled.form`
@ -51,6 +51,7 @@ export const SignInUpForm = () => {
const { handleResetPassword } = useHandleResetPassword();
const workspace = useWorkspaceFromInviteHash();
const { signInWithGoogle } = useSignInWithGoogle();
const { signInWithMicrosoft } = useSignInWithMicrosoft();
const { form } = useSignInUpForm();
const {
@ -125,119 +126,133 @@ export const SignInUpForm = () => {
onClick={signInWithGoogle}
fullWidth
/>
<HorizontalSeparator />
<HorizontalSeparator visible={!authProviders.microsoft} />
</>
)}
<StyledForm
onSubmit={(event) => {
event.preventDefault();
}}
>
{signInUpStep !== SignInUpStep.Init && (
<StyledFullWidthMotionDiv
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
transition={{
type: 'spring',
stiffness: 800,
damping: 35,
}}
>
<Controller
name="email"
control={form.control}
render={({
field: { onChange, onBlur, value },
fieldState: { error },
}) => (
<StyledInputContainer>
<TextInput
autoFocus
value={value}
placeholder="Email"
onBlur={onBlur}
onChange={(value: string) => {
onChange(value);
if (signInUpStep === SignInUpStep.Password) {
continueWithEmail();
}
}}
error={showErrors ? error?.message : undefined}
fullWidth
disableHotkeys
onKeyDown={handleKeyDown}
/>
</StyledInputContainer>
)}
/>
</StyledFullWidthMotionDiv>
)}
{signInUpStep === SignInUpStep.Password && (
<StyledFullWidthMotionDiv
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
transition={{
type: 'spring',
stiffness: 800,
damping: 35,
}}
>
<Controller
name="password"
control={form.control}
render={({
field: { onChange, onBlur, value },
fieldState: { error },
}) => (
<StyledInputContainer>
<TextInput
autoFocus
value={value}
type="password"
placeholder="Password"
onBlur={onBlur}
onChange={onChange}
error={showErrors ? error?.message : undefined}
fullWidth
disableHotkeys
onKeyDown={handleKeyDown}
/>
</StyledInputContainer>
)}
/>
</StyledFullWidthMotionDiv>
)}
{authProviders.microsoft && (
<>
<MainButton
Icon={() => <IconMicrosoft size={theme.icon.size.lg} />}
title="Continue with Microsoft"
onClick={signInWithMicrosoft}
fullWidth
/>
<HorizontalSeparator visible={authProviders.password} />
</>
)}
<MainButton
variant="secondary"
title={buttonTitle}
type="submit"
onClick={() => {
if (signInUpStep === SignInUpStep.Init) {
continueWithEmail();
return;
}
if (signInUpStep === SignInUpStep.Email) {
continueWithCredentials();
return;
}
setShowErrors(true);
form.handleSubmit(submitCredentials)();
{authProviders.password && (
<StyledForm
onSubmit={(event) => {
event.preventDefault();
}}
Icon={() => form.formState.isSubmitting && <Loader />}
disabled={
signInUpStep === SignInUpStep.Init
? false
: signInUpStep === SignInUpStep.Email
? !form.watch('email')
: !form.watch('email') ||
!form.watch('password') ||
form.formState.isSubmitting
}
fullWidth
/>
</StyledForm>
>
{signInUpStep !== SignInUpStep.Init && (
<StyledFullWidthMotionDiv
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
transition={{
type: 'spring',
stiffness: 800,
damping: 35,
}}
>
<Controller
name="email"
control={form.control}
render={({
field: { onChange, onBlur, value },
fieldState: { error },
}) => (
<StyledInputContainer>
<TextInput
autoFocus
value={value}
placeholder="Email"
onBlur={onBlur}
onChange={(value: string) => {
onChange(value);
if (signInUpStep === SignInUpStep.Password) {
continueWithEmail();
}
}}
error={showErrors ? error?.message : undefined}
fullWidth
disableHotkeys
onKeyDown={handleKeyDown}
/>
</StyledInputContainer>
)}
/>
</StyledFullWidthMotionDiv>
)}
{signInUpStep === SignInUpStep.Password && (
<StyledFullWidthMotionDiv
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
transition={{
type: 'spring',
stiffness: 800,
damping: 35,
}}
>
<Controller
name="password"
control={form.control}
render={({
field: { onChange, onBlur, value },
fieldState: { error },
}) => (
<StyledInputContainer>
<TextInput
autoFocus
value={value}
type="password"
placeholder="Password"
onBlur={onBlur}
onChange={onChange}
error={showErrors ? error?.message : undefined}
fullWidth
disableHotkeys
onKeyDown={handleKeyDown}
/>
</StyledInputContainer>
)}
/>
</StyledFullWidthMotionDiv>
)}
<MainButton
variant="secondary"
title={buttonTitle}
type="submit"
onClick={() => {
if (signInUpStep === SignInUpStep.Init) {
continueWithEmail();
return;
}
if (signInUpStep === SignInUpStep.Email) {
continueWithCredentials();
return;
}
setShowErrors(true);
form.handleSubmit(submitCredentials)();
}}
Icon={() => form.formState.isSubmitting && <Loader />}
disabled={
signInUpStep === SignInUpStep.Init
? false
: signInUpStep === SignInUpStep.Email
? !form.watch('email')
: !form.watch('email') ||
!form.watch('password') ||
form.formState.isSubmitting
}
fullWidth
/>
</StyledForm>
)}
</StyledContentContainer>
{signInUpStep === SignInUpStep.Password && (
<ActionLink onClick={handleResetPassword(form.getValues('email'))}>

View File

@ -0,0 +1,11 @@
import { useParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth.ts';
export const useSignInWithMicrosoft = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
const { signInWithMicrosoft } = useAuth();
return {
signInWithMicrosoft: () => signInWithMicrosoft(workspaceInviteHash),
};
};

View File

@ -38,6 +38,7 @@ export const ClientConfigProviderEffect = () => {
setIsClientConfigLoaded(true);
setAuthProviders({
google: data?.clientConfig.authProviders.google,
microsoft: data?.clientConfig.authProviders.microsoft,
password: data?.clientConfig.authProviders.password,
magicLink: false,
});

View File

@ -6,6 +6,7 @@ export const GET_CLIENT_CONFIG = gql`
authProviders {
google
password
microsoft
}
billing {
isBillingEnabled

View File

@ -4,5 +4,10 @@ import { AuthProviders } from '~/generated/graphql';
export const authProvidersState = createState<AuthProviders>({
key: 'authProvidersState',
defaultValue: { google: false, magicLink: false, password: true },
defaultValue: {
google: false,
magicLink: false,
password: false,
microsoft: false,
},
});

View File

@ -21,12 +21,18 @@ SIGN_IN_PREFILLED=true
# REFRESH_TOKEN_EXPIRES_IN=90d
# FILE_TOKEN_EXPIRES_IN=1d
# FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify
# AUTH_GOOGLE_ENABLED=false
# MESSAGING_PROVIDER_GMAIL_ENABLED=false
# CALENDAR_PROVIDER_GOOGLE_ENABLED=false
# IS_BILLING_ENABLED=false
# BILLING_PLAN_REQUIRED_LINK=https://twenty.com/stripe-redirection
# AUTH_PASSWORD_ENABLED=false
# IS_SIGN_UP_DISABLED=false
# AUTH_MICROSOFT_ENABLED=false
# AUTH_MICROSOFT_CLIENT_ID=replace_me_with_azure_client_id
# AUTH_MICROSOFT_TENANT_ID=replace_me_with_azure_tenant_id
# AUTH_MICROSOFT_CLIENT_SECRET=replace_me_with_azure_client_secret
# AUTH_MICROSOFT_CALLBACK_URL=http://localhost:3000/auth/microsoft/redirect
# AUTH_GOOGLE_ENABLED=false
# AUTH_GOOGLE_CLIENT_ID=replace_me_with_google_client_id
# AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret
# AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect

View File

@ -21,6 +21,7 @@ import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller';
import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata';
@ -65,6 +66,7 @@ const jwtModule = JwtModule.registerAsync({
],
controllers: [
GoogleAuthController,
MicrosoftAuthController,
GoogleAPIsAuthController,
VerifyAuthController,
],

View File

@ -0,0 +1,49 @@
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import { Response } from 'express';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { MicrosoftOAuthGuard } from 'src/engine/core-modules/auth/guards/microsoft-oauth.guard';
import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard';
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
@Controller('auth/microsoft')
export class MicrosoftAuthController {
constructor(
private readonly tokenService: TokenService,
private readonly typeORMService: TypeORMService,
private readonly authService: AuthService,
) {}
@Get()
@UseGuards(MicrosoftProviderEnabledGuard, MicrosoftOAuthGuard)
async microsoftAuth() {
// As this method is protected by Microsoft Auth guard, it will trigger Microsoft SSO flow
return;
}
@Get('redirect')
@UseGuards(MicrosoftProviderEnabledGuard, MicrosoftOAuthGuard)
async microsoftAuthRedirect(
@Req() req: MicrosoftRequest,
@Res() res: Response,
) {
const { firstName, lastName, email, picture, workspaceInviteHash } =
req.user;
const user = await this.authService.signInUp({
email,
firstName,
lastName,
picture,
workspaceInviteHash,
fromSSO: true,
});
const loginToken = await this.tokenService.generateLoginToken(user.email);
return res.redirect(this.tokenService.computeRedirectURI(loginToken.token));
}
}

View File

@ -16,8 +16,7 @@ export class GoogleOauthGuard extends AuthGuard('google') {
if (workspaceInviteHash && typeof workspaceInviteHash === 'string') {
request.params.workspaceInviteHash = workspaceInviteHash;
}
const activate = (await super.canActivate(context)) as boolean;
return activate;
return (await super.canActivate(context)) as boolean;
}
}

View File

@ -0,0 +1,22 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class MicrosoftOAuthGuard extends AuthGuard('microsoft') {
constructor() {
super({
prompt: 'select_account',
});
}
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const workspaceInviteHash = request.query.inviteHash;
if (workspaceInviteHash && typeof workspaceInviteHash === 'string') {
request.params.workspaceInviteHash = workspaceInviteHash;
}
return (await super.canActivate(context)) as boolean;
}
}

View File

@ -0,0 +1,21 @@
import { Injectable, CanActivate, NotFoundException } from '@nestjs/common';
import { Observable } from 'rxjs';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { MicrosoftStrategy } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
@Injectable()
export class MicrosoftProviderEnabledGuard implements CanActivate {
constructor(private readonly environmentService: EnvironmentService) {}
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
if (!this.environmentService.get('AUTH_MICROSOFT_ENABLED')) {
throw new NotFoundException('Microsoft auth is not enabled');
}
new MicrosoftStrategy(this.environmentService);
return true;
}
}

View File

@ -0,0 +1,76 @@
import { BadRequestException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import { VerifyCallback } from 'passport-google-oauth20';
import { Strategy } from 'passport-microsoft';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
export type MicrosoftRequest = Omit<
Request,
'user' | 'workspace' | 'cacheVersion'
> & {
user: {
firstName?: string | null;
lastName?: string | null;
email: string;
picture: string | null;
workspaceInviteHash?: string;
};
};
export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
constructor(environmentService: EnvironmentService) {
super({
clientID: environmentService.get('AUTH_MICROSOFT_CLIENT_ID'),
clientSecret: environmentService.get('AUTH_MICROSOFT_CLIENT_SECRET'),
callbackURL: environmentService.get('AUTH_MICROSOFT_CALLBACK_URL'),
tenant: environmentService.get('AUTH_MICROSOFT_TENANT_ID'),
scope: ['user.read'],
passReqToCallback: true,
});
}
authenticate(req: any, options: any) {
options = {
...options,
state: JSON.stringify({
workspaceInviteHash: req.params.workspaceInviteHash,
}),
};
return super.authenticate(req, options);
}
async validate(
request: MicrosoftRequest,
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<void> {
const { name, emails, photos } = profile;
const state =
typeof request.query.state === 'string'
? JSON.parse(request.query.state)
: undefined;
const email = emails?.[0]?.value ?? null;
if (!email) {
throw new BadRequestException('No email found in your Microsoft profile');
}
const user: MicrosoftRequest['user'] = {
email,
firstName: name.givenName,
lastName: name.familyName,
picture: photos?.[0]?.value,
workspaceInviteHash: state.workspaceInviteHash,
};
done(null, user);
}
}

View File

@ -10,6 +10,9 @@ class AuthProviders {
@Field(() => Boolean)
password: boolean;
@Field(() => Boolean)
microsoft: boolean;
}
@ObjectType()

View File

@ -14,7 +14,8 @@ export class ClientConfigResolver {
authProviders: {
google: this.environmentService.get('AUTH_GOOGLE_ENABLED'),
magicLink: false,
password: true,
password: this.environmentService.get('AUTH_PASSWORD_ENABLED'),
microsoft: this.environmentService.get('AUTH_MICROSOFT_ENABLED'),
},
telemetry: {
enabled: this.environmentService.get('TELEMETRY_ENABLED'),

View File

@ -52,11 +52,6 @@ export class EnvironmentVariables {
@Max(65535)
DEBUG_PORT = 9000;
@CastToBoolean()
@IsOptional()
@IsBoolean()
SIGN_IN_PREFILLED = false;
@CastToBoolean()
@IsOptional()
@IsBoolean()
@ -156,21 +151,53 @@ export class EnvironmentVariables {
@IsOptional()
FRONT_AUTH_CALLBACK_URL: string;
@CastToBoolean()
@IsOptional()
@IsBoolean()
AUTH_PASSWORD_ENABLED = true;
@CastToBoolean()
@IsOptional()
@IsBoolean()
@ValidateIf((env) => env.AUTH_PASSWORD_ENABLED)
SIGN_IN_PREFILLED = false;
@CastToBoolean()
@IsOptional()
@IsBoolean()
AUTH_MICROSOFT_ENABLED = false;
@IsString()
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
AUTH_MICROSOFT_CLIENT_ID: string;
@IsString()
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
AUTH_MICROSOFT_TENANT_ID: string;
@IsString()
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
AUTH_MICROSOFT_CLIENT_SECRET: string;
@IsUrl({ require_tld: false })
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
AUTH_MICROSOFT_CALLBACK_URL: string;
@CastToBoolean()
@IsOptional()
@IsBoolean()
AUTH_GOOGLE_ENABLED = false;
@IsString()
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED === true)
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
AUTH_GOOGLE_CLIENT_ID: string;
@IsString()
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED === true)
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
AUTH_GOOGLE_CLIENT_SECRET: string;
@IsUrl({ require_tld: false })
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED === true)
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
AUTH_GOOGLE_CALLBACK_URL: string;
// Storage

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
<path fill="#F35325" d="M1 1h6.5v6.5H1V1z"/>
<path fill="#81BC06" d="M8.5 1H15v6.5H8.5V1z"/>
<path fill="#05A6F0" d="M1 8.5h6.5V15H1V8.5z"/>
<path fill="#FFBA08" d="M8.5 8.5H15V15H8.5V8.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@ -0,0 +1,14 @@
import { useTheme } from '@emotion/react';
import IconMicrosoftRaw from '../assets/microsoft.svg?react';
interface IconMicrosoftProps {
size?: number;
}
export const IconMicrosoft = (props: IconMicrosoftProps) => {
const theme = useTheme();
const size = props.size ?? theme.icon.size.lg;
return <IconMicrosoftRaw height={size} width={size} />;
};

View File

@ -8,6 +8,7 @@ export * from './icon/components/IconAddressBook';
export * from './icon/components/IconGmail';
export * from './icon/components/IconGoogle';
export * from './icon/components/IconGoogleCalendar';
export * from './icon/components/IconMicrosoft';
export * from './icon/components/IconTwentyStar';
export * from './icon/components/IconTwentyStarFilled';
export * from './icon/components/TablerIcons';

View File

@ -17093,6 +17093,15 @@ __metadata:
languageName: node
linkType: hard
"@types/passport-microsoft@npm:^1.0.3":
version: 1.0.3
resolution: "@types/passport-microsoft@npm:1.0.3"
dependencies:
"@types/passport-oauth2": "npm:*"
checksum: d177f63ae4976253242966146030b9913dc3b77c4eb192f5491d397f9036b64f258325157c244960ef4539e9c46807d0bba53497630253f4ef98d1318142b3d8
languageName: node
linkType: hard
"@types/passport-oauth2@npm:*":
version: 1.4.15
resolution: "@types/passport-oauth2@npm:1.4.15"
@ -38165,6 +38174,13 @@ __metadata:
languageName: node
linkType: hard
"oauth@npm:0.10.x":
version: 0.10.0
resolution: "oauth@npm:0.10.0"
checksum: 76f3e186cfd76cb33e5d5d442861c86680a5c3b71b2db1b854212087532c265a69de1a2ab9db683e6c6df733e17cfc67476527b81b224a19c1917de2bc3f75fa
languageName: node
linkType: hard
"oauth@npm:0.9.x":
version: 0.9.15
resolution: "oauth@npm:0.9.15"
@ -39110,6 +39126,28 @@ __metadata:
languageName: node
linkType: hard
"passport-microsoft@npm:^2.0.0":
version: 2.0.0
resolution: "passport-microsoft@npm:2.0.0"
dependencies:
passport-oauth2: "npm:1.8.0"
checksum: 6b7d053673d5af6cbbce3812a628afff2435aa321d70c9feaa3309d7a610a2e61c753a7cde8c0febee2cacf74b971fcdc7699fa1c073c72052bc4334f1b203ec
languageName: node
linkType: hard
"passport-oauth2@npm:1.8.0":
version: 1.8.0
resolution: "passport-oauth2@npm:1.8.0"
dependencies:
base64url: "npm:3.x.x"
oauth: "npm:0.10.x"
passport-strategy: "npm:1.x.x"
uid2: "npm:0.0.x"
utils-merge: "npm:1.x.x"
checksum: 16b431bd856b84dfe0c9c913dcbea6ff54875befac1035171b0dce1c77f79072dc5e26d785b13c2e62c034c8174a1a47571751d1066bdbcdb9108de217c0b19b
languageName: node
linkType: hard
"passport-oauth2@npm:1.x.x":
version: 1.7.0
resolution: "passport-oauth2@npm:1.7.0"
@ -46629,6 +46667,7 @@ __metadata:
"@types/nodemailer": "npm:^6.4.14"
"@types/passport-google-oauth20": "npm:^2.0.11"
"@types/passport-jwt": "npm:^3.0.8"
"@types/passport-microsoft": "npm:^1.0.3"
"@types/react": "npm:^18.2.39"
"@types/react-datepicker": "npm:^6.2.0"
"@types/react-dom": "npm:^18.2.15"
@ -46745,6 +46784,7 @@ __metadata:
passport-google-oauth20: "npm:^2.0.0"
passport-jwt: "npm:^4.0.1"
passport-local: "npm:^1.0.0"
passport-microsoft: "npm:^2.0.0"
patch-package: "npm:^8.0.0"
pg: "npm:^8.11.3"
pg-boss: "npm:^9.0.3"