feature: reduce .env usage and move configurations to admin dashboard (#5194)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: nivedin <nivedinp@gmail.com> Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
37671ac9e7
commit
0b7d31a20c
49 changed files with 3330 additions and 899 deletions
55
.env.example
55
.env.example
|
|
@ -2,24 +2,9 @@
|
||||||
# Prisma Config
|
# Prisma Config
|
||||||
DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch
|
DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch
|
||||||
|
|
||||||
# Auth Tokens Config
|
|
||||||
JWT_SECRET="secret1233"
|
|
||||||
TOKEN_SALT_COMPLEXITY=10
|
|
||||||
MAGIC_LINK_TOKEN_VALIDITY= 3
|
|
||||||
# Default validity is 7 days (604800000 ms) in ms
|
|
||||||
REFRESH_TOKEN_VALIDITY="604800000"
|
|
||||||
# Default validity is 1 day (86400000 ms) in ms
|
|
||||||
ACCESS_TOKEN_VALIDITY="86400000"
|
|
||||||
SESSION_SECRET='add some secret here'
|
|
||||||
# Reccomended to be true, set to false if you are using http
|
|
||||||
# Note: Some auth providers may not support http requests
|
|
||||||
ALLOW_SECURE_COOKIES=true
|
|
||||||
|
|
||||||
# Sensitive Data Encryption Key while storing in Database (32 character)
|
# Sensitive Data Encryption Key while storing in Database (32 character)
|
||||||
DATA_ENCRYPTION_KEY="data encryption key with 32 char"
|
DATA_ENCRYPTION_KEY="data encryption key with 32 char"
|
||||||
|
|
||||||
# Hoppscotch App Domain Config
|
|
||||||
REDIRECT_URL="http://localhost:3000"
|
|
||||||
# Whitelisted origins for the Hoppscotch App.
|
# Whitelisted origins for the Hoppscotch App.
|
||||||
# This list controls which origins can interact with the app through cross-origin comms.
|
# This list controls which origins can interact with the app through cross-origin comms.
|
||||||
# - localhost ports (3170, 3000, 3100): app, backend, development servers and services
|
# - localhost ports (3170, 3000, 3100): app, backend, development servers and services
|
||||||
|
|
@ -28,50 +13,10 @@ REDIRECT_URL="http://localhost:3000"
|
||||||
# NOT where the app runs. The app itself uses the `app://` protocol with dynamic
|
# NOT where the app runs. The app itself uses the `app://` protocol with dynamic
|
||||||
# bundle names like `app://{bundle-name}/`
|
# bundle names like `app://{bundle-name}/`
|
||||||
WHITELISTED_ORIGINS="http://localhost:3170,http://localhost:3000,http://localhost:3100,app://localhost_3200,app://hoppscotch"
|
WHITELISTED_ORIGINS="http://localhost:3170,http://localhost:3000,http://localhost:3100,app://localhost_3200,app://hoppscotch"
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS=GOOGLE,GITHUB,MICROSOFT,EMAIL
|
|
||||||
|
|
||||||
# Google Auth Config
|
|
||||||
GOOGLE_CLIENT_ID="************************************************"
|
|
||||||
GOOGLE_CLIENT_SECRET="************************************************"
|
|
||||||
GOOGLE_CALLBACK_URL="http://localhost:3170/v1/auth/google/callback"
|
|
||||||
GOOGLE_SCOPE="email,profile"
|
|
||||||
|
|
||||||
# Github Auth Config
|
|
||||||
GITHUB_CLIENT_ID="************************************************"
|
|
||||||
GITHUB_CLIENT_SECRET="************************************************"
|
|
||||||
GITHUB_CALLBACK_URL="http://localhost:3170/v1/auth/github/callback"
|
|
||||||
GITHUB_SCOPE="user:email"
|
|
||||||
|
|
||||||
# Microsoft Auth Config
|
|
||||||
MICROSOFT_CLIENT_ID="************************************************"
|
|
||||||
MICROSOFT_CLIENT_SECRET="************************************************"
|
|
||||||
MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback"
|
|
||||||
MICROSOFT_SCOPE="user.read"
|
|
||||||
MICROSOFT_TENANT="common"
|
|
||||||
|
|
||||||
# Mailer config
|
|
||||||
MAILER_SMTP_ENABLE="true"
|
|
||||||
MAILER_USE_CUSTOM_CONFIGS="false"
|
|
||||||
MAILER_ADDRESS_FROM='"From Name Here" <from@example.com>'
|
|
||||||
|
|
||||||
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com" # used if custom mailer configs is false
|
|
||||||
|
|
||||||
# The following are used if custom mailer configs is true
|
|
||||||
MAILER_SMTP_HOST="smtp.domain.com"
|
|
||||||
MAILER_SMTP_PORT="587"
|
|
||||||
MAILER_SMTP_SECURE="true"
|
|
||||||
MAILER_SMTP_USER="user@domain.com"
|
|
||||||
MAILER_SMTP_PASSWORD="pass"
|
|
||||||
MAILER_TLS_REJECT_UNAUTHORIZED="true"
|
|
||||||
|
|
||||||
# Rate Limit Config
|
|
||||||
RATE_LIMIT_TTL=60 # In seconds
|
|
||||||
RATE_LIMIT_MAX=100 # Max requests per IP
|
|
||||||
|
|
||||||
|
|
||||||
#-----------------------Frontend Config------------------------------#
|
#-----------------------Frontend Config------------------------------#
|
||||||
|
|
||||||
|
|
||||||
# Base URLs
|
# Base URLs
|
||||||
VITE_BASE_URL=http://localhost:3000
|
VITE_BASE_URL=http://localhost:3000
|
||||||
VITE_SHORTCODE_BASE_URL=http://localhost:3000
|
VITE_SHORTCODE_BASE_URL=http://localhost:3000
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@
|
||||||
"handlebars": "4.7.8",
|
"handlebars": "4.7.8",
|
||||||
"io-ts": "2.2.22",
|
"io-ts": "2.2.22",
|
||||||
"luxon": "3.7.1",
|
"luxon": "3.7.1",
|
||||||
|
"morgan": "1.10.1",
|
||||||
"nodemailer": "7.0.5",
|
"nodemailer": "7.0.5",
|
||||||
"passport": "0.7.0",
|
"passport": "0.7.0",
|
||||||
"passport-github2": "0.1.12",
|
"passport-github2": "0.1.12",
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ import { PubSubModule } from './pubsub/pubsub.module';
|
||||||
}),
|
}),
|
||||||
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
||||||
driver: ApolloDriver,
|
driver: ApolloDriver,
|
||||||
imports: [ConfigModule],
|
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: async (configService: ConfigService) => {
|
useFactory: async (configService: ConfigService) => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -92,12 +91,11 @@ import { PubSubModule } from './pubsub/pubsub.module';
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
ThrottlerModule.forRootAsync({
|
ThrottlerModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: async (configService: ConfigService) => [
|
useFactory: async (configService: ConfigService) => [
|
||||||
{
|
{
|
||||||
ttl: +configService.get('RATE_LIMIT_TTL'),
|
ttl: +configService.get('INFRA.RATE_LIMIT_TTL'),
|
||||||
limit: +configService.get('RATE_LIMIT_MAX'),
|
limit: +configService.get('INFRA.RATE_LIMIT_MAX'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ export class AuthController {
|
||||||
async verify(@Body() data: VerifyMagicDto, @Res() res: Response) {
|
async verify(@Body() data: VerifyMagicDto, @Res() res: Response) {
|
||||||
const authTokens = await this.authService.verifyMagicLinkTokens(data);
|
const authTokens = await this.authService.verifyMagicLinkTokens(data);
|
||||||
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
||||||
authCookieHandler(res, authTokens.right, false, null);
|
authCookieHandler(res, authTokens.right, false, null, this.configService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -95,7 +95,7 @@ export class AuthController {
|
||||||
user,
|
user,
|
||||||
);
|
);
|
||||||
if (E.isLeft(newTokenPair)) throwHTTPErr(newTokenPair.left);
|
if (E.isLeft(newTokenPair)) throwHTTPErr(newTokenPair.left);
|
||||||
authCookieHandler(res, newTokenPair.right, false, null);
|
authCookieHandler(res, newTokenPair.right, false, null, this.configService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -121,6 +121,7 @@ export class AuthController {
|
||||||
authTokens.right,
|
authTokens.right,
|
||||||
true,
|
true,
|
||||||
req.authInfo.state.redirect_uri,
|
req.authInfo.state.redirect_uri,
|
||||||
|
this.configService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,6 +148,7 @@ export class AuthController {
|
||||||
authTokens.right,
|
authTokens.right,
|
||||||
true,
|
true,
|
||||||
req.authInfo.state.redirect_uri,
|
req.authInfo.state.redirect_uri,
|
||||||
|
this.configService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,6 +175,7 @@ export class AuthController {
|
||||||
authTokens.right,
|
authTokens.right,
|
||||||
true,
|
true,
|
||||||
req.authInfo.state.redirect_uri,
|
req.authInfo.state.redirect_uri,
|
||||||
|
this.configService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { GoogleStrategy } from './strategies/google.strategy';
|
||||||
import { GithubStrategy } from './strategies/github.strategy';
|
import { GithubStrategy } from './strategies/github.strategy';
|
||||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||||
import { AuthProvider, authProviderCheck } from './helper';
|
import { AuthProvider, authProviderCheck } from './helper';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import {
|
import {
|
||||||
getConfiguredSSOProvidersFromInfraConfig,
|
getConfiguredSSOProvidersFromInfraConfig,
|
||||||
isInfraConfigTablePopulated,
|
isInfraConfigTablePopulated,
|
||||||
|
|
@ -22,15 +22,14 @@ import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
||||||
UserModule,
|
UserModule,
|
||||||
PassportModule,
|
PassportModule,
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
imports: [ConfigModule],
|
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: async (configService: ConfigService) => ({
|
useFactory: async (configService: ConfigService) => ({
|
||||||
secret: configService.get('JWT_SECRET'),
|
secret: configService.get('INFRA.JWT_SECRET'),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
InfraConfigModule,
|
InfraConfigModule,
|
||||||
],
|
],
|
||||||
providers: [AuthService, JwtStrategy, RTJwtStrategy],
|
providers: [AuthService],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
export class AuthModule {
|
export class AuthModule {
|
||||||
|
|
@ -57,7 +56,7 @@ export class AuthModule {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
module: AuthModule,
|
module: AuthModule,
|
||||||
providers,
|
providers: [...providers, JwtStrategy, RTJwtStrategy],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,11 +50,13 @@ export class AuthService {
|
||||||
*/
|
*/
|
||||||
private async generateMagicLinkTokens(user: AuthUser) {
|
private async generateMagicLinkTokens(user: AuthUser) {
|
||||||
const salt = await bcrypt.genSalt(
|
const salt = await bcrypt.genSalt(
|
||||||
parseInt(this.configService.get('TOKEN_SALT_COMPLEXITY')),
|
parseInt(this.configService.get('INFRA.TOKEN_SALT_COMPLEXITY')),
|
||||||
);
|
);
|
||||||
const expiresOn = DateTime.now()
|
const expiresOn = DateTime.now()
|
||||||
.plus({
|
.plus({
|
||||||
hours: parseInt(this.configService.get('MAGIC_LINK_TOKEN_VALIDITY')),
|
hours: parseInt(
|
||||||
|
this.configService.get('INFRA.MAGIC_LINK_TOKEN_VALIDITY'),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.toISO()
|
.toISO()
|
||||||
.toString();
|
.toString();
|
||||||
|
|
@ -106,7 +108,7 @@ export class AuthService {
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshToken = await this.jwtService.sign(refreshTokenPayload, {
|
const refreshToken = await this.jwtService.sign(refreshTokenPayload, {
|
||||||
expiresIn: this.configService.get('REFRESH_TOKEN_VALIDITY'), //7 Days
|
expiresIn: this.configService.get('INFRA.REFRESH_TOKEN_VALIDITY'), //7 Days
|
||||||
});
|
});
|
||||||
|
|
||||||
const refreshTokenHash = await argon2.hash(refreshToken);
|
const refreshTokenHash = await argon2.hash(refreshToken);
|
||||||
|
|
@ -142,7 +144,7 @@ export class AuthService {
|
||||||
|
|
||||||
return E.right(<AuthTokens>{
|
return E.right(<AuthTokens>{
|
||||||
access_token: await this.jwtService.sign(accessTokenPayload, {
|
access_token: await this.jwtService.sign(accessTokenPayload, {
|
||||||
expiresIn: this.configService.get('ACCESS_TOKEN_VALIDITY'), //1 Day
|
expiresIn: this.configService.get('INFRA.ACCESS_TOKEN_VALIDITY'), //1 Day
|
||||||
}),
|
}),
|
||||||
refresh_token: refreshToken.right,
|
refresh_token: refreshToken.right,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -41,30 +41,29 @@ export const authCookieHandler = (
|
||||||
authTokens: AuthTokens,
|
authTokens: AuthTokens,
|
||||||
redirect: boolean,
|
redirect: boolean,
|
||||||
redirectUrl: string | null,
|
redirectUrl: string | null,
|
||||||
|
configService: ConfigService,
|
||||||
) => {
|
) => {
|
||||||
const configService = new ConfigService();
|
|
||||||
|
|
||||||
const currentTime = DateTime.now();
|
const currentTime = DateTime.now();
|
||||||
const accessTokenValidity = currentTime
|
const accessTokenValidity = currentTime
|
||||||
.plus({
|
.plus({
|
||||||
milliseconds: parseInt(configService.get('ACCESS_TOKEN_VALIDITY')),
|
milliseconds: parseInt(configService.get('INFRA.ACCESS_TOKEN_VALIDITY')),
|
||||||
})
|
})
|
||||||
.toMillis();
|
.toMillis();
|
||||||
const refreshTokenValidity = currentTime
|
const refreshTokenValidity = currentTime
|
||||||
.plus({
|
.plus({
|
||||||
milliseconds: parseInt(configService.get('REFRESH_TOKEN_VALIDITY')),
|
milliseconds: parseInt(configService.get('INFRA.REFRESH_TOKEN_VALIDITY')),
|
||||||
})
|
})
|
||||||
.toMillis();
|
.toMillis();
|
||||||
|
|
||||||
res.cookie(AuthTokenType.ACCESS_TOKEN, authTokens.access_token, {
|
res.cookie(AuthTokenType.ACCESS_TOKEN, authTokens.access_token, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: configService.get('ALLOW_SECURE_COOKIES') === 'true',
|
secure: configService.get('INFRA.ALLOW_SECURE_COOKIES') === 'true',
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
maxAge: accessTokenValidity,
|
maxAge: accessTokenValidity,
|
||||||
});
|
});
|
||||||
res.cookie(AuthTokenType.REFRESH_TOKEN, authTokens.refresh_token, {
|
res.cookie(AuthTokenType.REFRESH_TOKEN, authTokens.refresh_token, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: configService.get('ALLOW_SECURE_COOKIES') === 'true',
|
secure: configService.get('INFRA.ALLOW_SECURE_COOKIES') === 'true',
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
maxAge: refreshTokenValidity,
|
maxAge: refreshTokenValidity,
|
||||||
});
|
});
|
||||||
|
|
@ -74,12 +73,11 @@ export const authCookieHandler = (
|
||||||
}
|
}
|
||||||
|
|
||||||
// check to see if redirectUrl is a whitelisted url
|
// check to see if redirectUrl is a whitelisted url
|
||||||
const whitelistedOrigins = configService
|
const whitelistedOrigins =
|
||||||
.get('WHITELISTED_ORIGINS')
|
configService.get('WHITELISTED_ORIGINS')?.split(',') ?? [];
|
||||||
.split(',');
|
|
||||||
if (!whitelistedOrigins.includes(redirectUrl))
|
if (!whitelistedOrigins.includes(redirectUrl))
|
||||||
// if it is not redirect by default to REDIRECT_URL
|
// if it is not redirect by default to App
|
||||||
redirectUrl = configService.get('REDIRECT_URL');
|
redirectUrl = configService.get('VITE_BASE_URL');
|
||||||
|
|
||||||
return res.status(HttpStatus.OK).redirect(redirectUrl);
|
return res.status(HttpStatus.OK).redirect(redirectUrl);
|
||||||
};
|
};
|
||||||
|
|
@ -121,11 +119,7 @@ export function authProviderCheck(
|
||||||
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
const envVariables = VITE_ALLOWED_AUTH_PROVIDERS
|
const envVariables = VITE_ALLOWED_AUTH_PROVIDERS?.split(',') ?? [];
|
||||||
? VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
|
||||||
provider.trim().toUpperCase(),
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!envVariables.includes(provider.toUpperCase())) return false;
|
if (!envVariables.includes(provider.toUpperCase())) return false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
secretOrKey: configService.get<string>('JWT_SECRET'),
|
secretOrKey: configService.get('INFRA.JWT_SECRET'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
||||||
return RTCookie;
|
return RTCookie;
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
secretOrKey: configService.get<string>('JWT_SECRET'),
|
secretOrKey: configService.get('INFRA.JWT_SECRET'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,6 @@ export const JSON_INVALID = 'json_invalid';
|
||||||
*/
|
*/
|
||||||
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
|
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
|
||||||
|
|
||||||
/**
|
|
||||||
* Auth Provider not specified
|
|
||||||
* (Auth)
|
|
||||||
*/
|
|
||||||
export const AUTH_PROVIDER_NOT_CONFIGURED =
|
|
||||||
'auth/provider_not_configured_correctly';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Email not provided by OAuth provider
|
* Email not provided by OAuth provider
|
||||||
* (SSO Strategies)
|
* (SSO Strategies)
|
||||||
|
|
@ -50,12 +43,6 @@ export const AUTH_PROVIDER_NOT_CONFIGURED =
|
||||||
export const AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH =
|
export const AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH =
|
||||||
'auth/email_not_provided_by_oauth';
|
'auth/email_not_provided_by_oauth';
|
||||||
|
|
||||||
/**
|
|
||||||
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file
|
|
||||||
*/
|
|
||||||
export const ENV_NOT_FOUND_KEY_AUTH_PROVIDERS =
|
|
||||||
'"VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Environment variable "DATA_ENCRYPTION_KEY" is not present in .env file
|
* Environment variable "DATA_ENCRYPTION_KEY" is not present in .env file
|
||||||
*/
|
*/
|
||||||
|
|
@ -68,18 +55,6 @@ export const ENV_NOT_FOUND_KEY_DATA_ENCRYPTION_KEY =
|
||||||
export const ENV_INVALID_DATA_ENCRYPTION_KEY =
|
export const ENV_INVALID_DATA_ENCRYPTION_KEY =
|
||||||
'"DATA_ENCRYPTION_KEY" value changed in .env file. Please undo the changes and restart the server';
|
'"DATA_ENCRYPTION_KEY" value changed in .env file. Please undo the changes and restart the server';
|
||||||
|
|
||||||
/**
|
|
||||||
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file
|
|
||||||
*/
|
|
||||||
export const ENV_EMPTY_AUTH_PROVIDERS =
|
|
||||||
'"VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file
|
|
||||||
*/
|
|
||||||
export const ENV_NOT_SUPPORT_AUTH_PROVIDERS =
|
|
||||||
'"VITE_ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tried to delete a user data document from fb firestore but failed.
|
* Tried to delete a user data document from fb firestore but failed.
|
||||||
* (FirebaseService)
|
* (FirebaseService)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Expose } from 'class-transformer';
|
||||||
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
|
|
||||||
|
export class GetOnboardingStatusResponse {
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
onboardingCompleted: boolean;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
canReRunOnboarding: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SaveOnboardingConfigRequest {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS]: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.GOOGLE_CLIENT_ID]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.GOOGLE_CLIENT_SECRET]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
[InfraConfigEnum.GOOGLE_CALLBACK_URL]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.GOOGLE_SCOPE]: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.GITHUB_CLIENT_ID]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.GITHUB_CLIENT_SECRET]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
[InfraConfigEnum.GITHUB_CALLBACK_URL]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.GITHUB_SCOPE]: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.MICROSOFT_CLIENT_ID]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.MICROSOFT_CLIENT_SECRET]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
[InfraConfigEnum.MICROSOFT_CALLBACK_URL]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.MICROSOFT_SCOPE]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.MICROSOFT_TENANT]: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_ENABLE]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.MAILER_ADDRESS_FROM]: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_URL]: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_HOST]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_PORT]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_SECURE]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_USER]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_PASSWORD]: string;
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
[InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SaveOnboardingConfigResponse {
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetOnboardingConfigResponse {
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS]: string;
|
||||||
|
|
||||||
|
@ApiProperty({ default: null })
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.GOOGLE_CLIENT_ID]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.GOOGLE_CLIENT_SECRET]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.GOOGLE_CALLBACK_URL]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.GOOGLE_SCOPE]: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.GITHUB_CLIENT_ID]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.GITHUB_CLIENT_SECRET]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.GITHUB_CALLBACK_URL]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.GITHUB_SCOPE]: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MICROSOFT_CLIENT_ID]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MICROSOFT_CLIENT_SECRET]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MICROSOFT_CALLBACK_URL]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MICROSOFT_SCOPE]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MICROSOFT_TENANT]: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_ENABLE]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MAILER_ADDRESS_FROM]: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_URL]: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_HOST]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_PORT]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_SECURE]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_USER]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MAILER_SMTP_PASSWORD]: string;
|
||||||
|
@ApiProperty()
|
||||||
|
@Expose()
|
||||||
|
[InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED]: string;
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
import {
|
import { ENV_INVALID_DATA_ENCRYPTION_KEY } from 'src/errors';
|
||||||
AUTH_PROVIDER_NOT_CONFIGURED,
|
|
||||||
ENV_INVALID_DATA_ENCRYPTION_KEY,
|
|
||||||
} from 'src/errors';
|
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { decrypt, encrypt, throwErr } from 'src/utils';
|
import { decrypt, encrypt } from 'src/utils';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { InfraConfig } from '@prisma/client';
|
|
||||||
|
|
||||||
export enum ServiceStatus {
|
export enum ServiceStatus {
|
||||||
ENABLE = 'ENABLE',
|
ENABLE = 'ENABLE',
|
||||||
|
|
@ -17,43 +13,52 @@ export enum ServiceStatus {
|
||||||
type DefaultInfraConfig = {
|
type DefaultInfraConfig = {
|
||||||
name: InfraConfigEnum;
|
name: InfraConfigEnum;
|
||||||
value: string;
|
value: string;
|
||||||
lastSyncedEnvFileValue: string;
|
|
||||||
isEncrypted: boolean;
|
isEncrypted: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AuthProviderConfigurations = {
|
/**
|
||||||
[AuthProvider.GOOGLE]: [
|
* Returns a mapping of authentication providers to their required configuration keys based on the current environment configuration.
|
||||||
InfraConfigEnum.GOOGLE_CLIENT_ID,
|
*/
|
||||||
InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
export function getAuthProviderRequiredKeys(
|
||||||
InfraConfigEnum.GOOGLE_CALLBACK_URL,
|
env: Record<string, any>,
|
||||||
InfraConfigEnum.GOOGLE_SCOPE,
|
): Record<AuthProvider, InfraConfigEnum[]> {
|
||||||
],
|
return {
|
||||||
[AuthProvider.GITHUB]: [
|
[AuthProvider.GOOGLE]: [
|
||||||
InfraConfigEnum.GITHUB_CLIENT_ID,
|
InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||||
InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
||||||
InfraConfigEnum.GITHUB_CALLBACK_URL,
|
InfraConfigEnum.GOOGLE_CALLBACK_URL,
|
||||||
InfraConfigEnum.GITHUB_SCOPE,
|
InfraConfigEnum.GOOGLE_SCOPE,
|
||||||
],
|
],
|
||||||
[AuthProvider.MICROSOFT]: [
|
[AuthProvider.GITHUB]: [
|
||||||
InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
InfraConfigEnum.GITHUB_CLIENT_ID,
|
||||||
InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
||||||
InfraConfigEnum.MICROSOFT_CALLBACK_URL,
|
InfraConfigEnum.GITHUB_CALLBACK_URL,
|
||||||
InfraConfigEnum.MICROSOFT_SCOPE,
|
InfraConfigEnum.GITHUB_SCOPE,
|
||||||
InfraConfigEnum.MICROSOFT_TENANT,
|
],
|
||||||
],
|
[AuthProvider.MICROSOFT]: [
|
||||||
[AuthProvider.EMAIL]:
|
InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
||||||
process.env.MAILER_USE_CUSTOM_CONFIGS === 'true'
|
InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
||||||
? [
|
InfraConfigEnum.MICROSOFT_CALLBACK_URL,
|
||||||
InfraConfigEnum.MAILER_SMTP_HOST,
|
InfraConfigEnum.MICROSOFT_SCOPE,
|
||||||
InfraConfigEnum.MAILER_SMTP_PORT,
|
InfraConfigEnum.MICROSOFT_TENANT,
|
||||||
InfraConfigEnum.MAILER_SMTP_SECURE,
|
],
|
||||||
InfraConfigEnum.MAILER_SMTP_USER,
|
[AuthProvider.EMAIL]:
|
||||||
InfraConfigEnum.MAILER_SMTP_PASSWORD,
|
env['INFRA'].MAILER_USE_CUSTOM_CONFIGS === 'true'
|
||||||
InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED,
|
? [
|
||||||
InfraConfigEnum.MAILER_ADDRESS_FROM,
|
InfraConfigEnum.MAILER_SMTP_HOST,
|
||||||
]
|
InfraConfigEnum.MAILER_SMTP_PORT,
|
||||||
: [InfraConfigEnum.MAILER_SMTP_URL, InfraConfigEnum.MAILER_ADDRESS_FROM],
|
InfraConfigEnum.MAILER_SMTP_SECURE,
|
||||||
};
|
InfraConfigEnum.MAILER_SMTP_USER,
|
||||||
|
InfraConfigEnum.MAILER_SMTP_PASSWORD,
|
||||||
|
InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED,
|
||||||
|
InfraConfigEnum.MAILER_ADDRESS_FROM,
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
InfraConfigEnum.MAILER_SMTP_URL,
|
||||||
|
InfraConfigEnum.MAILER_ADDRESS_FROM,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load environment variables from the database and set them in the process
|
* Load environment variables from the database and set them in the process
|
||||||
|
|
@ -64,10 +69,9 @@ const AuthProviderConfigurations = {
|
||||||
export async function loadInfraConfiguration() {
|
export async function loadInfraConfiguration() {
|
||||||
try {
|
try {
|
||||||
const prisma = new PrismaService();
|
const prisma = new PrismaService();
|
||||||
|
|
||||||
const infraConfigs = await prisma.infraConfig.findMany();
|
const infraConfigs = await prisma.infraConfig.findMany();
|
||||||
|
|
||||||
const environmentObject: Record<string, any> = {};
|
const environmentObject: Record<string, string> = {};
|
||||||
infraConfigs.forEach((infraConfig) => {
|
infraConfigs.forEach((infraConfig) => {
|
||||||
if (infraConfig.isEncrypted) {
|
if (infraConfig.isEncrypted) {
|
||||||
environmentObject[infraConfig.name] = decrypt(infraConfig.value);
|
environmentObject[infraConfig.name] = decrypt(infraConfig.value);
|
||||||
|
|
@ -83,6 +87,7 @@ export async function loadInfraConfiguration() {
|
||||||
|
|
||||||
// Prisma throw error if 'Can't reach at database server' OR 'Table does not exist'
|
// Prisma throw error if 'Can't reach at database server' OR 'Table does not exist'
|
||||||
// Reason for not throwing error is, we want successful build during 'postinstall' and generate dist files
|
// Reason for not throwing error is, we want successful build during 'postinstall' and generate dist files
|
||||||
|
console.log('Error from loadInfraConfiguration', error);
|
||||||
return { INFRA: {} };
|
return { INFRA: {} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,176 +100,206 @@ export async function getDefaultInfraConfigs(): Promise<DefaultInfraConfig[]> {
|
||||||
const prisma = new PrismaService();
|
const prisma = new PrismaService();
|
||||||
|
|
||||||
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
||||||
const configuredSSOProviders = getConfiguredSSOProvidersFromEnvFile();
|
const onboardingCompleteStatus = await isOnboardingCompleted();
|
||||||
const generatedAnalyticsUserId = generateAnalyticsUserId();
|
const generatedAnalyticsUserId = generateAnalyticsUserId();
|
||||||
|
const isSecureCookies = determineAllowSecureCookies(
|
||||||
|
process.env.VITE_BASE_URL,
|
||||||
|
);
|
||||||
|
|
||||||
const infraConfigDefaultObjs: DefaultInfraConfig[] = [
|
const infraConfigDefaultObjs: DefaultInfraConfig[] = [
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.ONBOARDING_COMPLETED,
|
||||||
|
value: onboardingCompleteStatus.toString(),
|
||||||
|
isEncrypted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.ONBOARDING_RECOVERY_TOKEN,
|
||||||
|
value: null,
|
||||||
|
isEncrypted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.JWT_SECRET,
|
||||||
|
value: encrypt(randomBytes(32).toString('hex')),
|
||||||
|
isEncrypted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.SESSION_SECRET,
|
||||||
|
value: encrypt(randomBytes(32).toString('hex')),
|
||||||
|
isEncrypted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.TOKEN_SALT_COMPLEXITY,
|
||||||
|
value: '10',
|
||||||
|
isEncrypted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAGIC_LINK_TOKEN_VALIDITY,
|
||||||
|
value: '24', // 24 hours
|
||||||
|
isEncrypted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.REFRESH_TOKEN_VALIDITY,
|
||||||
|
value: '604800000', // 7 days in milliseconds
|
||||||
|
isEncrypted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.ACCESS_TOKEN_VALIDITY,
|
||||||
|
value: '86400000', // 1 day in milliseconds
|
||||||
|
isEncrypted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.ALLOW_SECURE_COOKIES,
|
||||||
|
value: isSecureCookies.toString(),
|
||||||
|
isEncrypted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.RATE_LIMIT_TTL,
|
||||||
|
value: '10000', // in milliseconds (10 seconds)
|
||||||
|
isEncrypted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.RATE_LIMIT_MAX,
|
||||||
|
value: '100', // 100 requests per IP per RATE_LIMIT_TTL
|
||||||
|
isEncrypted: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MAILER_SMTP_ENABLE,
|
name: InfraConfigEnum.MAILER_SMTP_ENABLE,
|
||||||
value: process.env.MAILER_SMTP_ENABLE ?? 'true',
|
value: 'false',
|
||||||
lastSyncedEnvFileValue: process.env.MAILER_SMTP_ENABLE ?? 'true',
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS,
|
name: InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS,
|
||||||
value: process.env.MAILER_USE_CUSTOM_CONFIGS ?? 'false',
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.MAILER_USE_CUSTOM_CONFIGS ?? 'false',
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MAILER_SMTP_URL,
|
name: InfraConfigEnum.MAILER_SMTP_URL,
|
||||||
value: encrypt(process.env.MAILER_SMTP_URL),
|
value: null,
|
||||||
lastSyncedEnvFileValue: encrypt(process.env.MAILER_SMTP_URL),
|
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
|
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
|
||||||
value: process.env.MAILER_ADDRESS_FROM,
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.MAILER_ADDRESS_FROM,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MAILER_SMTP_HOST,
|
name: InfraConfigEnum.MAILER_SMTP_HOST,
|
||||||
value: process.env.MAILER_SMTP_HOST,
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.MAILER_SMTP_HOST,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MAILER_SMTP_PORT,
|
name: InfraConfigEnum.MAILER_SMTP_PORT,
|
||||||
value: process.env.MAILER_SMTP_PORT,
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.MAILER_SMTP_PORT,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MAILER_SMTP_SECURE,
|
name: InfraConfigEnum.MAILER_SMTP_SECURE,
|
||||||
value: process.env.MAILER_SMTP_SECURE,
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.MAILER_SMTP_SECURE,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MAILER_SMTP_USER,
|
name: InfraConfigEnum.MAILER_SMTP_USER,
|
||||||
value: process.env.MAILER_SMTP_USER,
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.MAILER_SMTP_USER,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MAILER_SMTP_PASSWORD,
|
name: InfraConfigEnum.MAILER_SMTP_PASSWORD,
|
||||||
value: encrypt(process.env.MAILER_SMTP_PASSWORD),
|
value: null,
|
||||||
lastSyncedEnvFileValue: encrypt(process.env.MAILER_SMTP_PASSWORD),
|
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED,
|
name: InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED,
|
||||||
value: process.env.MAILER_TLS_REJECT_UNAUTHORIZED,
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.MAILER_TLS_REJECT_UNAUTHORIZED,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||||
value: encrypt(process.env.GOOGLE_CLIENT_ID),
|
value: null,
|
||||||
lastSyncedEnvFileValue: encrypt(process.env.GOOGLE_CLIENT_ID),
|
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
||||||
value: encrypt(process.env.GOOGLE_CLIENT_SECRET),
|
value: null,
|
||||||
lastSyncedEnvFileValue: encrypt(process.env.GOOGLE_CLIENT_SECRET),
|
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.GOOGLE_CALLBACK_URL,
|
name: InfraConfigEnum.GOOGLE_CALLBACK_URL,
|
||||||
value: process.env.GOOGLE_CALLBACK_URL,
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.GOOGLE_CALLBACK_URL,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.GOOGLE_SCOPE,
|
name: InfraConfigEnum.GOOGLE_SCOPE,
|
||||||
value: process.env.GOOGLE_SCOPE,
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.GOOGLE_SCOPE,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.GITHUB_CLIENT_ID,
|
name: InfraConfigEnum.GITHUB_CLIENT_ID,
|
||||||
value: encrypt(process.env.GITHUB_CLIENT_ID),
|
value: null,
|
||||||
lastSyncedEnvFileValue: encrypt(process.env.GITHUB_CLIENT_ID),
|
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
||||||
value: encrypt(process.env.GITHUB_CLIENT_SECRET),
|
value: null,
|
||||||
lastSyncedEnvFileValue: encrypt(process.env.GITHUB_CLIENT_SECRET),
|
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.GITHUB_CALLBACK_URL,
|
name: InfraConfigEnum.GITHUB_CALLBACK_URL,
|
||||||
value: process.env.GITHUB_CALLBACK_URL,
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.GITHUB_CALLBACK_URL,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.GITHUB_SCOPE,
|
name: InfraConfigEnum.GITHUB_SCOPE,
|
||||||
value: process.env.GITHUB_SCOPE,
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.GITHUB_SCOPE,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
||||||
value: encrypt(process.env.MICROSOFT_CLIENT_ID),
|
value: null,
|
||||||
lastSyncedEnvFileValue: encrypt(process.env.MICROSOFT_CLIENT_ID),
|
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
||||||
value: encrypt(process.env.MICROSOFT_CLIENT_SECRET),
|
value: null,
|
||||||
lastSyncedEnvFileValue: encrypt(process.env.MICROSOFT_CLIENT_SECRET),
|
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MICROSOFT_CALLBACK_URL,
|
name: InfraConfigEnum.MICROSOFT_CALLBACK_URL,
|
||||||
value: process.env.MICROSOFT_CALLBACK_URL,
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.MICROSOFT_CALLBACK_URL,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MICROSOFT_SCOPE,
|
name: InfraConfigEnum.MICROSOFT_SCOPE,
|
||||||
value: process.env.MICROSOFT_SCOPE,
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.MICROSOFT_SCOPE,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MICROSOFT_TENANT,
|
name: InfraConfigEnum.MICROSOFT_TENANT,
|
||||||
value: process.env.MICROSOFT_TENANT,
|
value: null,
|
||||||
lastSyncedEnvFileValue: process.env.MICROSOFT_TENANT,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
value: configuredSSOProviders,
|
value: null,
|
||||||
lastSyncedEnvFileValue: configuredSSOProviders,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||||
value: false.toString(),
|
value: false.toString(),
|
||||||
lastSyncedEnvFileValue: null,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.ANALYTICS_USER_ID,
|
name: InfraConfigEnum.ANALYTICS_USER_ID,
|
||||||
value: generatedAnalyticsUserId,
|
value: generatedAnalyticsUserId,
|
||||||
lastSyncedEnvFileValue: null,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
value: (await prisma.infraConfig.count()) === 0 ? 'true' : 'false',
|
value: (await prisma.infraConfig.count()) === 0 ? 'true' : 'false',
|
||||||
lastSyncedEnvFileValue: null,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.USER_HISTORY_STORE_ENABLED,
|
name: InfraConfigEnum.USER_HISTORY_STORE_ENABLED,
|
||||||
value: 'true',
|
value: 'true',
|
||||||
lastSyncedEnvFileValue: null,
|
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -311,48 +346,6 @@ export async function getEncryptionRequiredInfraConfigEntries(
|
||||||
return requiredEncryption;
|
return requiredEncryption;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync the 'infra_config' table with .env file
|
|
||||||
* @returns Array of InfraConfig
|
|
||||||
*/
|
|
||||||
export async function syncInfraConfigWithEnvFile() {
|
|
||||||
const prisma = new PrismaService();
|
|
||||||
const dbInfraConfigs = await prisma.infraConfig.findMany();
|
|
||||||
|
|
||||||
const updateRequiredObjs: (Partial<InfraConfig> & { id: string })[] = [];
|
|
||||||
|
|
||||||
for (const dbConfig of dbInfraConfigs) {
|
|
||||||
const envValue = process.env[dbConfig.name];
|
|
||||||
|
|
||||||
// lastSyncedEnvFileValue null check for backward compatibility from 2024.10.2 and below
|
|
||||||
if (!dbConfig.lastSyncedEnvFileValue && envValue) {
|
|
||||||
const configValue = dbConfig.isEncrypted ? encrypt(envValue) : envValue;
|
|
||||||
updateRequiredObjs.push({
|
|
||||||
id: dbConfig.id,
|
|
||||||
value: dbConfig.value === null ? configValue : undefined,
|
|
||||||
lastSyncedEnvFileValue: configValue,
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the value in the database is different from the value in the .env file, means the value in the .env file has been updated
|
|
||||||
const rawLastSyncedEnvFileValue = dbConfig.isEncrypted
|
|
||||||
? decrypt(dbConfig.lastSyncedEnvFileValue)
|
|
||||||
: dbConfig.lastSyncedEnvFileValue;
|
|
||||||
|
|
||||||
if (rawLastSyncedEnvFileValue != envValue) {
|
|
||||||
const configValue = dbConfig.isEncrypted ? encrypt(envValue) : envValue;
|
|
||||||
updateRequiredObjs.push({
|
|
||||||
id: dbConfig.id,
|
|
||||||
value: configValue ?? null,
|
|
||||||
lastSyncedEnvFileValue: configValue ?? null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateRequiredObjs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify if 'infra_config' table is loaded with all entries
|
* Verify if 'infra_config' table is loaded with all entries
|
||||||
* @returns boolean
|
* @returns boolean
|
||||||
|
|
@ -389,72 +382,31 @@ export function stopApp() {
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the configured SSO providers from .env file
|
|
||||||
* @description This function verify if the required parameters for each SSO provider are configured in .env file. Usage on first time setup and reset.
|
|
||||||
* @returns Array of configured SSO providers
|
|
||||||
*/
|
|
||||||
export function getConfiguredSSOProvidersFromEnvFile() {
|
|
||||||
const allowedAuthProviders: string[] =
|
|
||||||
process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',');
|
|
||||||
const configuredAuthProviders: string[] = [];
|
|
||||||
|
|
||||||
const addProviderIfConfigured = (provider) => {
|
|
||||||
const configParameters: string[] = AuthProviderConfigurations[provider];
|
|
||||||
|
|
||||||
const isConfigured = configParameters.every((configParameter) => {
|
|
||||||
return process.env[configParameter];
|
|
||||||
});
|
|
||||||
if (isConfigured) configuredAuthProviders.push(provider);
|
|
||||||
};
|
|
||||||
|
|
||||||
allowedAuthProviders.forEach((provider) => addProviderIfConfigured(provider));
|
|
||||||
|
|
||||||
if (configuredAuthProviders.length === 0) {
|
|
||||||
throwErr(AUTH_PROVIDER_NOT_CONFIGURED);
|
|
||||||
} else if (allowedAuthProviders.length !== configuredAuthProviders.length) {
|
|
||||||
const unConfiguredAuthProviders = allowedAuthProviders.filter(
|
|
||||||
(provider) => {
|
|
||||||
return !configuredAuthProviders.includes(provider);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${unConfiguredAuthProviders.join(
|
|
||||||
',',
|
|
||||||
)} SSO auth provider(s) are not configured properly in .env file. Do configure them from Admin Dashboard.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return configuredAuthProviders.join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the configured SSO providers from 'infra_config' table.
|
* Get the configured SSO providers from 'infra_config' table.
|
||||||
* @description Usage every time the app starts by AuthModule to initiate Strategies.
|
* @description Usage every time the app starts by AuthModule to initiate Strategies.
|
||||||
* @returns Array of configured SSO providers
|
* @returns Array of configured SSO providers
|
||||||
*/
|
*/
|
||||||
export async function getConfiguredSSOProvidersFromInfraConfig() {
|
export async function getConfiguredSSOProvidersFromInfraConfig() {
|
||||||
|
const prisma = new PrismaService();
|
||||||
const env = await loadInfraConfiguration();
|
const env = await loadInfraConfiguration();
|
||||||
|
const providerConfigKeys = getAuthProviderRequiredKeys(env);
|
||||||
|
|
||||||
const allowedAuthProviders: string[] =
|
const allowedAuthProviders: string[] =
|
||||||
env['INFRA'].VITE_ALLOWED_AUTH_PROVIDERS.split(',');
|
env['INFRA'].VITE_ALLOWED_AUTH_PROVIDERS?.split(',') ?? [];
|
||||||
const configuredAuthProviders: string[] = [];
|
|
||||||
|
|
||||||
const addProviderIfConfigured = (provider) => {
|
const configuredAuthProviders = allowedAuthProviders.filter((provider) => {
|
||||||
const configParameters: string[] = AuthProviderConfigurations[provider];
|
const requiredKeys = providerConfigKeys[provider];
|
||||||
|
return requiredKeys?.every((key) => env['INFRA'][key]);
|
||||||
const isConfigured = configParameters.every((configParameter) => {
|
});
|
||||||
return env['INFRA'][configParameter];
|
|
||||||
});
|
|
||||||
if (isConfigured) configuredAuthProviders.push(provider);
|
|
||||||
};
|
|
||||||
|
|
||||||
allowedAuthProviders.forEach((provider) => addProviderIfConfigured(provider));
|
|
||||||
|
|
||||||
if (configuredAuthProviders.length === 0) {
|
if (configuredAuthProviders.length === 0) {
|
||||||
|
await prisma.infraConfig.update({
|
||||||
|
where: { name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS },
|
||||||
|
data: { value: null },
|
||||||
|
});
|
||||||
return '';
|
return '';
|
||||||
} else if (allowedAuthProviders.length !== configuredAuthProviders.length) {
|
} else if (allowedAuthProviders.length !== configuredAuthProviders.length) {
|
||||||
const prisma = new PrismaService();
|
|
||||||
await prisma.infraConfig.update({
|
await prisma.infraConfig.update({
|
||||||
where: { name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS },
|
where: { name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS },
|
||||||
data: { value: configuredAuthProviders.join(',') },
|
data: { value: configuredAuthProviders.join(',') },
|
||||||
|
|
@ -468,6 +420,24 @@ export async function getConfiguredSSOProvidersFromInfraConfig() {
|
||||||
return configuredAuthProviders.join(',');
|
return configuredAuthProviders.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the onboarding is completed by verifying if the allowed auth providers are configured
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
export async function isOnboardingCompleted(): Promise<boolean> {
|
||||||
|
const prisma = new PrismaService();
|
||||||
|
const allowedProviders = await prisma.infraConfig.findUnique({
|
||||||
|
where: { name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS },
|
||||||
|
select: { value: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!allowedProviders?.value || allowedProviders.value === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a hashed valued for analytics
|
* Generate a hashed valued for analytics
|
||||||
* @returns Generated hashed value
|
* @returns Generated hashed value
|
||||||
|
|
@ -476,3 +446,58 @@ export function generateAnalyticsUserId() {
|
||||||
const hashedUserID = randomBytes(20).toString('hex');
|
const hashedUserID = randomBytes(20).toString('hex');
|
||||||
return hashedUserID;
|
return hashedUserID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if ALLOW_SECURE_COOKIES should be true or false
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
export function determineAllowSecureCookies(appBaseUrl: string) {
|
||||||
|
return appBaseUrl.startsWith('https');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a map of environment variables that are derived from other configuration values
|
||||||
|
* @returns Record<string, string>
|
||||||
|
*/
|
||||||
|
export async function buildDerivedEnv() {
|
||||||
|
const envConfigMap = await loadInfraConfiguration();
|
||||||
|
const derivedEnv: Record<string, string> = {};
|
||||||
|
|
||||||
|
// Normalize URLs
|
||||||
|
const baseUrl = process.env.VITE_BASE_URL || '';
|
||||||
|
const backendUrl = process.env.VITE_BACKEND_API_URL || '';
|
||||||
|
const normalizedBackendUrl = backendUrl?.replace(/\/+$/, ''); // remove trailing slash
|
||||||
|
|
||||||
|
// Set ALLOW_SECURE_COOKIES based on base URL protocol
|
||||||
|
const expectedSecure = determineAllowSecureCookies(baseUrl).toString();
|
||||||
|
const currentSecure = envConfigMap.INFRA.ALLOW_SECURE_COOKIES;
|
||||||
|
if (currentSecure !== expectedSecure) {
|
||||||
|
derivedEnv.ALLOW_SECURE_COOKIES = expectedSecure;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set GOOGLE_CALLBACK_URL, MICROSOFT_CALLBACK_URL, and GITHUB_CALLBACK_URL based on backend URL (self healing) if user changed the backend URL
|
||||||
|
// Callback URL definitions
|
||||||
|
const callbackConfigs = [
|
||||||
|
{ key: InfraConfigEnum.GOOGLE_CALLBACK_URL, path: '/auth/google/callback' },
|
||||||
|
{
|
||||||
|
key: InfraConfigEnum.MICROSOFT_CALLBACK_URL,
|
||||||
|
path: '/auth/microsoft/callback',
|
||||||
|
},
|
||||||
|
{ key: InfraConfigEnum.GITHUB_CALLBACK_URL, path: '/auth/github/callback' },
|
||||||
|
];
|
||||||
|
// Update callback URLs if they don't match the backend
|
||||||
|
for (const { key, path } of callbackConfigs) {
|
||||||
|
const currentCallback = envConfigMap.INFRA[key];
|
||||||
|
const expectedCallback = `${normalizedBackendUrl}${path}`;
|
||||||
|
|
||||||
|
if (
|
||||||
|
backendUrl.length > 0 &&
|
||||||
|
currentCallback &&
|
||||||
|
currentCallback !== expectedCallback
|
||||||
|
) {
|
||||||
|
derivedEnv[key] = expectedCallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return derivedEnv;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,13 @@ import { Module } from '@nestjs/common';
|
||||||
import { InfraConfigService } from './infra-config.service';
|
import { InfraConfigService } from './infra-config.service';
|
||||||
import { SiteController } from './infra-config.controller';
|
import { SiteController } from './infra-config.controller';
|
||||||
import { InfraConfigResolver } from './infra-config.resolver';
|
import { InfraConfigResolver } from './infra-config.resolver';
|
||||||
|
import { UserModule } from 'src/user/user.module';
|
||||||
|
import { OnboardingController } from './onboarding.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [UserModule],
|
||||||
|
controllers: [SiteController, OnboardingController],
|
||||||
providers: [InfraConfigResolver, InfraConfigService],
|
providers: [InfraConfigResolver, InfraConfigService],
|
||||||
exports: [InfraConfigService],
|
exports: [InfraConfigService],
|
||||||
controllers: [SiteController],
|
|
||||||
})
|
})
|
||||||
export class InfraConfigModule {}
|
export class InfraConfigModule {}
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,18 @@ import { InfraConfig } from './infra-config.model';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { ServiceStatus } from './helper';
|
import { ServiceStatus } from './helper';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { UserService } from 'src/user/user.service';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockConfigService = mockDeep<ConfigService>();
|
const mockConfigService = mockDeep<ConfigService>();
|
||||||
const mockPubsub = mockDeep<PubSubService>();
|
const mockPubsub = mockDeep<PubSubService>();
|
||||||
|
const mockUserService = mockDeep<UserService>();
|
||||||
|
|
||||||
const infraConfigService = new InfraConfigService(
|
const infraConfigService = new InfraConfigService(
|
||||||
mockPrisma,
|
mockPrisma,
|
||||||
mockConfigService,
|
mockConfigService,
|
||||||
mockPubsub,
|
mockPubsub,
|
||||||
|
mockUserService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const INITIALIZED_DATE_CONST = new Date();
|
const INITIALIZED_DATE_CONST = new Date();
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,23 @@ import {
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import {
|
import {
|
||||||
ServiceStatus,
|
ServiceStatus,
|
||||||
|
buildDerivedEnv,
|
||||||
getDefaultInfraConfigs,
|
getDefaultInfraConfigs,
|
||||||
getEncryptionRequiredInfraConfigEntries,
|
getEncryptionRequiredInfraConfigEntries,
|
||||||
getMissingInfraConfigEntries,
|
getMissingInfraConfigEntries,
|
||||||
stopApp,
|
stopApp,
|
||||||
syncInfraConfigWithEnvFile,
|
|
||||||
} from './helper';
|
} from './helper';
|
||||||
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
|
import { UserService } from 'src/user/user.service';
|
||||||
|
import {
|
||||||
|
GetOnboardingConfigResponse,
|
||||||
|
GetOnboardingStatusResponse,
|
||||||
|
SaveOnboardingConfigRequest,
|
||||||
|
SaveOnboardingConfigResponse,
|
||||||
|
} from './dto/onboarding.dto';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InfraConfigService implements OnModuleInit {
|
export class InfraConfigService implements OnModuleInit {
|
||||||
|
|
@ -41,6 +49,7 @@ export class InfraConfigService implements OnModuleInit {
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
|
private readonly userService: UserService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// Following fields are not updatable by `infraConfigs` Mutation. Use dedicated mutations for these fields instead.
|
// Following fields are not updatable by `infraConfigs` Mutation. Use dedicated mutations for these fields instead.
|
||||||
|
|
@ -94,14 +103,14 @@ export class InfraConfigService implements OnModuleInit {
|
||||||
await Promise.allSettled(dbOperations);
|
await Promise.allSettled(dbOperations);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync the InfraConfigs with the .env file, if .env file updates later on
|
// Derive env variables programmatically if they don't exist or need to be updated
|
||||||
const envFileChangesRequired = await syncInfraConfigWithEnvFile();
|
const derivedEnv = await buildDerivedEnv();
|
||||||
if (envFileChangesRequired.length > 0) {
|
|
||||||
const dbOperations = envFileChangesRequired.map((dbConfig) => {
|
if (Object.keys(derivedEnv).length > 0) {
|
||||||
const { id, ...dataObj } = dbConfig;
|
const dbOperations = Object.entries(derivedEnv).map(([name, value]) => {
|
||||||
return this.prisma.infraConfig.update({
|
return this.prisma.infraConfig.update({
|
||||||
where: { id: dbConfig.id },
|
where: { name: name as InfraConfigEnum },
|
||||||
data: dataObj,
|
data: { value },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await Promise.allSettled(dbOperations);
|
await Promise.allSettled(dbOperations);
|
||||||
|
|
@ -111,7 +120,7 @@ export class InfraConfigService implements OnModuleInit {
|
||||||
if (
|
if (
|
||||||
propsToInsert.length > 0 ||
|
propsToInsert.length > 0 ||
|
||||||
encryptionRequiredEntries.length > 0 ||
|
encryptionRequiredEntries.length > 0 ||
|
||||||
envFileChangesRequired.length > 0
|
Object.keys(derivedEnv).length > 0
|
||||||
) {
|
) {
|
||||||
stopApp();
|
stopApp();
|
||||||
}
|
}
|
||||||
|
|
@ -210,10 +219,16 @@ export class InfraConfigService implements OnModuleInit {
|
||||||
* @param infraConfigs InfraConfigs to update
|
* @param infraConfigs InfraConfigs to update
|
||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async updateMany(infraConfigs: InfraConfigArgs[]) {
|
async updateMany(
|
||||||
for (let i = 0; i < infraConfigs.length; i++) {
|
infraConfigs: InfraConfigArgs[],
|
||||||
if (this.EXCLUDE_FROM_UPDATE_CONFIGS.includes(infraConfigs[i].name))
|
checkDisallowedKeys: boolean = true,
|
||||||
return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
) {
|
||||||
|
if (checkDisallowedKeys) {
|
||||||
|
// Check if the names are allowed to update by client
|
||||||
|
for (let i = 0; i < infraConfigs.length; i++) {
|
||||||
|
if (this.EXCLUDE_FROM_UPDATE_CONFIGS.includes(infraConfigs[i].name))
|
||||||
|
return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidate = this.validateEnvValues(infraConfigs);
|
const isValidate = this.validateEnvValues(infraConfigs);
|
||||||
|
|
@ -374,7 +389,7 @@ export class InfraConfigService implements OnModuleInit {
|
||||||
const infra = await this.get(InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS);
|
const infra = await this.get(InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS);
|
||||||
if (E.isLeft(infra)) return E.left(infra.left);
|
if (E.isLeft(infra)) return E.left(infra.left);
|
||||||
|
|
||||||
const allowedAuthProviders = infra.right.value.split(',');
|
const allowedAuthProviders = infra.right.value?.split(',') ?? [];
|
||||||
let updatedAuthProviders = allowedAuthProviders;
|
let updatedAuthProviders = allowedAuthProviders;
|
||||||
|
|
||||||
const infraConfigMap = await this.getInfraConfigsMap();
|
const infraConfigMap = await this.getInfraConfigsMap();
|
||||||
|
|
@ -457,9 +472,11 @@ export class InfraConfigService implements OnModuleInit {
|
||||||
* @returns string[]
|
* @returns string[]
|
||||||
*/
|
*/
|
||||||
getAllowedAuthProviders() {
|
getAllowedAuthProviders() {
|
||||||
return this.configService
|
return (
|
||||||
.get<string>('INFRA.VITE_ALLOWED_AUTH_PROVIDERS')
|
this.configService
|
||||||
.split(',');
|
.get<string>('INFRA.VITE_ALLOWED_AUTH_PROVIDERS')
|
||||||
|
?.split(',') ?? []
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -485,6 +502,107 @@ export class InfraConfigService implements OnModuleInit {
|
||||||
return E.right(infraConfig.right);
|
return E.right(infraConfig.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get onboarding status
|
||||||
|
* @returns GetOnboardingStatusResponse
|
||||||
|
*/
|
||||||
|
async getOnboardingStatus() {
|
||||||
|
const configMap = await this.getInfraConfigsMap();
|
||||||
|
const usersCount = await this.userService.getUsersCount();
|
||||||
|
|
||||||
|
return E.right({
|
||||||
|
onboardingCompleted: configMap.ONBOARDING_COMPLETED === 'true',
|
||||||
|
canReRunOnboarding: usersCount === 0,
|
||||||
|
} as GetOnboardingStatusResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the onboarding configuration
|
||||||
|
* @param dto SaveOnboardingConfigRequest
|
||||||
|
*/
|
||||||
|
async updateOnboardingConfig(dto: SaveOnboardingConfigRequest) {
|
||||||
|
const onboardingRecoveryToken = crypto.randomUUID();
|
||||||
|
|
||||||
|
const configEntries: InfraConfigArgs[] = [
|
||||||
|
...Object.entries(dto).map(([key, value]) => ({
|
||||||
|
name: key as InfraConfigEnum,
|
||||||
|
value,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.ONBOARDING_COMPLETED,
|
||||||
|
value: 'true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.ONBOARDING_RECOVERY_TOKEN,
|
||||||
|
value: onboardingRecoveryToken,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const isValidated = this.validateEnvValues(configEntries);
|
||||||
|
if (E.isLeft(isValidated)) return E.left(isValidated.left);
|
||||||
|
|
||||||
|
// Verify MAILER_SMTP_ENABLE
|
||||||
|
if (
|
||||||
|
dto[InfraConfigEnum.MAILER_SMTP_ENABLE] === 'true' &&
|
||||||
|
!this.isServiceConfigured(
|
||||||
|
AuthProvider.EMAIL,
|
||||||
|
dto as unknown as Record<string, string>,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return E.left(INFRA_CONFIG_SERVICE_NOT_CONFIGURED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify VITE_ALLOWED_AUTH_PROVIDERS
|
||||||
|
const allowedAuthProviders =
|
||||||
|
dto[InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS].split(',');
|
||||||
|
|
||||||
|
if (allowedAuthProviders.length === 0) {
|
||||||
|
return E.left(AUTH_PROVIDER_NOT_SPECIFIED);
|
||||||
|
}
|
||||||
|
for (const provider of allowedAuthProviders) {
|
||||||
|
if (
|
||||||
|
!Object.values(AuthProvider).includes(provider as AuthProvider) ||
|
||||||
|
!this.isServiceConfigured(
|
||||||
|
provider as AuthProvider,
|
||||||
|
dto as unknown as Record<string, string>,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return E.left(INFRA_CONFIG_SERVICE_NOT_CONFIGURED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move forward with updating the InfraConfigs
|
||||||
|
const isUpdated = await this.updateMany(configEntries, false);
|
||||||
|
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||||
|
|
||||||
|
return E.right({
|
||||||
|
token: onboardingRecoveryToken,
|
||||||
|
} as SaveOnboardingConfigResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get onboarding configuration
|
||||||
|
* @param token Onboarding recovery token
|
||||||
|
* @returns GetOnboardingConfigResponse
|
||||||
|
*/
|
||||||
|
async getOnboardingConfig(token: string) {
|
||||||
|
const configs = await this.getMany(Object.values(InfraConfigEnum), false);
|
||||||
|
if (E.isLeft(configs)) return E.left(configs.left);
|
||||||
|
|
||||||
|
// Check if the onboarding recovery token is valid
|
||||||
|
const recoveryToken = configs.right.find(
|
||||||
|
(config) => config.name === InfraConfigEnum.ONBOARDING_RECOVERY_TOKEN,
|
||||||
|
)?.value;
|
||||||
|
const tokenIsValid = token === recoveryToken;
|
||||||
|
|
||||||
|
const onboardingConfig = configs.right.reduce((acc, config) => {
|
||||||
|
acc[config.name] = tokenIsValid ? config.value : null;
|
||||||
|
return acc;
|
||||||
|
}, {} as GetOnboardingConfigResponse);
|
||||||
|
|
||||||
|
return E.right(onboardingConfig);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset all the InfraConfigs to their default values (from .env)
|
* Reset all the InfraConfigs to their default values (from .env)
|
||||||
*/
|
*/
|
||||||
|
|
@ -530,98 +648,75 @@ export class InfraConfigService implements OnModuleInit {
|
||||||
value: string;
|
value: string;
|
||||||
}[],
|
}[],
|
||||||
) {
|
) {
|
||||||
for (let i = 0; i < infraConfigs.length; i++) {
|
for (const config of infraConfigs) {
|
||||||
switch (infraConfigs[i].name) {
|
const { name, value } = config;
|
||||||
|
|
||||||
|
const fail = () => {
|
||||||
|
console.error(`[Infra Validation Failed] Key: ${name}`);
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
case InfraConfigEnum.MAILER_SMTP_ENABLE:
|
case InfraConfigEnum.MAILER_SMTP_ENABLE:
|
||||||
if (
|
|
||||||
infraConfigs[i].value !== 'true' &&
|
|
||||||
infraConfigs[i].value !== 'false'
|
|
||||||
)
|
|
||||||
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS:
|
case InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS:
|
||||||
if (
|
|
||||||
infraConfigs[i].value !== 'true' &&
|
|
||||||
infraConfigs[i].value !== 'false'
|
|
||||||
)
|
|
||||||
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MAILER_SMTP_URL:
|
|
||||||
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
|
||||||
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MAILER_ADDRESS_FROM:
|
|
||||||
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
|
|
||||||
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MAILER_SMTP_HOST:
|
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MAILER_SMTP_PORT:
|
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MAILER_SMTP_SECURE:
|
case InfraConfigEnum.MAILER_SMTP_SECURE:
|
||||||
if (
|
|
||||||
infraConfigs[i].value !== 'true' &&
|
|
||||||
infraConfigs[i].value !== 'false'
|
|
||||||
)
|
|
||||||
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MAILER_SMTP_USER:
|
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MAILER_SMTP_PASSWORD:
|
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED:
|
case InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED:
|
||||||
if (
|
if (value !== 'true' && value !== 'false') return fail();
|
||||||
infraConfigs[i].value !== 'true' &&
|
|
||||||
infraConfigs[i].value !== 'false'
|
|
||||||
)
|
|
||||||
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case InfraConfigEnum.MAILER_SMTP_URL:
|
||||||
|
if (!validateSMTPUrl(value)) return fail();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InfraConfigEnum.MAILER_ADDRESS_FROM:
|
||||||
|
if (!validateSMTPEmail(value)) return fail();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InfraConfigEnum.MAILER_SMTP_HOST:
|
||||||
|
case InfraConfigEnum.MAILER_SMTP_PORT:
|
||||||
|
case InfraConfigEnum.MAILER_SMTP_USER:
|
||||||
|
case InfraConfigEnum.MAILER_SMTP_PASSWORD:
|
||||||
case InfraConfigEnum.GOOGLE_CLIENT_ID:
|
case InfraConfigEnum.GOOGLE_CLIENT_ID:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.GOOGLE_CLIENT_SECRET:
|
case InfraConfigEnum.GOOGLE_CLIENT_SECRET:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.GOOGLE_CALLBACK_URL:
|
|
||||||
if (!validateUrl(infraConfigs[i].value))
|
|
||||||
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.GOOGLE_SCOPE:
|
case InfraConfigEnum.GOOGLE_SCOPE:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.GITHUB_CLIENT_ID:
|
case InfraConfigEnum.GITHUB_CLIENT_ID:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.GITHUB_CLIENT_SECRET:
|
case InfraConfigEnum.GITHUB_CLIENT_SECRET:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.GITHUB_CALLBACK_URL:
|
|
||||||
if (!validateUrl(infraConfigs[i].value))
|
|
||||||
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.GITHUB_SCOPE:
|
case InfraConfigEnum.GITHUB_SCOPE:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MICROSOFT_CLIENT_ID:
|
case InfraConfigEnum.MICROSOFT_CLIENT_ID:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MICROSOFT_CLIENT_SECRET:
|
case InfraConfigEnum.MICROSOFT_CLIENT_SECRET:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MICROSOFT_CALLBACK_URL:
|
|
||||||
if (!validateUrl(infraConfigs[i].value))
|
|
||||||
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MICROSOFT_SCOPE:
|
case InfraConfigEnum.MICROSOFT_SCOPE:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
|
||||||
break;
|
|
||||||
case InfraConfigEnum.MICROSOFT_TENANT:
|
case InfraConfigEnum.MICROSOFT_TENANT:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!value) return fail();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case InfraConfigEnum.GOOGLE_CALLBACK_URL:
|
||||||
|
case InfraConfigEnum.GITHUB_CALLBACK_URL:
|
||||||
|
case InfraConfigEnum.MICROSOFT_CALLBACK_URL:
|
||||||
|
if (!validateUrl(value)) return fail();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS:
|
||||||
|
const allowedAuthProviders = value.split(',');
|
||||||
|
if (
|
||||||
|
allowedAuthProviders.length === 0 ||
|
||||||
|
allowedAuthProviders.some(
|
||||||
|
(p) => !Object.values(AuthProvider).includes(p as AuthProvider),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InfraConfigEnum.TOKEN_SALT_COMPLEXITY:
|
||||||
|
case InfraConfigEnum.MAGIC_LINK_TOKEN_VALIDITY:
|
||||||
|
case InfraConfigEnum.ACCESS_TOKEN_VALIDITY:
|
||||||
|
case InfraConfigEnum.REFRESH_TOKEN_VALIDITY:
|
||||||
|
case InfraConfigEnum.RATE_LIMIT_TTL:
|
||||||
|
case InfraConfigEnum.RATE_LIMIT_MAX:
|
||||||
|
if (!Number.isInteger(Number(value)) || Number(value) < 1)
|
||||||
|
return fail();
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { Body, Controller, Get, HttpStatus, Post, Query } from '@nestjs/common';
|
||||||
|
import { InfraConfigService } from './infra-config.service';
|
||||||
|
import { RESTError } from 'src/types/RESTError';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import {
|
||||||
|
GetOnboardingConfigResponse,
|
||||||
|
GetOnboardingStatusResponse,
|
||||||
|
SaveOnboardingConfigRequest,
|
||||||
|
SaveOnboardingConfigResponse,
|
||||||
|
} from './dto/onboarding.dto';
|
||||||
|
import { ApiCreatedResponse, ApiOkResponse } from '@nestjs/swagger';
|
||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
|
||||||
|
@Controller({ path: 'onboarding', version: '1' })
|
||||||
|
export class OnboardingController {
|
||||||
|
constructor(private infraConfigService: InfraConfigService) {}
|
||||||
|
|
||||||
|
@Get('status')
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: 'Get onboarding status',
|
||||||
|
type: GetOnboardingStatusResponse,
|
||||||
|
})
|
||||||
|
async getOnboardingStatus(): Promise<GetOnboardingStatusResponse> {
|
||||||
|
const onboardingStatus =
|
||||||
|
await this.infraConfigService.getOnboardingStatus();
|
||||||
|
|
||||||
|
if (E.isLeft(onboardingStatus))
|
||||||
|
throwHTTPErr(<RESTError>{
|
||||||
|
message: onboardingStatus.left,
|
||||||
|
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
|
});
|
||||||
|
|
||||||
|
return plainToInstance(
|
||||||
|
GetOnboardingStatusResponse,
|
||||||
|
{
|
||||||
|
onboardingCompleted: onboardingStatus.right.onboardingCompleted,
|
||||||
|
canReRunOnboarding: onboardingStatus.right.canReRunOnboarding,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
excludeExtraneousValues: true,
|
||||||
|
enableImplicitConversion: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('config')
|
||||||
|
@ApiCreatedResponse({
|
||||||
|
description: 'Onboarding configuration updated successfully',
|
||||||
|
type: SaveOnboardingConfigResponse,
|
||||||
|
})
|
||||||
|
async updateOnboardingConfig(@Body() dto: SaveOnboardingConfigRequest) {
|
||||||
|
const updateConfigResult =
|
||||||
|
await this.infraConfigService.updateOnboardingConfig(dto);
|
||||||
|
|
||||||
|
if (E.isLeft(updateConfigResult))
|
||||||
|
throwHTTPErr(<RESTError>{
|
||||||
|
message: updateConfigResult.left,
|
||||||
|
statusCode: HttpStatus.BAD_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
return plainToInstance(
|
||||||
|
SaveOnboardingConfigResponse,
|
||||||
|
updateConfigResult.right,
|
||||||
|
{
|
||||||
|
excludeExtraneousValues: true,
|
||||||
|
enableImplicitConversion: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('config')
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: 'Get onboarding configuration',
|
||||||
|
type: GetOnboardingConfigResponse,
|
||||||
|
})
|
||||||
|
async getOnboardingConfig(@Query('token') token: string) {
|
||||||
|
const onboardingConfig =
|
||||||
|
await this.infraConfigService.getOnboardingConfig(token);
|
||||||
|
|
||||||
|
if (E.isLeft(onboardingConfig))
|
||||||
|
throwHTTPErr(<RESTError>{
|
||||||
|
message: onboardingConfig.left,
|
||||||
|
statusCode: HttpStatus.BAD_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
return plainToInstance(
|
||||||
|
GetOnboardingConfigResponse,
|
||||||
|
onboardingConfig.right,
|
||||||
|
{
|
||||||
|
excludeExtraneousValues: true,
|
||||||
|
enableImplicitConversion: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,49 +10,33 @@ function isSMTPCustomConfigsEnabled(value) {
|
||||||
return value === 'true';
|
return value === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMailerAddressFrom(env, config): string {
|
export function getMailerAddressFrom(env): string {
|
||||||
return (
|
return env.INFRA.MAILER_ADDRESS_FROM ?? throwErr(MAILER_SMTP_URL_UNDEFINED);
|
||||||
env.INFRA.MAILER_ADDRESS_FROM ??
|
|
||||||
config.get('MAILER_ADDRESS_FROM') ??
|
|
||||||
throwErr(MAILER_SMTP_URL_UNDEFINED)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTransportOption(env, config): TransportType {
|
export function getTransportOption(env): TransportType {
|
||||||
const useCustomConfigs = isSMTPCustomConfigsEnabled(
|
const useCustomConfigs = isSMTPCustomConfigsEnabled(
|
||||||
env.INFRA.MAILER_USE_CUSTOM_CONFIGS ??
|
env.INFRA.MAILER_USE_CUSTOM_CONFIGS,
|
||||||
config.get('MAILER_USE_CUSTOM_CONFIGS'),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!useCustomConfigs) {
|
if (!useCustomConfigs) {
|
||||||
console.log('Using simple mailer configuration');
|
console.log('Using simple mailer configuration');
|
||||||
return (
|
return env.INFRA.MAILER_SMTP_URL ?? throwErr(MAILER_SMTP_URL_UNDEFINED);
|
||||||
env.INFRA.MAILER_SMTP_URL ??
|
|
||||||
config.get('MAILER_SMTP_URL') ??
|
|
||||||
throwErr(MAILER_SMTP_URL_UNDEFINED)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
console.log('Using advanced mailer configuration');
|
console.log('Using advanced mailer configuration');
|
||||||
return {
|
return {
|
||||||
host: env.INFRA.MAILER_SMTP_HOST ?? config.get('MAILER_SMTP_HOST'),
|
host: env.INFRA.MAILER_SMTP_HOST,
|
||||||
port: +(env.INFRA.MAILER_SMTP_PORT ?? config.get('MAILER_SMTP_PORT')),
|
port: +env.INFRA.MAILER_SMTP_PORT,
|
||||||
secure:
|
secure: env.INFRA.MAILER_SMTP_SECURE === 'true',
|
||||||
(env.INFRA.MAILER_SMTP_SECURE ?? config.get('MAILER_SMTP_SECURE')) ===
|
|
||||||
'true',
|
|
||||||
auth: {
|
auth: {
|
||||||
user:
|
user:
|
||||||
env.INFRA.MAILER_SMTP_USER ??
|
env.INFRA.MAILER_SMTP_USER ?? throwErr(MAILER_SMTP_USER_UNDEFINED),
|
||||||
config.get('MAILER_SMTP_USER') ??
|
|
||||||
throwErr(MAILER_SMTP_USER_UNDEFINED),
|
|
||||||
pass:
|
pass:
|
||||||
env.INFRA.MAILER_SMTP_PASSWORD ??
|
env.INFRA.MAILER_SMTP_PASSWORD ??
|
||||||
config.get('MAILER_SMTP_PASSWORD') ??
|
|
||||||
throwErr(MAILER_SMTP_PASSWORD_UNDEFINED),
|
throwErr(MAILER_SMTP_PASSWORD_UNDEFINED),
|
||||||
},
|
},
|
||||||
tls: {
|
tls: {
|
||||||
rejectUnauthorized:
|
rejectUnauthorized: env.INFRA.MAILER_TLS_REJECT_UNAUTHORIZED === 'true',
|
||||||
(env.INFRA.MAILER_TLS_REJECT_UNAUTHORIZED ??
|
|
||||||
config.get('MAILER_TLS_REJECT_UNAUTHORIZED')) === 'true',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { Global, Module } from '@nestjs/common';
|
||||||
import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer';
|
import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer';
|
||||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
|
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
|
||||||
import { MailerService } from './mailer.service';
|
import { MailerService } from './mailer.service';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
||||||
import { getMailerAddressFrom, getTransportOption } from './helper';
|
import { getMailerAddressFrom, getTransportOption } from './helper';
|
||||||
|
|
||||||
|
|
@ -14,7 +13,6 @@ import { getMailerAddressFrom, getTransportOption } from './helper';
|
||||||
})
|
})
|
||||||
export class MailerModule {
|
export class MailerModule {
|
||||||
static async register() {
|
static async register() {
|
||||||
const config = new ConfigService();
|
|
||||||
const env = await loadInfraConfiguration();
|
const env = await loadInfraConfiguration();
|
||||||
|
|
||||||
// If mailer SMTP is DISABLED, return the module without any configuration (service, listener, etc.)
|
// If mailer SMTP is DISABLED, return the module without any configuration (service, listener, etc.)
|
||||||
|
|
@ -28,9 +26,9 @@ export class MailerModule {
|
||||||
// If mailer is ENABLED, return the module with configuration (service, etc.)
|
// If mailer is ENABLED, return the module with configuration (service, etc.)
|
||||||
|
|
||||||
// Determine transport configuration based on custom config flag
|
// Determine transport configuration based on custom config flag
|
||||||
const transportOption = getTransportOption(env, config);
|
const transportOption = getTransportOption(env);
|
||||||
// Get mailer address from environment or config
|
// Get mailer address from environment or config
|
||||||
const mailerAddressFrom = getMailerAddressFrom(env, config);
|
const mailerAddressFrom = getMailerAddressFrom(env);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
module: MailerModule,
|
module: MailerModule,
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,16 @@ import { NestFactory } from '@nestjs/core';
|
||||||
import { json } from 'express';
|
import { json } from 'express';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import * as cookieParser from 'cookie-parser';
|
import * as cookieParser from 'cookie-parser';
|
||||||
import * as crypto from 'crypto';
|
|
||||||
import { ValidationPipe, VersioningType } from '@nestjs/common';
|
import { ValidationPipe, VersioningType } from '@nestjs/common';
|
||||||
import * as session from 'express-session';
|
import * as session from 'express-session';
|
||||||
import { emitGQLSchemaFile } from './gql-schema';
|
import { emitGQLSchemaFile } from './gql-schema';
|
||||||
import { checkEnvironmentAuthProvider } from './utils';
|
import * as crypto from 'crypto';
|
||||||
|
import * as morgan from 'morgan';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
import { InfraTokenModule } from './infra-token/infra-token.module';
|
import { InfraTokenModule } from './infra-token/infra-token.module';
|
||||||
|
|
||||||
function setupSwagger(app) {
|
function setupSwagger(app, isProduction: boolean) {
|
||||||
const swaggerDocPath = '/api-docs';
|
const swaggerDocPath = '/api-docs';
|
||||||
|
|
||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
|
|
@ -30,7 +30,7 @@ function setupSwagger(app) {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const document = SwaggerModule.createDocument(app, config, {
|
const document = SwaggerModule.createDocument(app, config, {
|
||||||
include: [InfraTokenModule],
|
include: isProduction ? [InfraTokenModule] : [],
|
||||||
});
|
});
|
||||||
SwaggerModule.setup(swaggerDocPath, app, document, {
|
SwaggerModule.setup(swaggerDocPath, app, document, {
|
||||||
swaggerOptions: { persistAuthorization: true, ignoreGlobalPrefix: true },
|
swaggerOptions: { persistAuthorization: true, ignoreGlobalPrefix: true },
|
||||||
|
|
@ -41,15 +41,11 @@ async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
const configService = app.get(ConfigService);
|
const configService = app.get(ConfigService);
|
||||||
|
const isProduction = configService.get('PRODUCTION') === 'true';
|
||||||
|
|
||||||
console.log(`Running in production: ${configService.get('PRODUCTION')}`);
|
console.log(`Running in production: ${isProduction}`);
|
||||||
console.log(`Port: ${configService.get('PORT')}`);
|
console.log(`Port: ${configService.get('PORT')}`);
|
||||||
|
|
||||||
checkEnvironmentAuthProvider(
|
|
||||||
configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS') ??
|
|
||||||
configService.get('VITE_ALLOWED_AUTH_PROVIDERS'),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
secret:
|
secret:
|
||||||
|
|
@ -72,7 +68,7 @@ async function bootstrap() {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (configService.get('PRODUCTION') === 'true') {
|
if (isProduction) {
|
||||||
console.log('Enabling CORS with production settings');
|
console.log('Enabling CORS with production settings');
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: configService.get('WHITELISTED_ORIGINS').split(','),
|
origin: configService.get('WHITELISTED_ORIGINS').split(','),
|
||||||
|
|
@ -95,8 +91,9 @@ async function bootstrap() {
|
||||||
transform: true,
|
transform: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
app.use(morgan(':method :url :status - :response-time ms'));
|
||||||
|
|
||||||
await setupSwagger(app);
|
await setupSwagger(app, isProduction);
|
||||||
|
|
||||||
await app.listen(configService.get('PORT') || 3170);
|
await app.listen(configService.get('PORT') || 3170);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -910,7 +910,7 @@ describe('deleteUserFromAllTeams', () => {
|
||||||
|
|
||||||
const result = teamService.deleteUserFromAllTeams(dbTeamMember.userUid)();
|
const result = teamService.deleteUserFromAllTeams(dbTeamMember.userUid)();
|
||||||
|
|
||||||
await expect(result).rejects.toThrowError(TEAM_ONLY_ONE_OWNER);
|
await expect(result).rejects.toThrow(TEAM_ONLY_ONE_OWNER);
|
||||||
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
userUid: dbTeamMember.userUid,
|
userUid: dbTeamMember.userUid,
|
||||||
|
|
@ -932,7 +932,7 @@ describe('deleteUserFromAllTeams', () => {
|
||||||
|
|
||||||
const result = teamService.deleteUserFromAllTeams(dbTeamMember.userUid);
|
const result = teamService.deleteUserFromAllTeams(dbTeamMember.userUid);
|
||||||
|
|
||||||
await expect(result).rejects.toThrowError(TEAM_INVALID_ID_OR_USER);
|
await expect(result).rejects.toThrow(TEAM_INVALID_ID_OR_USER);
|
||||||
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
userUid: dbTeamMember.userUid,
|
userUid: dbTeamMember.userUid,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,18 @@
|
||||||
export enum InfraConfigEnum {
|
export enum InfraConfigEnum {
|
||||||
|
ONBOARDING_COMPLETED = 'ONBOARDING_COMPLETED',
|
||||||
|
ONBOARDING_RECOVERY_TOKEN = 'ONBOARDING_RECOVERY_TOKEN',
|
||||||
|
|
||||||
|
JWT_SECRET = 'JWT_SECRET',
|
||||||
|
SESSION_SECRET = 'SESSION_SECRET',
|
||||||
|
TOKEN_SALT_COMPLEXITY = 'TOKEN_SALT_COMPLEXITY',
|
||||||
|
MAGIC_LINK_TOKEN_VALIDITY = 'MAGIC_LINK_TOKEN_VALIDITY',
|
||||||
|
REFRESH_TOKEN_VALIDITY = 'REFRESH_TOKEN_VALIDITY',
|
||||||
|
ACCESS_TOKEN_VALIDITY = 'ACCESS_TOKEN_VALIDITY',
|
||||||
|
ALLOW_SECURE_COOKIES = 'ALLOW_SECURE_COOKIES',
|
||||||
|
|
||||||
|
RATE_LIMIT_TTL = 'RATE_LIMIT_TTL',
|
||||||
|
RATE_LIMIT_MAX = 'RATE_LIMIT_MAX',
|
||||||
|
|
||||||
MAILER_SMTP_ENABLE = 'MAILER_SMTP_ENABLE',
|
MAILER_SMTP_ENABLE = 'MAILER_SMTP_ENABLE',
|
||||||
MAILER_USE_CUSTOM_CONFIGS = 'MAILER_USE_CUSTOM_CONFIGS',
|
MAILER_USE_CUSTOM_CONFIGS = 'MAILER_USE_CUSTOM_CONFIGS',
|
||||||
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
|
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ describe('UserSettingsService', () => {
|
||||||
|
|
||||||
await userSettingsService.createUserSettings(user, settings.properties);
|
await userSettingsService.createUserSettings(user, settings.properties);
|
||||||
|
|
||||||
expect(mockPubSub.publish).toBeCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`user_settings/${user.uid}/created`,
|
`user_settings/${user.uid}/created`,
|
||||||
settings,
|
settings,
|
||||||
);
|
);
|
||||||
|
|
@ -126,7 +126,7 @@ describe('UserSettingsService', () => {
|
||||||
|
|
||||||
await userSettingsService.updateUserSettings(user, settings.properties);
|
await userSettingsService.updateUserSettings(user, settings.properties);
|
||||||
|
|
||||||
expect(mockPubSub.publish).toBeCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`user_settings/${user.uid}/updated`,
|
`user_settings/${user.uid}/updated`,
|
||||||
settings,
|
settings,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,7 @@ import { pipe } from 'fp-ts/lib/function';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as T from 'fp-ts/Task';
|
import * as T from 'fp-ts/Task';
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
import { AuthProvider } from './auth/helper';
|
import { ENV_NOT_FOUND_KEY_DATA_ENCRYPTION_KEY, JSON_INVALID } from './errors';
|
||||||
import {
|
|
||||||
ENV_EMPTY_AUTH_PROVIDERS,
|
|
||||||
ENV_NOT_FOUND_KEY_AUTH_PROVIDERS,
|
|
||||||
ENV_NOT_FOUND_KEY_DATA_ENCRYPTION_KEY,
|
|
||||||
ENV_NOT_SUPPORT_AUTH_PROVIDERS,
|
|
||||||
JSON_INVALID,
|
|
||||||
} from './errors';
|
|
||||||
import { TeamAccessRole } from './team/team.model';
|
import { TeamAccessRole } from './team/team.model';
|
||||||
import { RESTError } from './types/RESTError';
|
import { RESTError } from './types/RESTError';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
|
@ -234,36 +227,6 @@ export function isValidLength(title: string, length: number) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is called by bootstrap() in main.ts
|
|
||||||
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
|
|
||||||
* If not, it throws an error.
|
|
||||||
*/
|
|
||||||
export function checkEnvironmentAuthProvider(
|
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS: string,
|
|
||||||
) {
|
|
||||||
if (!VITE_ALLOWED_AUTH_PROVIDERS) {
|
|
||||||
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (VITE_ALLOWED_AUTH_PROVIDERS === '') {
|
|
||||||
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
|
|
||||||
}
|
|
||||||
|
|
||||||
const givenAuthProviders = VITE_ALLOWED_AUTH_PROVIDERS.split(',').map(
|
|
||||||
(provider) => provider.toLocaleUpperCase(),
|
|
||||||
);
|
|
||||||
const supportedAuthProviders = Object.values(AuthProvider).map(
|
|
||||||
(provider: string) => provider.toLocaleUpperCase(),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const givenAuthProvider of givenAuthProviders) {
|
|
||||||
if (!supportedAuthProviders.includes(givenAuthProvider)) {
|
|
||||||
throw new Error(ENV_NOT_SUPPORT_AUTH_PROVIDERS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds escape backslashes to the input so that it can be used inside
|
* Adds escape backslashes to the input so that it can be used inside
|
||||||
* SQL LIKE/ILIKE queries. Inspired by PHP's `mysql_real_escape_string`
|
* SQL LIKE/ILIKE queries. Inspired by PHP's `mysql_real_escape_string`
|
||||||
|
|
@ -342,7 +305,7 @@ const ENCRYPTION_ALGORITHM = 'aes-256-cbc';
|
||||||
export function encrypt(text: string, key = process.env.DATA_ENCRYPTION_KEY) {
|
export function encrypt(text: string, key = process.env.DATA_ENCRYPTION_KEY) {
|
||||||
if (!key) throw new Error(ENV_NOT_FOUND_KEY_DATA_ENCRYPTION_KEY);
|
if (!key) throw new Error(ENV_NOT_FOUND_KEY_DATA_ENCRYPTION_KEY);
|
||||||
|
|
||||||
if (text === null || text === undefined) return text;
|
if (!text || text === '') return text;
|
||||||
|
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
const cipher = crypto.createCipheriv(
|
const cipher = crypto.createCipheriv(
|
||||||
|
|
@ -367,7 +330,7 @@ export function decrypt(
|
||||||
) {
|
) {
|
||||||
if (!key) throw new Error(ENV_NOT_FOUND_KEY_DATA_ENCRYPTION_KEY);
|
if (!key) throw new Error(ENV_NOT_FOUND_KEY_DATA_ENCRYPTION_KEY);
|
||||||
|
|
||||||
if (encryptedData === null || encryptedData === undefined) {
|
if (!encryptedData || encryptedData === '') {
|
||||||
return encryptedData;
|
return encryptedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1332,7 +1332,7 @@
|
||||||
"admin_success": "User is now an admin!!",
|
"admin_success": "User is now an admin!!",
|
||||||
"and": "and",
|
"and": "and",
|
||||||
"clear_selection": "Clear Selection",
|
"clear_selection": "Clear Selection",
|
||||||
"configure_auth": "Check out the documentation to configure auth providers.",
|
"configure_auth": "Please set up an auth provider from the admin settings or check out the documentation to configure auth providers.",
|
||||||
"confirm_admin_to_user": "Do you want to remove admin status from this user?",
|
"confirm_admin_to_user": "Do you want to remove admin status from this user?",
|
||||||
"confirm_admins_to_users": "Do you want to remove admin status from selected users?",
|
"confirm_admins_to_users": "Do you want to remove admin status from selected users?",
|
||||||
"confirm_delete_infra_token": "Are you sure you want to delete the infra token {tokenLabel}?",
|
"confirm_delete_infra_token": "Are you sure you want to delete the infra token {tokenLabel}?",
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,27 @@
|
||||||
:label="`${t('auth.send_magic_link')}`"
|
:label="`${t('auth.send_magic_link')}`"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!allowedAuthProviders?.length"
|
||||||
|
class="flex flex-col items-center text-center"
|
||||||
|
>
|
||||||
|
<p>{{ t("state.require_auth_provider") }}</p>
|
||||||
|
<p>{{ t("state.configure_auth") }}</p>
|
||||||
|
<div class="mt-5">
|
||||||
|
<a
|
||||||
|
href="https://docs.hoppscotch.io/documentation/self-host/getting-started"
|
||||||
|
>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
outline
|
||||||
|
filled
|
||||||
|
blank
|
||||||
|
:icon="IconFileText"
|
||||||
|
:label="t('state.self_host_docs')"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
|
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
|
||||||
<div class="flex max-w-md flex-col items-center justify-center">
|
<div class="flex max-w-md flex-col items-center justify-center">
|
||||||
<icon-lucide-inbox class="h-6 w-6 text-accent" />
|
<icon-lucide-inbox class="h-6 w-6 text-accent" />
|
||||||
|
|
@ -133,6 +154,7 @@ import IconGithub from "~icons/auth/github"
|
||||||
import IconGoogle from "~icons/auth/google"
|
import IconGoogle from "~icons/auth/google"
|
||||||
import IconMicrosoft from "~icons/auth/microsoft"
|
import IconMicrosoft from "~icons/auth/microsoft"
|
||||||
import IconArrowLeft from "~icons/lucide/arrow-left"
|
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||||
|
import IconFileText from "~icons/lucide/file-text"
|
||||||
|
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { LoginItemDef } from "~/platform/auth"
|
import { LoginItemDef } from "~/platform/auth"
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,21 @@
|
||||||
"sso": "SSO",
|
"sso": "SSO",
|
||||||
"tenant": "TENANT",
|
"tenant": "TENANT",
|
||||||
"title": "OAuth Providers",
|
"title": "OAuth Providers",
|
||||||
|
"token": {
|
||||||
|
"description": "Configure token for your server",
|
||||||
|
"title": "Token",
|
||||||
|
"jwt_secret": " JWT Secret",
|
||||||
|
"token_salt_complexity": "Token Salt Complexity",
|
||||||
|
"magic_link_token_validity": "Magic Link Token Validity (in hour)",
|
||||||
|
"refresh_token_validity": "Refresh Token Validity (in milliseconds)",
|
||||||
|
"access_token_validity": "Access Token Validity (in milliseconds)",
|
||||||
|
"session_secret": "Session Secret",
|
||||||
|
"update_failure": "Failed to update token configurations!!"
|
||||||
|
},
|
||||||
"update_failure": "Failed to update authentication provider configurations!!"
|
"update_failure": "Failed to update authentication provider configurations!!"
|
||||||
},
|
},
|
||||||
"confirm_changes": "Hoppscotch server must restart to reflect the new changes. Confirm changes made to the server configurations?",
|
"confirm_changes": "Hoppscotch server must restart to reflect the new changes. Confirm changes made to the server configurations?",
|
||||||
|
"invalid_number": "Please enter a valid number for the field",
|
||||||
"input_empty": "Please fill all the fields before updating the configurations",
|
"input_empty": "Please fill all the fields before updating the configurations",
|
||||||
"input_validation_error": "Some fields have invalid values. Please correct them before updating the configurations",
|
"input_validation_error": "Some fields have invalid values. Please correct them before updating the configurations",
|
||||||
"data_sharing": {
|
"data_sharing": {
|
||||||
|
|
@ -85,6 +97,14 @@
|
||||||
"toggle_failure": "Failed to toggle history!!",
|
"toggle_failure": "Failed to toggle history!!",
|
||||||
"clear_confirm": "Are you sure you want to clear all history?"
|
"clear_confirm": "Are you sure you want to clear all history?"
|
||||||
},
|
},
|
||||||
|
"rate_limit": {
|
||||||
|
"description": "Configure rate limiting for your Hoppscotch instance",
|
||||||
|
"rate_limit_max": "Maximum Requests",
|
||||||
|
"rate_limit_ttl": "Time to Leave (in seconds)",
|
||||||
|
"title": "Rate Limit Configurations",
|
||||||
|
"update_failure": "Failed to update rate limit configurations!!",
|
||||||
|
"input_validation_error": "Please enter valid values for rate limit configurations"
|
||||||
|
},
|
||||||
"reset": {
|
"reset": {
|
||||||
"confirm_reset": "Hoppscotch server must restart to reflect the new changes. Confirm the reset of server configurations?",
|
"confirm_reset": "Hoppscotch server must restart to reflect the new changes. Confirm the reset of server configurations?",
|
||||||
"description": "Default configurations will be loaded as specified in the environment file",
|
"description": "Default configurations will be loaded as specified in the environment file",
|
||||||
|
|
@ -102,6 +122,7 @@
|
||||||
"auth": "Authentication",
|
"auth": "Authentication",
|
||||||
"activity": "Activity",
|
"activity": "Activity",
|
||||||
"infra_tokens": "Infra Tokens",
|
"infra_tokens": "Infra Tokens",
|
||||||
|
"rate_limit": "Rate Limit",
|
||||||
"smtp": "SMTP",
|
"smtp": "SMTP",
|
||||||
"miscellaneous": "Miscellaneous",
|
"miscellaneous": "Miscellaneous",
|
||||||
"reset": "Reset Configurations"
|
"reset": "Reset Configurations"
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
<div v-else class="flex flex-1 flex-col">
|
<div v-else class="flex flex-1 flex-col">
|
||||||
<div
|
<div
|
||||||
class="p-6 bg-primaryLight rounded-lg border border-primaryDark shadow"
|
class="p-4 bg-primaryLight rounded-lg border border-primaryDark shadow"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="mode === 'sign-in' && allowedAuthProviders"
|
v-if="mode === 'sign-in' && allowedAuthProviders"
|
||||||
|
|
@ -71,7 +71,10 @@
|
||||||
:label="t('state.send_magic_link')"
|
:label="t('state.send_magic_link')"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
<div v-if="!allowedAuthProviders">
|
<div
|
||||||
|
v-if="!allowedAuthProviders?.length"
|
||||||
|
class="flex flex-col items-center text-center"
|
||||||
|
>
|
||||||
<p>{{ t('state.require_auth_provider') }}</p>
|
<p>{{ t('state.require_auth_provider') }}</p>
|
||||||
<p>{{ t('state.configure_auth') }}</p>
|
<p>{{ t('state.configure_auth') }}</p>
|
||||||
<div class="mt-5">
|
<div class="mt-5">
|
||||||
|
|
@ -102,7 +105,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="mt-16">
|
<div v-if="canReRunOnboarding" class="mt-4">
|
||||||
|
<span class="text-tiny"> Need to re-configure auth providers? </span>
|
||||||
|
<HoppSmartAnchor
|
||||||
|
class="link text-tiny"
|
||||||
|
:to="`/onboarding`"
|
||||||
|
label="Setup Authentication"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="mt-8">
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
mode === 'sign-in' &&
|
mode === 'sign-in' &&
|
||||||
|
|
@ -151,6 +163,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computedAsync } from '@vueuse/core';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useI18n } from '~/composables/i18n';
|
import { useI18n } from '~/composables/i18n';
|
||||||
import { useToast } from '~/composables/toast';
|
import { useToast } from '~/composables/toast';
|
||||||
|
|
@ -183,6 +196,15 @@ const nonAdminUser = ref(false);
|
||||||
|
|
||||||
const allowedAuthProviders = ref<string[]>([]);
|
const allowedAuthProviders = ref<string[]>([]);
|
||||||
|
|
||||||
|
// check if the user can re-run onboarding
|
||||||
|
const canReRunOnboarding = computedAsync(async () => {
|
||||||
|
const onboardingStatus = await auth.getOnboardingStatus();
|
||||||
|
|
||||||
|
return (
|
||||||
|
onboardingStatus?.onboardingCompleted && onboardingStatus.canReRunOnboarding
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const user = auth.getCurrentUser();
|
const user = auth.getCurrentUser();
|
||||||
if (user && !user.isAdmin) {
|
if (user && !user.isAdmin) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,304 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col flex-1 py-8 p-4 justify-center w-full">
|
||||||
|
<div class="w-full max-w-screen-md mx-auto">
|
||||||
|
<div
|
||||||
|
v-if="isFirstTimeSetup && authConfigStep === 1"
|
||||||
|
class="py-8 rounded max-h-lg overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-[1rem] font-normal">
|
||||||
|
Please select the authentication methods you want to set up.
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center space-x-4 my-8">
|
||||||
|
<div
|
||||||
|
class="flex flex-col space-y-2 p-5 h-40 cursor-pointer flex-1 border-2 bg-primaryLight rounded-lg border-primaryDark shadow hover:border-accent transition"
|
||||||
|
:class="{
|
||||||
|
'!bg-primary !border-accentDark':
|
||||||
|
selectedOptions.includes('OAuth'),
|
||||||
|
}"
|
||||||
|
@click="toggleSelectedOption('OAuth')"
|
||||||
|
>
|
||||||
|
<span class="text-[0.9rem] text-secondaryDark"> OAuth </span>
|
||||||
|
<span class="text-secondaryLight h-10">
|
||||||
|
Set up OAuth providers like Google, GitHub, Microsoft, etc.
|
||||||
|
</span>
|
||||||
|
<div class="my-4">
|
||||||
|
<div class="flex items-center -space-x-2">
|
||||||
|
<img
|
||||||
|
alt="user 1"
|
||||||
|
src="/assets/icons/auth/google.svg"
|
||||||
|
class="relative inline-block h-6 w-6 rounded-full border-2 border-primary object-cover object-center hover:z-10 focus:z-10"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
alt="user 2"
|
||||||
|
src="/assets/icons/auth/github.svg"
|
||||||
|
class="relative inline-block h-6 w-6 rounded-full border-2 border-primary object-cover object-center hover:z-10 focus:z-10"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
alt="user 3"
|
||||||
|
src="/assets/icons/auth/microsoft.svg"
|
||||||
|
class="relative inline-block h-6 w-6 rounded-full border-2 border-primary object-cover object-center hover:z-10 focus:z-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex flex-col space-y-2 p-5 h-40 cursor-pointer flex-1 border-2 bg-primaryLight rounded-lg border-primaryDark shadow hover:border-accent transition"
|
||||||
|
:class="{
|
||||||
|
'!bg-primary !border-accentDark':
|
||||||
|
selectedOptions.includes('SMTP'),
|
||||||
|
}"
|
||||||
|
@click="toggleSelectedOption('SMTP')"
|
||||||
|
>
|
||||||
|
<span class="text-[0.9rem] text-secondaryDark"> SMTP </span>
|
||||||
|
<span class="text-secondaryLight h-10">
|
||||||
|
Set up SMTP for email authentication.
|
||||||
|
</span>
|
||||||
|
<div class="my-4">
|
||||||
|
<div class="flex items-center -space-x-2">
|
||||||
|
<img
|
||||||
|
alt="user 2"
|
||||||
|
src="/assets/icons/auth/email.svg"
|
||||||
|
class="relative inline-block h-6 w-6 rounded-full border-2 border-primary object-cover object-center hover:z-10 focus:z-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<HoppButtonPrimary
|
||||||
|
:label="'Add Auth Configs'"
|
||||||
|
@click="addAuthConfig"
|
||||||
|
:icon="IconLucideArrowRight"
|
||||||
|
:reverse="true"
|
||||||
|
class="mt-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth setup step 2 -->
|
||||||
|
<div v-else-if="authConfigStep === 2">
|
||||||
|
<button
|
||||||
|
v-if="isFirstTimeSetup"
|
||||||
|
class="items-center flex space-x-2 cursor-pointer mb-4 group"
|
||||||
|
@click="authConfigStep = 1"
|
||||||
|
>
|
||||||
|
<span class="group-hover:opacity-80 transition-opacity">
|
||||||
|
<IconLucideArrowLeft class="svg-icons" />
|
||||||
|
</span>
|
||||||
|
<span class="group-hover:opacity-80 transition-opacity">Back</span>
|
||||||
|
</button>
|
||||||
|
<h2>
|
||||||
|
Please add the configurations for the selected authentication methods.
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="my-5 overflow-y-auto max-h-[60vh]">
|
||||||
|
<div
|
||||||
|
id="accordion-nested-parent"
|
||||||
|
data-accordion="collapse"
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
|
<UiAccordion
|
||||||
|
v-if="selectedOptions.includes('OAuth')"
|
||||||
|
:initial-open="isOAuthEnabled"
|
||||||
|
class="bg-primary rounded-lg border-primaryDark shadow p-4 border flex flex-col"
|
||||||
|
>
|
||||||
|
<template v-slot:header="{ isOpen, toggleAccordion }">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex items-center justify-between flex-1 mb-2">
|
||||||
|
<span class="font-semibold text-[0.8rem]">OAuth </span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<HoppSmartToggle
|
||||||
|
:on="isOpen"
|
||||||
|
@change="
|
||||||
|
() => {
|
||||||
|
toggleAccordion();
|
||||||
|
toggleConfig('OAUTH');
|
||||||
|
}
|
||||||
|
"
|
||||||
|
class="ml-2"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-tiny text-secondaryLight">
|
||||||
|
Select the OAuth providers you want to enable and provide
|
||||||
|
<br />
|
||||||
|
the necessary configurations.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:content>
|
||||||
|
<OAuthSetup
|
||||||
|
v-model:currentConfigs="currentConfigs"
|
||||||
|
v-model:enabledConfigs="enabledConfigs"
|
||||||
|
@toggleConfig="toggleConfig"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UiAccordion>
|
||||||
|
|
||||||
|
<UiAccordion
|
||||||
|
v-if="selectedOptions.includes('SMTP')"
|
||||||
|
:initial-open="enabledConfigs.includes('MAILER')"
|
||||||
|
class="bg-primary rounded-lg border-primaryDark shadow p-4 border flex flex-col"
|
||||||
|
>
|
||||||
|
<template v-slot:header="{ isOpen, toggleAccordion }">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex items-center justify-between flex-1 mb-2">
|
||||||
|
<span class="font-semibold text-[0.8rem]">SMTP</span>
|
||||||
|
<span>
|
||||||
|
<HoppSmartToggle
|
||||||
|
:on="isOpen"
|
||||||
|
@change="
|
||||||
|
() => {
|
||||||
|
toggleAccordion();
|
||||||
|
toggleConfig('EMAIL');
|
||||||
|
toggleSmtpConfig();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
class="ml-2"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-secondaryLight text-tiny">
|
||||||
|
Configure the SMTP settings for sending emails.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:content>
|
||||||
|
<SmtpSetup
|
||||||
|
v-model:currentConfigs="currentConfigs"
|
||||||
|
v-model:enabledConfigs="enabledConfigs"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UiAccordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<HoppButtonPrimary
|
||||||
|
:label="'Save Auth Configs'"
|
||||||
|
@click="addOnboardingConfigs"
|
||||||
|
:reverse="true"
|
||||||
|
:icon="IconLucideSave"
|
||||||
|
class="mt-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import {
|
||||||
|
OAuthProvider,
|
||||||
|
OnBoardingSummary,
|
||||||
|
useOnboardingConfigHandler,
|
||||||
|
} from '~/composables/useOnboardingConfigHandler';
|
||||||
|
import OAuthSetup from './OAuthSetup.vue';
|
||||||
|
import SmtpSetup from './SmtpSetup.vue';
|
||||||
|
import IconLucideArrowRight from '~icons/lucide/arrow-right';
|
||||||
|
import IconLucideArrowLeft from '~icons/lucide/arrow-left';
|
||||||
|
import IconLucideSave from '~icons/lucide/save';
|
||||||
|
import { useToast } from '~/composables/toast';
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
isFirstTimeSetup: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
isFirstTimeSetup: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(
|
||||||
|
e: 'complete-onboarding',
|
||||||
|
payload: {
|
||||||
|
submittingConfigs: boolean;
|
||||||
|
summary: OnBoardingSummary;
|
||||||
|
}
|
||||||
|
): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const authConfigStep = ref(1);
|
||||||
|
|
||||||
|
const selectedOptions = ref<Array<'OAuth' | 'SMTP' | ''>>([]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.isFirstTimeSetup,
|
||||||
|
(newValue) => {
|
||||||
|
if (!newValue) {
|
||||||
|
authConfigStep.value = 2; // Skip to step 2 if not first time setup
|
||||||
|
selectedOptions.value = ['OAuth', 'SMTP']; // Default to both options
|
||||||
|
} else {
|
||||||
|
authConfigStep.value = 1; // Reset to step 1 for first time setup
|
||||||
|
selectedOptions.value = []; // Reset selected options
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentConfigs,
|
||||||
|
enabledConfigs,
|
||||||
|
isProvidersLoading,
|
||||||
|
submittingConfigs,
|
||||||
|
onBoardingSummary,
|
||||||
|
addOnBoardingConfigs,
|
||||||
|
toggleConfig,
|
||||||
|
toggleSmtpConfig,
|
||||||
|
} = useOnboardingConfigHandler();
|
||||||
|
|
||||||
|
const OAuthProviders: OAuthProvider[] = ['GOOGLE', 'GITHUB', 'MICROSOFT'];
|
||||||
|
|
||||||
|
const isOAuthEnabled = ref(false);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Check if any OAuth provider is enabled
|
||||||
|
isOAuthEnabled.value = OAuthProviders.some((provider) =>
|
||||||
|
enabledConfigs.value.includes(provider)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
isProvidersLoading,
|
||||||
|
(isLoading) => {
|
||||||
|
if (!isLoading) {
|
||||||
|
isOAuthEnabled.value = OAuthProviders.some((provider) =>
|
||||||
|
enabledConfigs.value.includes(provider)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const addOnboardingConfigs = async () => {
|
||||||
|
const res = await addOnBoardingConfigs();
|
||||||
|
if (res && res.token) {
|
||||||
|
emit('complete-onboarding', {
|
||||||
|
submittingConfigs: submittingConfigs.value,
|
||||||
|
summary: onBoardingSummary.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSelectedOption = (option: 'OAuth' | 'SMTP') => {
|
||||||
|
if (selectedOptions.value.includes(option)) {
|
||||||
|
selectedOptions.value = selectedOptions.value.filter(
|
||||||
|
(opt) => opt !== option
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
selectedOptions.value.push(option);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAuthConfig = () => {
|
||||||
|
if (selectedOptions.value.length === 0) {
|
||||||
|
toast.error('Please select at least one authentication method.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
authConfigStep.value = 2;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col space-y-4 p-4 max-w-screen-md mx-auto w-full">
|
||||||
|
<template
|
||||||
|
v-if="
|
||||||
|
onBoardingSummary.type === 'success' &&
|
||||||
|
onBoardingSummary.configsAdded.length > 0
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h1 class="text-2xl font-bold text-white">Setup Complete 🎉</h1>
|
||||||
|
<h2>
|
||||||
|
You have successfully completed the onboarding process for your
|
||||||
|
Hoppscotch instance.
|
||||||
|
<br />
|
||||||
|
The page will automatically redirect to the dashboard in a few seconds
|
||||||
|
or you can reload once the server is restarted.
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
class="my-4 p-4 bg-primaryLight rounded-lg border border-primaryDark shadow"
|
||||||
|
>
|
||||||
|
<h3 class="text-lg mb-6">Configuration Summary</h3>
|
||||||
|
<div v-for="config in onBoardingSummary.configsAdded" class="my-2">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<icon-lucide-check class="svg-icons text-green-500" />
|
||||||
|
<p class="text-secondary">
|
||||||
|
<span class="capitalize"> {{ config.toLocaleLowerCase() }}</span>
|
||||||
|
authentication has been successfully configured.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<HoppButtonPrimary
|
||||||
|
v-if="duration"
|
||||||
|
:label="`Server is restarting in ${duration} seconds...`"
|
||||||
|
:disabled="duration > 0"
|
||||||
|
@click="emit('finish')"
|
||||||
|
class="w-min"
|
||||||
|
/>
|
||||||
|
<HoppButtonPrimary
|
||||||
|
v-else
|
||||||
|
label="Go to Dashboard"
|
||||||
|
@click="emit('finish')"
|
||||||
|
class="w-min"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<h1 class="text-2xl font-bold text-white">Onboarding Incomplete</h1>
|
||||||
|
<h2>
|
||||||
|
There was an issue completing the onboarding process. Please check the
|
||||||
|
configurations and try again.
|
||||||
|
</h2>
|
||||||
|
<HoppButtonPrimary
|
||||||
|
label="Retry Onboarding"
|
||||||
|
@click="emit('back')"
|
||||||
|
class="w-min"
|
||||||
|
/>
|
||||||
|
<p class="text-secondaryLight text-tiny mt-2">
|
||||||
|
If you need help, please refer to the
|
||||||
|
<a
|
||||||
|
href="https://docs.hoppscotch.io/documentation/self-host/community-edition"
|
||||||
|
target="_blank"
|
||||||
|
class="text-secondaryLight underline"
|
||||||
|
>documentation</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { HoppButtonPrimary } from '@hoppscotch/ui';
|
||||||
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { OnBoardingSummary } from '~/composables/useOnboardingConfigHandler';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
onBoardingSummary: OnBoardingSummary;
|
||||||
|
submittingConfigs: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'back'): void;
|
||||||
|
(e: 'finish'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const duration = ref(30);
|
||||||
|
const timer = ref<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const startCountdown = () => {
|
||||||
|
timer.value = setInterval(() => {
|
||||||
|
duration.value--;
|
||||||
|
if (duration.value === 0 && timer.value) {
|
||||||
|
clearInterval(timer.value);
|
||||||
|
|
||||||
|
// Redirect to dashboard after countdown
|
||||||
|
router.push({ name: 'index' });
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.onBoardingSummary.type === 'success') {
|
||||||
|
startCountdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (!timer.value) return;
|
||||||
|
clearInterval(timer.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
<template>
|
||||||
|
<div class="space-y-2 flex flex-col my-4">
|
||||||
|
<UiAccordion
|
||||||
|
v-for="[provider, value] in Object.entries(currentConfigs.oAuthProviders)"
|
||||||
|
:initial-open="enabledConfigs.includes(provider as EnabledConfig)"
|
||||||
|
>
|
||||||
|
<template v-slot:header="{ isOpen, toggleAccordion }">
|
||||||
|
<div class="flex items-center justify-between flex-1">
|
||||||
|
<span class="font-semibold text-[.8rem] capitalize">{{
|
||||||
|
provider.toLocaleLowerCase()
|
||||||
|
}}</span>
|
||||||
|
<span>
|
||||||
|
<HoppSmartToggle
|
||||||
|
:on="isOpen"
|
||||||
|
@change="() => {
|
||||||
|
toggleAccordion()
|
||||||
|
emit('toggleConfig', (provider as EnabledConfig));
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:content>
|
||||||
|
<div class="flex flex-col space-y-4 w-full flex-1 py-4">
|
||||||
|
<template v-for="(_, key) in value" :key="key">
|
||||||
|
<HoppSmartInput
|
||||||
|
v-if="isCallbackUrl(key as string)"
|
||||||
|
v-model="currentConfigs.oAuthProviders[provider as OAuthProvider][key]"
|
||||||
|
:label="(makeReadableKey(key as string,true))"
|
||||||
|
input-styles="floating-input !border-0"
|
||||||
|
:autofocus="false"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1 rounded border border-divider"
|
||||||
|
:disabled="true"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
#button
|
||||||
|
v-if="currentConfigs.oAuthProviders[provider as OAuthProvider][key]"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconLucideCopy"
|
||||||
|
:title="'Copy to clipboard'"
|
||||||
|
@click="() => copyCallbackUrl(currentConfigs.oAuthProviders[provider as OAuthProvider][key])"
|
||||||
|
class="hover:bg-transparent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartInput>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-else
|
||||||
|
v-model="currentConfigs.oAuthProviders[provider as OAuthProvider][key]"
|
||||||
|
:label="(makeReadableKey(key as string,true))"
|
||||||
|
input-styles="floating-input"
|
||||||
|
:autofocus="false"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UiAccordion>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { HoppSmartItem } from '@hoppscotch/ui';
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { useI18n } from '~/composables/i18n';
|
||||||
|
import { useToast } from '~/composables/toast';
|
||||||
|
import {
|
||||||
|
Configs,
|
||||||
|
EnabledConfig,
|
||||||
|
OAuthProvider,
|
||||||
|
} from '~/composables/useOnboardingConfigHandler';
|
||||||
|
import { copyToClipboard } from '~/helpers/utils/clipboard';
|
||||||
|
import { makeReadableKey } from '~/helpers/utils/readableKey';
|
||||||
|
import IconLucideCopy from '~icons/lucide/copy';
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
currentConfigs: Configs;
|
||||||
|
enabledConfigs: EnabledConfig[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'toggleConfig', provider: EnabledConfig): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const currentConfigs = useVModel(props, 'currentConfigs');
|
||||||
|
|
||||||
|
// check if the key is a callback URL
|
||||||
|
const isCallbackUrl = (key: string): boolean => {
|
||||||
|
return key.toLowerCase().includes('callback');
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyCallbackUrl = (callbackURL: string): void => {
|
||||||
|
if (!callbackURL) return;
|
||||||
|
copyToClipboard(callbackURL);
|
||||||
|
|
||||||
|
toast.success(`${t('state.copied_to_clipboard')}`);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
<template>
|
||||||
|
<div class="my-4">
|
||||||
|
<!-- Basic SMTP Fields -->
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="currentConfigs.mailerConfigs[smtp.ADDRESS_FROM.id]"
|
||||||
|
:label="smtp.ADDRESS_FROM.text"
|
||||||
|
input-styles="floating-input"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
/>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-if="smtp.SMTP_URL.enabled"
|
||||||
|
v-model="currentConfigs.mailerConfigs[smtp.SMTP_URL.id]"
|
||||||
|
:label="smtp.SMTP_URL.text"
|
||||||
|
input-styles="floating-input"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Config Fields -->
|
||||||
|
<div v-if="smtp.USE_CUSTOM_CONFIGS.enabled" class="flex flex-col space-y-2">
|
||||||
|
<HoppSmartInput
|
||||||
|
v-for="key in ['SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASSWORD'] as AllMailerConfigKeys[]"
|
||||||
|
:key="key"
|
||||||
|
v-model="currentConfigs.mailerConfigs[smtp[key].id]"
|
||||||
|
:label="smtp[key].text"
|
||||||
|
input-styles="floating-input"
|
||||||
|
:type="key === 'SMTP_PASSWORD' ? 'password' : 'text'"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Custom Config Checkboxes -->
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<HoppSmartCheckbox
|
||||||
|
:on="smtp.SMTP_SECURE.enabled"
|
||||||
|
@change="toggleConfig('SMTP_SECURE')"
|
||||||
|
>
|
||||||
|
{{ smtp.SMTP_SECURE.text }}
|
||||||
|
</HoppSmartCheckbox>
|
||||||
|
|
||||||
|
<HoppSmartCheckbox
|
||||||
|
:on="smtp.TLS_REJECT_UNAUTHORIZED.enabled"
|
||||||
|
@change="toggleConfig('TLS_REJECT_UNAUTHORIZED')"
|
||||||
|
>
|
||||||
|
{{ smtp.TLS_REJECT_UNAUTHORIZED.text }}
|
||||||
|
</HoppSmartCheckbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle: Use Custom Configs -->
|
||||||
|
<HoppSmartCheckbox
|
||||||
|
class="mt-4"
|
||||||
|
:on="smtp.USE_CUSTOM_CONFIGS.enabled"
|
||||||
|
@change="toggleConfig('USE_CUSTOM_CONFIGS')"
|
||||||
|
>
|
||||||
|
{{ smtp.USE_CUSTOM_CONFIGS.text }}
|
||||||
|
</HoppSmartCheckbox>
|
||||||
|
<p class="text-secondaryLight text-tiny mt-2">
|
||||||
|
Enable to configure your own SMTP credentials
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import {
|
||||||
|
Configs,
|
||||||
|
EnabledConfig,
|
||||||
|
MailerConfigKeys,
|
||||||
|
} from '~/composables/useOnboardingConfigHandler';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
currentConfigs: Configs;
|
||||||
|
enabledConfigs: EnabledConfig[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const currentConfigs = useVModel(props, 'currentConfigs');
|
||||||
|
|
||||||
|
type ConfigKey = keyof Configs['mailerConfigs'];
|
||||||
|
type ConfigField = {
|
||||||
|
id: ConfigKey;
|
||||||
|
text: string;
|
||||||
|
value: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AllMailerConfigKeys = Exclude<MailerConfigKeys, 'SMTP_ENABLE'>;
|
||||||
|
|
||||||
|
type MailerConfig = Record<AllMailerConfigKeys, ConfigField>;
|
||||||
|
|
||||||
|
const smtp = computed<MailerConfig>(() => {
|
||||||
|
const cfg = currentConfigs.value.mailerConfigs;
|
||||||
|
const isCustom = cfg.MAILER_USE_CUSTOM_CONFIGS === 'true';
|
||||||
|
|
||||||
|
return {
|
||||||
|
SMTP_URL: {
|
||||||
|
id: 'MAILER_SMTP_URL',
|
||||||
|
text: 'SMTP URL',
|
||||||
|
value: cfg.MAILER_SMTP_URL,
|
||||||
|
enabled: !isCustom,
|
||||||
|
},
|
||||||
|
ADDRESS_FROM: {
|
||||||
|
id: 'MAILER_ADDRESS_FROM',
|
||||||
|
text: 'Address From',
|
||||||
|
value: cfg.MAILER_ADDRESS_FROM,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
USE_CUSTOM_CONFIGS: {
|
||||||
|
id: 'MAILER_USE_CUSTOM_CONFIGS',
|
||||||
|
text: 'Use Custom Configs',
|
||||||
|
value: cfg.MAILER_USE_CUSTOM_CONFIGS,
|
||||||
|
enabled: isCustom,
|
||||||
|
},
|
||||||
|
SMTP_SECURE: {
|
||||||
|
id: 'MAILER_SMTP_SECURE',
|
||||||
|
text: 'SMTP Secure',
|
||||||
|
value: cfg.MAILER_SMTP_SECURE,
|
||||||
|
enabled: isCustom && cfg.MAILER_SMTP_SECURE === 'true',
|
||||||
|
},
|
||||||
|
TLS_REJECT_UNAUTHORIZED: {
|
||||||
|
id: 'MAILER_TLS_REJECT_UNAUTHORIZED',
|
||||||
|
text: 'TLS Reject Unauthorized',
|
||||||
|
value: cfg.MAILER_TLS_REJECT_UNAUTHORIZED,
|
||||||
|
enabled: isCustom && cfg.MAILER_TLS_REJECT_UNAUTHORIZED === 'true',
|
||||||
|
},
|
||||||
|
SMTP_USER: {
|
||||||
|
id: 'MAILER_SMTP_USER',
|
||||||
|
text: 'SMTP User',
|
||||||
|
value: cfg.MAILER_SMTP_USER,
|
||||||
|
enabled: isCustom,
|
||||||
|
},
|
||||||
|
SMTP_PASSWORD: {
|
||||||
|
id: 'MAILER_SMTP_PASSWORD',
|
||||||
|
text: 'SMTP Password',
|
||||||
|
value: cfg.MAILER_SMTP_PASSWORD,
|
||||||
|
enabled: isCustom,
|
||||||
|
},
|
||||||
|
SMTP_HOST: {
|
||||||
|
id: 'MAILER_SMTP_HOST',
|
||||||
|
text: 'SMTP Host',
|
||||||
|
value: cfg.MAILER_SMTP_HOST,
|
||||||
|
enabled: isCustom,
|
||||||
|
},
|
||||||
|
SMTP_PORT: {
|
||||||
|
id: 'MAILER_SMTP_PORT',
|
||||||
|
text: 'SMTP Port',
|
||||||
|
value: cfg.MAILER_SMTP_PORT,
|
||||||
|
enabled: isCustom,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleConfig = (key: AllMailerConfigKeys) => {
|
||||||
|
const id = smtp.value[key].id;
|
||||||
|
const current = currentConfigs.value.mailerConfigs[id];
|
||||||
|
currentConfigs.value.mailerConfigs[id] =
|
||||||
|
current === 'true' ? 'false' : 'true';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-1 justify-center items-center">
|
||||||
|
<div class="max-w-screen-md mx-auto p-8 flex flex-col space-y-4">
|
||||||
|
<div class="flex flex-col space-y-2 mb-4">
|
||||||
|
<img src="/logo.svg" alt="hoppscotch-logo" class="w-20 mb-4" />
|
||||||
|
<h1 class="text-3xl font-bold text-secondaryDark">Welcome</h1>
|
||||||
|
<h2 class="mt-3">
|
||||||
|
This is the onboarding process for setting up your Hoppscotch
|
||||||
|
instance.
|
||||||
|
</h2>
|
||||||
|
<p class="text-secondaryLight">
|
||||||
|
Please set up either SMTP or OAuth for authentication.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<HoppButtonPrimary label="Start Onboarding" @click="$emit('next')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -61,6 +61,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
|
|
||||||
|
<HoppSmartTab id="token" :label="t('configs.auth_providers.token.title')">
|
||||||
|
<div class="pb-8 px-4">
|
||||||
|
<SettingsAuthToken v-model:config="workingConfigs" />
|
||||||
|
</div>
|
||||||
|
</HoppSmartTab>
|
||||||
</HoppSmartTabs>
|
</HoppSmartTabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -82,7 +88,7 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Auth Sub Tabs
|
// Auth Sub Tabs
|
||||||
type AuthSubTabs = 'auth-providers' | 'email-auth';
|
type AuthSubTabs = 'auth-providers' | 'email-auth' | 'token';
|
||||||
const selectedAuthSubTab = ref<AuthSubTabs>('auth-providers');
|
const selectedAuthSubTab = ref<AuthSubTabs>('auth-providers');
|
||||||
|
|
||||||
const workingConfigs = useVModel(props, 'config', emit);
|
const workingConfigs = useVModel(props, 'config', emit);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="authTokenConfig" class="grid md:grid-cols-3 gap-4 md:gap-4 pt-8">
|
||||||
|
<div class="md:col-span-1">
|
||||||
|
<h3 class="heading">{{ t('configs.auth_providers.token.title') }}</h3>
|
||||||
|
<p class="my-1 text-secondaryLight">
|
||||||
|
{{ t('configs.auth_providers.token.description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-8 sm:px-8 md:col-span-2">
|
||||||
|
<section>
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
blank
|
||||||
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
|
to="https://docs.hoppscotch.io/documentation/self-host/community-edition/prerequisites#email-delivery"
|
||||||
|
:title="t('support.documentation')"
|
||||||
|
:icon="IconHelpCircle"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4 pt-10 pb-4">
|
||||||
|
<div class="">
|
||||||
|
<div class="max-w-xs flex flex-col space-y-4">
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<label>{{
|
||||||
|
t('configs.auth_providers.token.jwt_secret')
|
||||||
|
}}</label>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="authTokenConfig.fields.jwt_secret"
|
||||||
|
placeholder="e.g., your-secret-key"
|
||||||
|
:autofocus="false"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1 border border-divider rounded"
|
||||||
|
input-styles="!border-0 "
|
||||||
|
:type="isMasked('jwt_secret') ? 'password' : 'text'"
|
||||||
|
>
|
||||||
|
<template #button>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:icon="isMasked('jwt_secret') ? IconEye : IconEyeOff"
|
||||||
|
class="bg-primaryLight rounded"
|
||||||
|
@click="toggleMask('jwt_secret')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartInput>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<label>{{
|
||||||
|
t('configs.auth_providers.token.token_salt_complexity')
|
||||||
|
}}</label>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="authTokenConfig.fields.token_salt_complexity"
|
||||||
|
placeholder="e.g., 10 (salt complexity)"
|
||||||
|
:autofocus="false"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
type="number"
|
||||||
|
@update:model-value="
|
||||||
|
validateNumberValue(
|
||||||
|
authTokenConfig.fields.token_salt_complexity
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<label>{{
|
||||||
|
t('configs.auth_providers.token.magic_link_token_validity')
|
||||||
|
}}</label>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="authTokenConfig.fields.magic_link_token_validity"
|
||||||
|
placeholder="e.g., 3 (in hour)"
|
||||||
|
:autofocus="false"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
type="number"
|
||||||
|
@update:model-value="
|
||||||
|
validateNumberValue(
|
||||||
|
authTokenConfig.fields.magic_link_token_validity
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<label>{{
|
||||||
|
t('configs.auth_providers.token.refresh_token_validity')
|
||||||
|
}}</label>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="authTokenConfig.fields.refresh_token_validity"
|
||||||
|
placeholder="e.g., 604800000 (in milliseconds)"
|
||||||
|
:autofocus="false"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
type="number"
|
||||||
|
@update:model-value="
|
||||||
|
validateNumberValue(
|
||||||
|
authTokenConfig.fields.refresh_token_validity
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<label>{{
|
||||||
|
t('configs.auth_providers.token.access_token_validity')
|
||||||
|
}}</label>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="authTokenConfig.fields.access_token_validity"
|
||||||
|
placeholder="e.g., 86400000 (in milliseconds)"
|
||||||
|
:autofocus="false"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
type="number"
|
||||||
|
@update:model-value="
|
||||||
|
validateNumberValue(
|
||||||
|
authTokenConfig.fields.access_token_validity
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<label>{{
|
||||||
|
t('configs.auth_providers.token.session_secret')
|
||||||
|
}}</label>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="authTokenConfig.fields.session_secret"
|
||||||
|
placeholder="e.g., your-session-secret"
|
||||||
|
:autofocus="false"
|
||||||
|
input-styles="!border-0 "
|
||||||
|
class="!my-2 !bg-primaryLight flex-1 border border-divider rounded"
|
||||||
|
:type="isMasked('session_secret') ? 'password' : 'text'"
|
||||||
|
>
|
||||||
|
<template #button>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:icon="isMasked('session_secret') ? IconEye : IconEyeOff"
|
||||||
|
class="bg-primaryLight rounded"
|
||||||
|
@click="toggleMask('session_secret')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from '~/composables/i18n';
|
||||||
|
import { ServerConfigs } from '~/helpers/configs';
|
||||||
|
import IconHelpCircle from '~icons/lucide/help-circle';
|
||||||
|
import IconEye from '~icons/lucide/eye';
|
||||||
|
import IconEyeOff from '~icons/lucide/eye-off';
|
||||||
|
import { useToast } from '~/composables/toast';
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: ServerConfigs;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:config', v: ServerConfigs): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const workingConfigs = useVModel(props, 'config', emit);
|
||||||
|
|
||||||
|
// Get or set token from workingConfigs
|
||||||
|
const authTokenConfig = computed({
|
||||||
|
get: () => workingConfigs.value?.tokenConfigs,
|
||||||
|
set: (value) => (workingConfigs.value.tokenConfigs = value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const maskState = ref<Record<string, boolean>>({
|
||||||
|
jwt_secret: true,
|
||||||
|
session_secret: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleMask = (fieldKey: string) => {
|
||||||
|
maskState.value[fieldKey] = !maskState.value[fieldKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMasked = (fieldKey: string) => maskState.value[fieldKey];
|
||||||
|
|
||||||
|
const validateNumberValue = (value: string | number) => {
|
||||||
|
const num = typeof value === 'string' ? parseInt(value, 10) : value;
|
||||||
|
if (isNaN(num) || num <= 0) {
|
||||||
|
toast.error(t('configs.invalid_number'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
/* Chrome, Safari, Edge, Opera */
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
input[type='number'] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
appearance: textfield;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
<template
|
<template
|
||||||
v-if="field.applicableProviders.includes(provider.name)"
|
v-if="field.applicableProviders.includes(provider.name)"
|
||||||
>
|
>
|
||||||
<label>{{ field.name }}</label>
|
<label>{{ makeReadableKey(field.name, true) }}</label>
|
||||||
<span class="flex max-w-lg">
|
<span class="flex max-w-lg">
|
||||||
<HoppSmartInput
|
<HoppSmartInput
|
||||||
v-model="provider.fields[field.key as keyof typeof provider['fields']]"
|
v-model="provider.fields[field.key as keyof typeof provider['fields']]"
|
||||||
|
|
@ -51,15 +51,21 @@
|
||||||
isMasked(provider.name, field.key) ? 'password' : 'text'
|
isMasked(provider.name, field.key) ? 'password' : 'text'
|
||||||
"
|
"
|
||||||
:autofocus="false"
|
:autofocus="false"
|
||||||
class="!my-2 !bg-primaryLight flex-1"
|
class="!my-2 !bg-primaryLight flex-1 border border-divider rounded"
|
||||||
/>
|
input-styles="!border-0"
|
||||||
<HoppButtonSecondary
|
>
|
||||||
:icon="
|
<template #button>
|
||||||
isMasked(provider.name, field.key) ? IconEye : IconEyeOff
|
<HoppButtonSecondary
|
||||||
"
|
:icon="
|
||||||
class="bg-primaryLight h-9 mt-2"
|
isMasked(provider.name, field.key)
|
||||||
@click="toggleMask(provider.name, field.key)"
|
? IconEye
|
||||||
/>
|
: IconEyeOff
|
||||||
|
"
|
||||||
|
class="bg-primaryLight rounded"
|
||||||
|
@click="toggleMask(provider.name, field.key)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartInput>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -75,6 +81,7 @@ import { useVModel } from '@vueuse/core';
|
||||||
import { reactive } from 'vue';
|
import { reactive } from 'vue';
|
||||||
import { useI18n } from '~/composables/i18n';
|
import { useI18n } from '~/composables/i18n';
|
||||||
import { ServerConfigs, SsoAuthProviders } from '~/helpers/configs';
|
import { ServerConfigs, SsoAuthProviders } from '~/helpers/configs';
|
||||||
|
import { makeReadableKey } from '~/helpers/utils/readableKey';
|
||||||
import IconCircleHelp from '~icons/lucide/circle-help';
|
import IconCircleHelp from '~icons/lucide/circle-help';
|
||||||
import IconEye from '~icons/lucide/eye';
|
import IconEye from '~icons/lucide/eye';
|
||||||
import IconEyeOff from '~icons/lucide/eye-off';
|
import IconEyeOff from '~icons/lucide/eye-off';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="rateLimitConfig" class="grid md:grid-cols-3 gap-4 md:gap-4 pt-8">
|
||||||
|
<div class="md:col-span-1 px-4">
|
||||||
|
<h3 class="heading">{{ t('configs.rate_limit.title') }}</h3>
|
||||||
|
<p class="my-1 text-secondaryLight">
|
||||||
|
{{ t('configs.rate_limit.description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-8 sm:px-8 md:col-span-2">
|
||||||
|
<section>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h4 class="font-semibold text-secondaryDark">
|
||||||
|
{{ t('configs.rate_limit.title') }}
|
||||||
|
</h4>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
blank
|
||||||
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
|
to="https://docs.hoppscotch.io/documentation/self-host/community-edition/prerequisites#email-delivery"
|
||||||
|
:title="t('support.documentation')"
|
||||||
|
:icon="IconHelpCircle"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4 py-4">
|
||||||
|
<div class="">
|
||||||
|
<div class="max-w-xs flex flex-col space-y-4">
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<label>{{ t('configs.rate_limit.rate_limit_ttl') }}</label>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="rateLimitConfig.fields.rate_limit_ttl"
|
||||||
|
placeholder="e.g., 60 (in seconds)"
|
||||||
|
:autofocus="false"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
type="number"
|
||||||
|
@update:model-value="
|
||||||
|
validateNumberValue(rateLimitConfig.fields.rate_limit_ttl)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<label>{{ t('configs.rate_limit.rate_limit_max') }}</label>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="rateLimitConfig.fields.rate_limit_max"
|
||||||
|
placeholder="e.g., 100 (requests per TTL)"
|
||||||
|
:autofocus="false"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
type="number"
|
||||||
|
@update:model-value="
|
||||||
|
validateNumberValue(rateLimitConfig.fields.rate_limit_max)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from '~/composables/i18n';
|
||||||
|
import { useToast } from '~/composables/toast';
|
||||||
|
import { ServerConfigs } from '~/helpers/configs';
|
||||||
|
import IconHelpCircle from '~icons/lucide/help-circle';
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: ServerConfigs;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:config', v: ServerConfigs): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const workingConfigs = useVModel(props, 'config', emit);
|
||||||
|
|
||||||
|
// Get or set rate limit from workingConfigs
|
||||||
|
const rateLimitConfig = computed({
|
||||||
|
get: () => workingConfigs.value?.rateLimitConfigs,
|
||||||
|
set: (value) => (workingConfigs.value.rateLimitConfigs = value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const validateNumberValue = (value: string | number) => {
|
||||||
|
const num = typeof value === 'string' ? parseInt(value, 10) : value;
|
||||||
|
if (isNaN(num) || num <= 0) {
|
||||||
|
toast.error(t('configs.invalid_number'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
/* Chrome, Safari, Edge, Opera */
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
input[type='number'] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
appearance: textfield;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -68,6 +68,8 @@ const {
|
||||||
updateDataSharingConfigs,
|
updateDataSharingConfigs,
|
||||||
toggleSMTPConfigs,
|
toggleSMTPConfigs,
|
||||||
toggleUserHistoryStore,
|
toggleUserHistoryStore,
|
||||||
|
updateRateLimitConfigs,
|
||||||
|
updateAuthTokenConfigs,
|
||||||
} = useConfigHandler(props.workingConfigs);
|
} = useConfigHandler(props.workingConfigs);
|
||||||
|
|
||||||
// Call relevant mutations on component mount and initiate server restart
|
// Call relevant mutations on component mount and initiate server restart
|
||||||
|
|
@ -134,6 +136,22 @@ onMounted(async () => {
|
||||||
if (!userHistoryStoreResult) {
|
if (!userHistoryStoreResult) {
|
||||||
return triggerComponentUnMount();
|
return triggerComponentUnMount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rateLimitResult = await updateRateLimitConfigs(
|
||||||
|
updateInfraConfigsMutation
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!rateLimitResult) {
|
||||||
|
return triggerComponentUnMount();
|
||||||
|
}
|
||||||
|
|
||||||
|
const authTokenResult = await updateAuthTokenConfigs(
|
||||||
|
updateInfraConfigsMutation
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!authTokenResult) {
|
||||||
|
return triggerComponentUnMount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
restart.value = true;
|
restart.value = true;
|
||||||
|
|
|
||||||
|
|
@ -60,23 +60,27 @@
|
||||||
:on="Boolean(smtpConfigs.fields[field.key])"
|
:on="Boolean(smtpConfigs.fields[field.key])"
|
||||||
@change="toggleCheckbox(field)"
|
@change="toggleCheckbox(field)"
|
||||||
>
|
>
|
||||||
{{ field.name }}
|
{{ makeReadableKey(field.name, true) }}
|
||||||
</HoppSmartCheckbox>
|
</HoppSmartCheckbox>
|
||||||
</div>
|
</div>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<label>{{ field.name }}</label>
|
<label>{{ makeReadableKey(field.name, true) }}</label>
|
||||||
<span class="flex max-w-lg">
|
<span class="flex max-w-lg">
|
||||||
<HoppSmartInput
|
<HoppSmartInput
|
||||||
v-model="smtpConfigs.fields[field.key]"
|
v-model="smtpConfigs.fields[field.key]"
|
||||||
:type="isMasked(field.key) ? 'password' : 'text'"
|
:type="isMasked(field.key) ? 'password' : 'text'"
|
||||||
:autofocus="false"
|
:autofocus="false"
|
||||||
class="!my-2 !bg-primaryLight flex-1"
|
class="!my-2 !bg-primaryLight flex-1 border border-divider rounded"
|
||||||
/>
|
input-styles="!border-0"
|
||||||
<HoppButtonSecondary
|
>
|
||||||
:icon="isMasked(field.key) ? IconEye : IconEyeOff"
|
<template #button>
|
||||||
class="bg-primaryLight h-9 mt-2"
|
<HoppButtonSecondary
|
||||||
@click="toggleMask(field.key)"
|
:icon="isMasked(field.key) ? IconEye : IconEyeOff"
|
||||||
/>
|
class="bg-primaryLight rounded"
|
||||||
|
@click="toggleMask(field.key)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartInput>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -103,6 +107,7 @@ import { useVModel } from '@vueuse/core';
|
||||||
import { computed, reactive, watch } from 'vue';
|
import { computed, reactive, watch } from 'vue';
|
||||||
import { useI18n } from '~/composables/i18n';
|
import { useI18n } from '~/composables/i18n';
|
||||||
import { hasInputValidationFailed, ServerConfigs } from '~/helpers/configs';
|
import { hasInputValidationFailed, ServerConfigs } from '~/helpers/configs';
|
||||||
|
import { makeReadableKey } from '~/helpers/utils/readableKey';
|
||||||
import IconEye from '~icons/lucide/eye';
|
import IconEye from '~icons/lucide/eye';
|
||||||
import IconEyeOff from '~icons/lucide/eye-off';
|
import IconEyeOff from '~icons/lucide/eye-off';
|
||||||
import IconHelpCircle from '~icons/lucide/help-circle';
|
import IconHelpCircle from '~icons/lucide/help-circle';
|
||||||
|
|
|
||||||
43
packages/hoppscotch-sh-admin/src/components/ui/Accordion.vue
Normal file
43
packages/hoppscotch-sh-admin/src/components/ui/Accordion.vue
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="flex items-center space-x-3"
|
||||||
|
:aria-expanded="isOpen"
|
||||||
|
:aria-controls="UID"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
name="header"
|
||||||
|
:is-open="isOpen"
|
||||||
|
:toggle-accordion="toggleAccordion"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="isOpen" :aria-labelledby="UID" :id="UID">
|
||||||
|
<slot name="content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const UID = 'accordion-' + Math.random().toString(36).substr(2, 9);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
initialOpen?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isOpen = ref(false);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialOpen,
|
||||||
|
(newVal) => {
|
||||||
|
isOpen.value = newVal ?? false;
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleAccordion = () => {
|
||||||
|
isOpen.value = !isOpen.value;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -136,6 +136,25 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||||
getFieldValue(InfraConfigEnum.MailerUseCustomConfigs) === 'true',
|
getFieldValue(InfraConfigEnum.MailerUseCustomConfigs) === 'true',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
tokenConfigs: {
|
||||||
|
name: 'token',
|
||||||
|
fields: {
|
||||||
|
jwt_secret: getFieldValue(InfraConfigEnum.JwtSecret),
|
||||||
|
token_salt_complexity: getFieldValue(
|
||||||
|
InfraConfigEnum.TokenSaltComplexity
|
||||||
|
),
|
||||||
|
magic_link_token_validity: getFieldValue(
|
||||||
|
InfraConfigEnum.MagicLinkTokenValidity
|
||||||
|
),
|
||||||
|
refresh_token_validity: getFieldValue(
|
||||||
|
InfraConfigEnum.RefreshTokenValidity
|
||||||
|
),
|
||||||
|
access_token_validity: getFieldValue(
|
||||||
|
InfraConfigEnum.AccessTokenValidity
|
||||||
|
),
|
||||||
|
session_secret: getFieldValue(InfraConfigEnum.SessionSecret),
|
||||||
|
},
|
||||||
|
},
|
||||||
dataSharingConfigs: {
|
dataSharingConfigs: {
|
||||||
name: 'data_sharing',
|
name: 'data_sharing',
|
||||||
enabled: !!infraConfigs.value.find(
|
enabled: !!infraConfigs.value.find(
|
||||||
|
|
@ -150,6 +169,13 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||||
config.value === 'ENABLE'
|
config.value === 'ENABLE'
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
rateLimitConfigs: {
|
||||||
|
name: 'rate_limit',
|
||||||
|
fields: {
|
||||||
|
rate_limit_ttl: getFieldValue(InfraConfigEnum.RateLimitTtl),
|
||||||
|
rate_limit_max: getFieldValue(InfraConfigEnum.RateLimitMax),
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cloning the current configs to working configs
|
// Cloning the current configs to working configs
|
||||||
|
|
@ -165,18 +191,39 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||||
Check if any of the config fields are empty
|
Check if any of the config fields are empty
|
||||||
*/
|
*/
|
||||||
const isFieldEmpty = (field: string | boolean) => {
|
const isFieldEmpty = (field: string | boolean) => {
|
||||||
if (typeof field === 'boolean') {
|
if (typeof field === 'boolean' || typeof field === 'number') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return field.trim() === '';
|
return field.trim() === '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the field is not valid
|
||||||
|
* This is used to validate number fields, ensuring they are not NaN or less than or equal to zero.
|
||||||
|
* @param field Field value to validate
|
||||||
|
* @returns Boolean indicating if the field is valid
|
||||||
|
*/
|
||||||
|
const isFieldNotValid = (field: string | boolean) => {
|
||||||
|
if (typeof field === 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(field);
|
||||||
|
if (isNaN(num) && typeof field === 'string') {
|
||||||
|
return field.trim() === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return num <= 0;
|
||||||
|
};
|
||||||
|
|
||||||
const AreAnyConfigFieldsEmpty = (config: ServerConfigs): boolean => {
|
const AreAnyConfigFieldsEmpty = (config: ServerConfigs): boolean => {
|
||||||
const sections: Array<ConfigSection> = [
|
const sections: Array<ConfigSection> = [
|
||||||
config.providers.github,
|
config.providers.github,
|
||||||
config.providers.google,
|
config.providers.google,
|
||||||
config.providers.microsoft,
|
config.providers.microsoft,
|
||||||
config.mailConfigs,
|
config.mailConfigs,
|
||||||
|
config.rateLimitConfigs,
|
||||||
|
config.tokenConfigs,
|
||||||
];
|
];
|
||||||
|
|
||||||
const hasSectionWithEmptyFields = sections.some((section) => {
|
const hasSectionWithEmptyFields = sections.some((section) => {
|
||||||
|
|
@ -197,6 +244,11 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This section has no enabled property, so we check fields directly
|
||||||
|
// eg: tokenConfigs, rateLimitConfigs
|
||||||
|
if (!('enabled' in section))
|
||||||
|
Object.values(section.fields).some(isFieldNotValid);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
section.enabled && Object.values(section.fields).some(isFieldEmpty)
|
section.enabled && Object.values(section.fields).some(isFieldEmpty)
|
||||||
);
|
);
|
||||||
|
|
@ -407,6 +459,118 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||||
'configs.user_history_store.toggle_failure'
|
'configs.user_history_store.toggle_failure'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateRateLimitConfigs = (
|
||||||
|
updateRateLimitMutation: UseMutationResponse<UpdateInfraConfigsMutation>
|
||||||
|
) => {
|
||||||
|
if (!updatedConfigs?.rateLimitConfigs) {
|
||||||
|
toast.error(t('configs.rate_limit.input_validation_error'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rateLimitTtl = String(
|
||||||
|
updatedConfigs?.rateLimitConfigs.fields.rate_limit_ttl
|
||||||
|
);
|
||||||
|
const rateLimitMax = String(
|
||||||
|
updatedConfigs?.rateLimitConfigs.fields.rate_limit_max
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isFieldEmpty(rateLimitTtl) || isFieldEmpty(rateLimitMax)) {
|
||||||
|
toast.error(t('configs.rate_limit.input_validation_error'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rateLimitConfigs: InfraConfigArgs[] = [
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.RateLimitTtl,
|
||||||
|
value: String(rateLimitTtl),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.RateLimitMax,
|
||||||
|
value: String(rateLimitMax),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return executeMutation(
|
||||||
|
updateRateLimitMutation,
|
||||||
|
{
|
||||||
|
infraConfigs: rateLimitConfigs,
|
||||||
|
},
|
||||||
|
'configs.rate_limit.update_failure'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAuthTokenConfigs = (
|
||||||
|
updateAuthTokenMutation: UseMutationResponse<UpdateInfraConfigsMutation>
|
||||||
|
) => {
|
||||||
|
if (!updatedConfigs?.tokenConfigs) {
|
||||||
|
toast.error(t('configs.auth_providers.token.update_failure'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jwtSecret = String(updatedConfigs?.tokenConfigs.fields.jwt_secret);
|
||||||
|
const tokenSaltComplexity = String(
|
||||||
|
updatedConfigs?.tokenConfigs.fields.token_salt_complexity
|
||||||
|
);
|
||||||
|
const magicLinkTokenValidity = String(
|
||||||
|
updatedConfigs?.tokenConfigs.fields.magic_link_token_validity
|
||||||
|
);
|
||||||
|
const refreshTokenValidity = String(
|
||||||
|
updatedConfigs?.tokenConfigs.fields.refresh_token_validity
|
||||||
|
);
|
||||||
|
const accessTokenValidity = String(
|
||||||
|
updatedConfigs?.tokenConfigs.fields.access_token_validity
|
||||||
|
);
|
||||||
|
const sessionSecret = String(
|
||||||
|
updatedConfigs?.tokenConfigs.fields.session_secret
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
isFieldEmpty(jwtSecret) ||
|
||||||
|
isFieldEmpty(tokenSaltComplexity) ||
|
||||||
|
isFieldEmpty(magicLinkTokenValidity) ||
|
||||||
|
isFieldEmpty(refreshTokenValidity) ||
|
||||||
|
isFieldEmpty(accessTokenValidity) ||
|
||||||
|
isFieldEmpty(sessionSecret)
|
||||||
|
) {
|
||||||
|
toast.error(t('configs.auth_providers.token.update_failure'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authTokenConfigs: InfraConfigArgs[] = [
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.JwtSecret,
|
||||||
|
value: jwtSecret,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.TokenSaltComplexity,
|
||||||
|
value: tokenSaltComplexity,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MagicLinkTokenValidity,
|
||||||
|
value: magicLinkTokenValidity,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.RefreshTokenValidity,
|
||||||
|
value: refreshTokenValidity,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.AccessTokenValidity,
|
||||||
|
value: accessTokenValidity,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.SessionSecret,
|
||||||
|
value: sessionSecret,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return executeMutation(
|
||||||
|
updateAuthTokenMutation,
|
||||||
|
{
|
||||||
|
infraConfigs: authTokenConfigs,
|
||||||
|
},
|
||||||
|
'configs.auth_providers.token.update_failure'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentConfigs,
|
currentConfigs,
|
||||||
workingConfigs,
|
workingConfigs,
|
||||||
|
|
@ -414,6 +578,8 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
|
||||||
updateDataSharingConfigs,
|
updateDataSharingConfigs,
|
||||||
toggleSMTPConfigs,
|
toggleSMTPConfigs,
|
||||||
toggleUserHistoryStore,
|
toggleUserHistoryStore,
|
||||||
|
updateRateLimitConfigs,
|
||||||
|
updateAuthTokenConfigs,
|
||||||
updateInfraConfigs,
|
updateInfraConfigs,
|
||||||
resetInfraConfigs,
|
resetInfraConfigs,
|
||||||
fetchingInfraConfigs,
|
fetchingInfraConfigs,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,376 @@
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from './i18n';
|
||||||
|
import { useToast } from './toast';
|
||||||
|
import { auth } from '~/helpers/auth';
|
||||||
|
import { InfraConfigEnum } from '~/helpers/backend/graphql';
|
||||||
|
import { getLocalConfig, setLocalConfig } from '~/helpers/localpersistence';
|
||||||
|
import { makeReadableKey } from '~/helpers/utils/readableKey';
|
||||||
|
|
||||||
|
export type OAuthProvider = 'GOOGLE' | 'GITHUB' | 'MICROSOFT';
|
||||||
|
export type EnabledConfig = OAuthProvider | 'MAILER' | 'EMAIL';
|
||||||
|
|
||||||
|
// common OAuth keys used across providers
|
||||||
|
type OAuthKeys = 'CLIENT_ID' | 'CLIENT_SECRET' | 'CALLBACK_URL' | 'SCOPE';
|
||||||
|
|
||||||
|
// Microsoft specific keys
|
||||||
|
type MicrosoftKeys = OAuthKeys | 'TENANT';
|
||||||
|
|
||||||
|
type OAuthConfig<Keys extends string, Prefix extends string> = {
|
||||||
|
[K in Keys as `${Prefix}_${K}`]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mailer specific keys
|
||||||
|
export type MailerConfigKeys =
|
||||||
|
| 'SMTP_ENABLE'
|
||||||
|
| 'USE_CUSTOM_CONFIGS'
|
||||||
|
| 'SMTP_URL'
|
||||||
|
| 'ADDRESS_FROM'
|
||||||
|
| 'SMTP_HOST'
|
||||||
|
| 'SMTP_PORT'
|
||||||
|
| 'SMTP_SECURE'
|
||||||
|
| 'SMTP_USER'
|
||||||
|
| 'SMTP_PASSWORD'
|
||||||
|
| 'TLS_REJECT_UNAUTHORIZED';
|
||||||
|
|
||||||
|
export type Configs = {
|
||||||
|
oAuthProviders: {
|
||||||
|
GOOGLE: OAuthConfig<OAuthKeys, 'GOOGLE'>;
|
||||||
|
GITHUB: OAuthConfig<OAuthKeys, 'GITHUB'>;
|
||||||
|
MICROSOFT: OAuthConfig<MicrosoftKeys, 'MICROSOFT'>;
|
||||||
|
};
|
||||||
|
mailerConfigs: {
|
||||||
|
[K in `MAILER_${MailerConfigKeys}`]: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnBoardingSummary = {
|
||||||
|
type: 'success' | 'error';
|
||||||
|
message: string;
|
||||||
|
description: string;
|
||||||
|
configsAdded: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapOAuthProviders(
|
||||||
|
configs: Partial<Record<InfraConfigEnum, string>>
|
||||||
|
): Configs['oAuthProviders'] {
|
||||||
|
return {
|
||||||
|
GOOGLE: {
|
||||||
|
GOOGLE_CLIENT_ID: configs.GOOGLE_CLIENT_ID ?? '',
|
||||||
|
GOOGLE_CLIENT_SECRET: configs.GOOGLE_CLIENT_SECRET ?? '',
|
||||||
|
GOOGLE_CALLBACK_URL: '',
|
||||||
|
GOOGLE_SCOPE: configs.GOOGLE_SCOPE ?? '',
|
||||||
|
},
|
||||||
|
GITHUB: {
|
||||||
|
GITHUB_CLIENT_ID: configs.GITHUB_CLIENT_ID ?? '',
|
||||||
|
GITHUB_CLIENT_SECRET: configs.GITHUB_CLIENT_SECRET ?? '',
|
||||||
|
GITHUB_CALLBACK_URL: '',
|
||||||
|
GITHUB_SCOPE: configs.GITHUB_SCOPE ?? '',
|
||||||
|
},
|
||||||
|
MICROSOFT: {
|
||||||
|
MICROSOFT_CLIENT_ID: configs.MICROSOFT_CLIENT_ID ?? '',
|
||||||
|
MICROSOFT_CLIENT_SECRET: configs.MICROSOFT_CLIENT_SECRET ?? '',
|
||||||
|
MICROSOFT_CALLBACK_URL: '',
|
||||||
|
MICROSOFT_SCOPE: configs.MICROSOFT_SCOPE ?? '',
|
||||||
|
MICROSOFT_TENANT: configs.MICROSOFT_TENANT ?? '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapMailerConfigs(
|
||||||
|
configs: Partial<Record<InfraConfigEnum, string>>
|
||||||
|
): Configs['mailerConfigs'] {
|
||||||
|
return {
|
||||||
|
MAILER_SMTP_ENABLE: configs.MAILER_SMTP_ENABLE ?? '',
|
||||||
|
MAILER_USE_CUSTOM_CONFIGS: configs.MAILER_USE_CUSTOM_CONFIGS || 'false',
|
||||||
|
MAILER_SMTP_URL: configs.MAILER_SMTP_URL ?? '',
|
||||||
|
MAILER_ADDRESS_FROM: configs.MAILER_ADDRESS_FROM ?? '',
|
||||||
|
MAILER_SMTP_HOST: configs.MAILER_SMTP_HOST ?? '',
|
||||||
|
MAILER_SMTP_PORT: configs.MAILER_SMTP_PORT ?? '',
|
||||||
|
MAILER_SMTP_SECURE: configs.MAILER_SMTP_SECURE || 'false',
|
||||||
|
MAILER_SMTP_USER: configs.MAILER_SMTP_USER ?? '',
|
||||||
|
MAILER_SMTP_PASSWORD: configs.MAILER_SMTP_PASSWORD ?? '',
|
||||||
|
MAILER_TLS_REJECT_UNAUTHORIZED:
|
||||||
|
configs.MAILER_TLS_REJECT_UNAUTHORIZED || 'false',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The handler for onboarding configuration.
|
||||||
|
* This composable manages the state and logic for onboarding configurations,
|
||||||
|
* including enabling/disabling configs, validating inputs,
|
||||||
|
* and submitting the onboarding form.
|
||||||
|
* @returns Composable for handling onboarding configuration.
|
||||||
|
*/
|
||||||
|
export function useOnboardingConfigHandler() {
|
||||||
|
const t = useI18n();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const enabledConfigs = ref<EnabledConfig[]>([]);
|
||||||
|
const isProvidersLoading = ref(false);
|
||||||
|
const submittingConfigs = ref(false);
|
||||||
|
|
||||||
|
const onBoardingSummary = ref<OnBoardingSummary>({
|
||||||
|
type: 'success',
|
||||||
|
message: t('onboarding.addConfigsSuccess'),
|
||||||
|
description: t('onboarding.addConfigsDescription'),
|
||||||
|
configsAdded: [] as string[],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentConfigs = ref<Configs>({
|
||||||
|
oAuthProviders: mapOAuthProviders({}),
|
||||||
|
mailerConfigs: mapMailerConfigs({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const enableConfig = (config: EnabledConfig) => {
|
||||||
|
if (!enabledConfigs.value.includes(config)) {
|
||||||
|
enabledConfigs.value.push(config);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleConfig = (key: EnabledConfig | 'OAUTH' | 'EMAIL') => {
|
||||||
|
if (key === 'OAUTH') {
|
||||||
|
enabledConfigs.value = enabledConfigs.value.filter(
|
||||||
|
(c) => !['GOOGLE', 'GITHUB', 'MICROSOFT'].includes(c)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'EMAIL') {
|
||||||
|
const hasEmail = enabledConfigs.value.includes('EMAIL');
|
||||||
|
const hasMailer = enabledConfigs.value.includes('MAILER');
|
||||||
|
enabledConfigs.value = enabledConfigs.value.filter(
|
||||||
|
(c) => c !== 'EMAIL' && c !== 'MAILER'
|
||||||
|
);
|
||||||
|
if (!hasEmail || !hasMailer) {
|
||||||
|
enabledConfigs.value.push('EMAIL', 'MAILER');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabledConfigs.value.includes(key)) {
|
||||||
|
enabledConfigs.value = enabledConfigs.value.filter((c) => c !== key);
|
||||||
|
} else {
|
||||||
|
enableConfig(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSmtpConfig = () => {
|
||||||
|
const current = currentConfigs.value.mailerConfigs.MAILER_SMTP_ENABLE;
|
||||||
|
currentConfigs.value.mailerConfigs.MAILER_SMTP_ENABLE =
|
||||||
|
current === 'true' ? 'false' : 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set callback URLs for OAuth providers based on the current backend API URL
|
||||||
|
const setCallbackUrls = () => {
|
||||||
|
const base = import.meta.env.VITE_BACKEND_API_URL;
|
||||||
|
const oAuth = currentConfigs.value.oAuthProviders;
|
||||||
|
|
||||||
|
if (oAuth.GOOGLE.GOOGLE_CLIENT_ID) {
|
||||||
|
oAuth.GOOGLE.GOOGLE_CALLBACK_URL = `${base}/auth/google/callback`;
|
||||||
|
}
|
||||||
|
if (oAuth.GITHUB.GITHUB_CLIENT_ID) {
|
||||||
|
oAuth.GITHUB.GITHUB_CALLBACK_URL = `${base}/auth/github/callback`;
|
||||||
|
}
|
||||||
|
if (oAuth.MICROSOFT.MICROSOFT_CLIENT_ID) {
|
||||||
|
oAuth.MICROSOFT.MICROSOFT_CALLBACK_URL = `${base}/auth/microsoft/callback`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeOnboardingSummary = (error?: Error): OnBoardingSummary => {
|
||||||
|
const addedConfigs = enabledConfigs.value;
|
||||||
|
|
||||||
|
if (addedConfigs.length === 0) {
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
message: t('onboarding.addConfigsError'),
|
||||||
|
description: t('onboarding.addConfigsDescription', {
|
||||||
|
error: error?.message || t('onboarding.addConfigsDefaultError'),
|
||||||
|
}),
|
||||||
|
configsAdded: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'success',
|
||||||
|
message: t('onboarding.addConfigsSuccess'),
|
||||||
|
description: t('onboarding.addConfigsDescription'),
|
||||||
|
configsAdded: addedConfigs.filter((key) => key !== 'MAILER'),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out unnecessary configs based on the current state.
|
||||||
|
* For example, if MAILER_USE_CUSTOM_CONFIGS is false,
|
||||||
|
* we don't need MAILER_SMTP_URL, MAILER_TLS_REJECT_UNAUTHORIZED, MAILER_SMTP_SECURE.
|
||||||
|
* @param keys Array of config keys to filter
|
||||||
|
* @returns Filtered array of keys that are needed based on the current state
|
||||||
|
*/
|
||||||
|
const filterNeededConfigs = (keys: string[]) => {
|
||||||
|
const mailer = currentConfigs.value.mailerConfigs;
|
||||||
|
const usingCustom = mailer.MAILER_USE_CUSTOM_CONFIGS === 'true';
|
||||||
|
|
||||||
|
return keys.filter((key) => {
|
||||||
|
if (!key.startsWith('MAILER_')) return true;
|
||||||
|
if (!enabledConfigs.value.includes('MAILER')) return false;
|
||||||
|
|
||||||
|
if (!usingCustom) {
|
||||||
|
return ['MAILER_SMTP_URL', 'MAILER_ADDRESS_FROM'].includes(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'MAILER_SMTP_HOST',
|
||||||
|
'MAILER_SMTP_PORT',
|
||||||
|
'MAILER_SMTP_USER',
|
||||||
|
'MAILER_SMTP_PASSWORD',
|
||||||
|
].includes(key);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the provided configs.
|
||||||
|
* Checks if all required fields are filled and returns a filtered object
|
||||||
|
* with only the enabled configs that have values.
|
||||||
|
* @param configs Object containing config key-value pairs
|
||||||
|
* @returns Filtered object with valid configs or undefined if validation fails
|
||||||
|
*/
|
||||||
|
const validateConfigs = (configs: Partial<Record<string, string>>) => {
|
||||||
|
if (!configs || Object.keys(configs).length === 0) {
|
||||||
|
toast.error(t('onboarding.addConfigsError'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relevantKeys = Object.keys(configs).filter((key) =>
|
||||||
|
enabledConfigs.value.includes(key.split('_')[0] as EnabledConfig)
|
||||||
|
);
|
||||||
|
|
||||||
|
const neededKeys = filterNeededConfigs(relevantKeys);
|
||||||
|
const allFilled = neededKeys.every((key) => configs[key]);
|
||||||
|
|
||||||
|
if (!allFilled) {
|
||||||
|
neededKeys.forEach((key) => {
|
||||||
|
if (!configs[key])
|
||||||
|
toast.error(
|
||||||
|
`Please fill the required field: ${makeReadableKey(key)}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(configs).filter(
|
||||||
|
([key, val]) =>
|
||||||
|
enabledConfigs.value.includes(key.split('_')[0] as EnabledConfig) &&
|
||||||
|
val
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the onboarding configs to the backend.
|
||||||
|
* It validates the configs, prepares the payload,
|
||||||
|
* and sends it to the backend API.
|
||||||
|
* We set the token in localStorage for re-fetching configs later.
|
||||||
|
* @returns The token for re-fetching configs or undefined if failed
|
||||||
|
*/
|
||||||
|
const addOnBoardingConfigs = async () => {
|
||||||
|
submittingConfigs.value = true;
|
||||||
|
const payload = {
|
||||||
|
...currentConfigs.value.oAuthProviders.GOOGLE,
|
||||||
|
...currentConfigs.value.oAuthProviders.GITHUB,
|
||||||
|
...currentConfigs.value.oAuthProviders.MICROSOFT,
|
||||||
|
...currentConfigs.value.mailerConfigs,
|
||||||
|
};
|
||||||
|
|
||||||
|
const validated = validateConfigs(payload);
|
||||||
|
|
||||||
|
if (!validated || Object.keys(validated).length === 0) {
|
||||||
|
toast.error('Please add at least one config');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configWithAuth = {
|
||||||
|
...validated,
|
||||||
|
[InfraConfigEnum.ViteAllowedAuthProviders]: enabledConfigs.value
|
||||||
|
.filter((x) => x !== 'MAILER')
|
||||||
|
.join(','),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await auth.addOnBoardingConfigs(configWithAuth);
|
||||||
|
if (res?.token) {
|
||||||
|
setLocalConfig('access_token', res.token);
|
||||||
|
toast.success('Onboarding configs added successfully');
|
||||||
|
onBoardingSummary.value = makeOnboardingSummary();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to add onboarding configs', err);
|
||||||
|
toast.error('Failed to add onboarding configs');
|
||||||
|
onBoardingSummary.value = makeOnboardingSummary(err as Error);
|
||||||
|
} finally {
|
||||||
|
submittingConfigs.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch onboarding configs on mount and populate the currentConfigs
|
||||||
|
// and enabledConfigs based on the response.
|
||||||
|
// This is used to pre-fill the form with existing configs.
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
isProvidersLoading.value = true;
|
||||||
|
const token = getLocalConfig('access_token');
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
const configs = await auth.getOnboardingConfigs(token);
|
||||||
|
if (!configs) return;
|
||||||
|
|
||||||
|
const allowed = configs[InfraConfigEnum.ViteAllowedAuthProviders];
|
||||||
|
if (allowed) {
|
||||||
|
enabledConfigs.value = allowed.split(',') as EnabledConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
currentConfigs.value = {
|
||||||
|
oAuthProviders: mapOAuthProviders(configs),
|
||||||
|
mailerConfigs: mapMailerConfigs(configs),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching onboarding configs', err);
|
||||||
|
} finally {
|
||||||
|
isProvidersLoading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes in currentConfigs and update the callback URLs
|
||||||
|
// and enable/disable configs based on the SMTP settings.
|
||||||
|
// This ensures that the form reflects the current state of the configs.
|
||||||
|
watch(
|
||||||
|
currentConfigs,
|
||||||
|
() => {
|
||||||
|
setCallbackUrls();
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentConfigs.value.mailerConfigs.MAILER_SMTP_ENABLE?.toLowerCase() ===
|
||||||
|
'true'
|
||||||
|
) {
|
||||||
|
// Enable MAILER and EMAIL configs if SMTP is enabled
|
||||||
|
// because we need to add EMAIL in VITE_ALLOWED_AUTH_PROVIDERS
|
||||||
|
// and MAILER because the key is used in backend
|
||||||
|
enableConfig('MAILER');
|
||||||
|
enableConfig('EMAIL');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentConfigs,
|
||||||
|
enabledConfigs,
|
||||||
|
isProvidersLoading,
|
||||||
|
onBoardingSummary,
|
||||||
|
submittingConfigs,
|
||||||
|
toggleConfig,
|
||||||
|
toggleSmtpConfig,
|
||||||
|
enabledConfig: enableConfig,
|
||||||
|
addOnBoardingConfigs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,11 @@ export const authEvents$ = new Subject<
|
||||||
AuthEvent | { event: 'token_refresh' }
|
AuthEvent | { event: 'token_refresh' }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
|
export type OnboardingStatus = {
|
||||||
|
canReRunOnboarding: boolean;
|
||||||
|
onboardingCompleted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const currentUser$ = new BehaviorSubject<HoppUser | null>(null);
|
const currentUser$ = new BehaviorSubject<HoppUser | null>(null);
|
||||||
|
|
||||||
const signOut = async (reloadWindow = false) => {
|
const signOut = async (reloadWindow = false) => {
|
||||||
|
|
@ -252,4 +257,36 @@ export const auth = {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getOnboardingStatus: async (): Promise<OnboardingStatus | null> => {
|
||||||
|
try {
|
||||||
|
const res = await authQuery.getOnboardingStatus();
|
||||||
|
return res.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addOnBoardingConfigs: async (config: Record<string, any>) => {
|
||||||
|
try {
|
||||||
|
const res = await authQuery.addOnBoardingConfigs(config);
|
||||||
|
return res.data as {
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getOnboardingConfigs: async (token: string) => {
|
||||||
|
try {
|
||||||
|
const res = await authQuery.getOnBoardingConfigs(token);
|
||||||
|
return res.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,5 +31,10 @@ export default {
|
||||||
}),
|
}),
|
||||||
getFirstTimeInfraSetupStatus: () => restApi.get('/site/setup'),
|
getFirstTimeInfraSetupStatus: () => restApi.get('/site/setup'),
|
||||||
updateFirstTimeInfraSetupStatus: () => restApi.put('/site/setup'),
|
updateFirstTimeInfraSetupStatus: () => restApi.put('/site/setup'),
|
||||||
|
addOnBoardingConfigs: (config: Record<string, any>) =>
|
||||||
|
restApi.post('/onboarding/config', config),
|
||||||
|
getOnboardingStatus: () => restApi.get('/onboarding/status'),
|
||||||
|
getOnBoardingConfigs: (token: string) =>
|
||||||
|
restApi.get('/onboarding/config?token=' + token),
|
||||||
logout: () => restApi.get('/auth/logout'),
|
logout: () => restApi.get('/auth/logout'),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,18 @@ export type ServerConfigs = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tokenConfigs: {
|
||||||
|
name: string;
|
||||||
|
fields: {
|
||||||
|
jwt_secret: string;
|
||||||
|
token_salt_complexity: string;
|
||||||
|
magic_link_token_validity: string;
|
||||||
|
refresh_token_validity: string;
|
||||||
|
access_token_validity: string;
|
||||||
|
session_secret: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
historyConfig: {
|
historyConfig: {
|
||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
@ -67,6 +79,14 @@ export type ServerConfigs = {
|
||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
rateLimitConfigs: {
|
||||||
|
name: string;
|
||||||
|
fields: {
|
||||||
|
rate_limit_ttl: string;
|
||||||
|
rate_limit_max: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdatedConfigs = {
|
export type UpdatedConfigs = {
|
||||||
|
|
@ -82,7 +102,7 @@ export type ConfigTransform = {
|
||||||
|
|
||||||
export type ConfigSection = {
|
export type ConfigSection = {
|
||||||
name: SsoAuthProviders | string;
|
name: SsoAuthProviders | string;
|
||||||
enabled: boolean;
|
enabled?: boolean;
|
||||||
fields: Record<string, string | boolean>;
|
fields: Record<string, string | boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -211,6 +231,44 @@ export const HISTORY_STORE_CONFIG: Config[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const RATE_LIMIT_CONFIGS: Config[] = [
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.RateLimitTtl,
|
||||||
|
key: 'rate_limit_ttl',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.RateLimitMax,
|
||||||
|
key: 'rate_limit_max',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TOKEN_VALIDATION_CONFIGS: Config[] = [
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.JwtSecret,
|
||||||
|
key: 'jwt_secret',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.SessionSecret,
|
||||||
|
key: 'session_secret',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.TokenSaltComplexity,
|
||||||
|
key: 'token_salt_complexity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MagicLinkTokenValidity,
|
||||||
|
key: 'magic_link_token_validity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.RefreshTokenValidity,
|
||||||
|
key: 'refresh_token_validity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.AccessTokenValidity,
|
||||||
|
key: 'access_token_validity',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const ALL_CONFIGS = [
|
export const ALL_CONFIGS = [
|
||||||
GOOGLE_CONFIGS,
|
GOOGLE_CONFIGS,
|
||||||
MICROSOFT_CONFIGS,
|
MICROSOFT_CONFIGS,
|
||||||
|
|
@ -219,4 +277,6 @@ export const ALL_CONFIGS = [
|
||||||
CUSTOM_MAIL_CONFIGS,
|
CUSTOM_MAIL_CONFIGS,
|
||||||
DATA_SHARING_CONFIGS,
|
DATA_SHARING_CONFIGS,
|
||||||
HISTORY_STORE_CONFIG,
|
HISTORY_STORE_CONFIG,
|
||||||
|
RATE_LIMIT_CONFIGS,
|
||||||
|
TOKEN_VALIDATION_CONFIGS,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* The makeReadableKey function formats a string to be more human-readable.
|
||||||
|
* It replaces underscores with spaces, converts it to lowercase,
|
||||||
|
* and capitalizes the first letter.
|
||||||
|
* @param string The string to be formatted
|
||||||
|
* @returns A human-readable version of the string
|
||||||
|
*/
|
||||||
|
export const makeReadableKey = (string: string, capitalizeAll?: boolean) => {
|
||||||
|
if (!string) return '';
|
||||||
|
const val = string.replace(/_/g, ' ').toLocaleLowerCase();
|
||||||
|
|
||||||
|
if (capitalizeAll) {
|
||||||
|
return val
|
||||||
|
.split(' ')
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(val).charAt(0).toUpperCase() + String(val).slice(1);
|
||||||
|
};
|
||||||
|
|
@ -4,7 +4,8 @@ import { HoppModule } from '.';
|
||||||
|
|
||||||
const isSetupRoute = (to: unknown) => to === 'setup';
|
const isSetupRoute = (to: unknown) => to === 'setup';
|
||||||
|
|
||||||
const isGuestRoute = (to: unknown) => ['index', 'enter'].includes(to as string);
|
const isGuestRoute = (to: unknown) =>
|
||||||
|
['index', 'enter', 'onboarding'].includes(to as string);
|
||||||
|
|
||||||
const getFirstTimeInfraSetupStatus = async () => {
|
const getFirstTimeInfraSetupStatus = async () => {
|
||||||
const isInfraNotSetup = await auth.getFirstTimeInfraSetupStatus();
|
const isInfraNotSetup = await auth.getFirstTimeInfraSetupStatus();
|
||||||
|
|
@ -23,9 +24,20 @@ const getFirstTimeInfraSetupStatus = async () => {
|
||||||
* @param {function} next
|
* @param {function} next
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default <HoppModule>{
|
export default <HoppModule>{
|
||||||
async onBeforeRouteChange(to, _from, next) {
|
async onBeforeRouteChange(to, _from, next) {
|
||||||
|
// Check if onboarding is completed
|
||||||
|
const onboardingStatus = await auth.getOnboardingStatus();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!onboardingStatus?.onboardingCompleted &&
|
||||||
|
to.name !== 'onboarding' &&
|
||||||
|
to.name === 'index'
|
||||||
|
) {
|
||||||
|
// If onboarding is not completed, redirect to the onboarding page
|
||||||
|
return next({ name: 'onboarding' });
|
||||||
|
}
|
||||||
|
|
||||||
const res = await auth.getUserDetails();
|
const res = await auth.getUserDetails();
|
||||||
|
|
||||||
// Allow performing the silent refresh flow for an invalid access token state
|
// Allow performing the silent refresh flow for an invalid access token state
|
||||||
|
|
@ -35,6 +47,15 @@ export default <HoppModule>{
|
||||||
|
|
||||||
const isAdmin = res.data?.me.isAdmin;
|
const isAdmin = res.data?.me.isAdmin;
|
||||||
|
|
||||||
|
if (
|
||||||
|
onboardingStatus?.onboardingCompleted &&
|
||||||
|
!onboardingStatus.canReRunOnboarding &&
|
||||||
|
to.name === 'onboarding'
|
||||||
|
) {
|
||||||
|
// If onboarding is completed, redirect to the dashboard
|
||||||
|
return next({ name: 'index' });
|
||||||
|
}
|
||||||
|
|
||||||
// Route Guards
|
// Route Guards
|
||||||
if (!isGuestRoute(to.name) && !isAdmin) {
|
if (!isGuestRoute(to.name) && !isAdmin) {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
135
packages/hoppscotch-sh-admin/src/pages/onboarding.vue
Normal file
135
packages/hoppscotch-sh-admin/src/pages/onboarding.vue
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
<template>
|
||||||
|
<main class="flex h-screen flex-col items-center justify-center">
|
||||||
|
<div
|
||||||
|
class="fixed top-0 left-0 p-5 flex items-center justify-between w-full"
|
||||||
|
>
|
||||||
|
<span class="text-md font-bold">HOPPSCOTCH</span>
|
||||||
|
<HoppButtonPrimary
|
||||||
|
v-if="!isFirstTimeSetup"
|
||||||
|
label="Go to Dashboard"
|
||||||
|
@click="goToDashboard"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isLoading" class="flex flex-1 justify-center items-center">
|
||||||
|
<HoppSmartSpinner />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<WelcomeScreen v-if="step === STEP.WELCOME" @next="nextStep" />
|
||||||
|
|
||||||
|
<AuthSetup
|
||||||
|
v-else-if="step === STEP.AUTH"
|
||||||
|
:is-first-time-setup="isFirstTimeSetup"
|
||||||
|
@complete-onboarding="(payload:SuccessPayload) => finishOnboarding(payload)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CompleteOnboarding
|
||||||
|
v-else
|
||||||
|
:on-boarding-summary="onBoardingSummary"
|
||||||
|
:submitting-configs="submittingConfigs"
|
||||||
|
@back="prevStep"
|
||||||
|
@finish="goToDashboard"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { HoppButtonPrimary } from '@hoppscotch/ui';
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import AuthSetup from '~/components/onboarding/AuthSetup.vue';
|
||||||
|
import CompleteOnboarding from '~/components/onboarding/CompleteScreen.vue';
|
||||||
|
import WelcomeScreen from '~/components/onboarding/WelcomeScreen.vue';
|
||||||
|
import { OnBoardingSummary } from '~/composables/useOnboardingConfigHandler';
|
||||||
|
import { auth } from '~/helpers/auth';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Steps
|
||||||
|
enum STEP {
|
||||||
|
WELCOME = 1,
|
||||||
|
AUTH = 2,
|
||||||
|
COMPLETE = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
type SuccessPayload = {
|
||||||
|
submittingConfigs: boolean;
|
||||||
|
summary: OnBoardingSummary;
|
||||||
|
};
|
||||||
|
|
||||||
|
const step = ref<STEP>(STEP.WELCOME);
|
||||||
|
const isFirstTimeSetup = ref(true);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
const onBoardingSummary = ref<OnBoardingSummary>({
|
||||||
|
type: 'success',
|
||||||
|
configsAdded: [],
|
||||||
|
description: '',
|
||||||
|
message: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const submittingConfigs = ref(false);
|
||||||
|
|
||||||
|
// Sync from query param
|
||||||
|
const syncStepFromRoute = () => {
|
||||||
|
const queryStep = parseInt(
|
||||||
|
router.currentRoute.value.query.step as string,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!isNaN(queryStep) &&
|
||||||
|
queryStep >= STEP.WELCOME &&
|
||||||
|
queryStep <= STEP.COMPLETE
|
||||||
|
) {
|
||||||
|
step.value = queryStep;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToDashboard = () => {
|
||||||
|
router.push({ name: 'dashboard' });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch route changes (e.g., browser back/forward)
|
||||||
|
watch(
|
||||||
|
() => router.currentRoute.value.query.step,
|
||||||
|
() => {
|
||||||
|
if (isFirstTimeSetup.value) syncStepFromRoute();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Push to URL when step changes
|
||||||
|
watch(step, (newStep) => {
|
||||||
|
router.replace({ name: 'onboarding', query: { step: newStep.toString() } });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load onboarding status
|
||||||
|
onMounted(async () => {
|
||||||
|
const onboardingStatus = await auth.getOnboardingStatus();
|
||||||
|
isFirstTimeSetup.value = !onboardingStatus?.onboardingCompleted;
|
||||||
|
isLoading.value = false;
|
||||||
|
|
||||||
|
if (!isFirstTimeSetup.value) {
|
||||||
|
step.value = STEP.AUTH;
|
||||||
|
} else {
|
||||||
|
syncStepFromRoute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step Controls
|
||||||
|
const nextStep = () => step.value++;
|
||||||
|
const prevStep = () => step.value--;
|
||||||
|
const finishOnboarding = (payload: {
|
||||||
|
submittingConfigs: boolean;
|
||||||
|
summary: OnBoardingSummary;
|
||||||
|
}) => {
|
||||||
|
step.value = STEP.COMPLETE;
|
||||||
|
onBoardingSummary.value = payload.summary;
|
||||||
|
submittingConfigs.value = payload.submittingConfigs;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
layout: empty
|
||||||
|
</route>
|
||||||
|
|
@ -31,6 +31,9 @@
|
||||||
<HoppSmartTab :id="'token'" :label="t('configs.tabs.infra_tokens')">
|
<HoppSmartTab :id="'token'" :label="t('configs.tabs.infra_tokens')">
|
||||||
<Tokens />
|
<Tokens />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
|
<HoppSmartTab :id="'rate-limit'" :label="t('configs.tabs.rate_limit')">
|
||||||
|
<SettingsRateLimit v-model:config="workingConfigs" />
|
||||||
|
</HoppSmartTab>
|
||||||
<HoppSmartTab id="miscellaneous" :label="t('configs.tabs.miscellaneous')">
|
<HoppSmartTab id="miscellaneous" :label="t('configs.tabs.miscellaneous')">
|
||||||
<div class="pb-8 px-4 flex flex-col space-y-8 divide-y divide-divider">
|
<div class="pb-8 px-4 flex flex-col space-y-8 divide-y divide-divider">
|
||||||
<SettingsDataSharing v-model:config="workingConfigs" />
|
<SettingsDataSharing v-model:config="workingConfigs" />
|
||||||
|
|
@ -76,7 +79,7 @@ const showSaveChangesModal = ref(false);
|
||||||
const initiateServerRestart = ref(false);
|
const initiateServerRestart = ref(false);
|
||||||
|
|
||||||
// Tabs
|
// Tabs
|
||||||
type OptionTabs = 'auth' | 'smtp' | 'token' | 'miscellaneous';
|
type OptionTabs = 'auth' | 'smtp' | 'token' | 'miscellaneous' | 'rate-limit';
|
||||||
const selectedOptionTab = ref<OptionTabs>('auth');
|
const selectedOptionTab = ref<OptionTabs>('auth');
|
||||||
|
|
||||||
// Obtain the current and working configs from the useConfigHandler composable
|
// Obtain the current and working configs from the useConfigHandler composable
|
||||||
|
|
|
||||||
933
pnpm-lock.yaml
933
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue