mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-13 16:22:59 +10:00
feat(feature-flags): fixes #1592, introduces new flags DISABLE_SIGNUPS and DISABLE_EMAIL_AUTH, renamed STORAGE_SKIP_BUCKET_CHECK
This commit is contained in:
@ -4,6 +4,8 @@ import { ArrowRight } from "@phosphor-icons/react";
|
||||
import { loginSchema } from "@reactive-resume/dto";
|
||||
import { usePasswordToggle } from "@reactive-resume/hooks";
|
||||
import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Button,
|
||||
Form,
|
||||
FormControl,
|
||||
@ -22,15 +24,13 @@ import { Link } from "react-router-dom";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useLogin } from "@/client/services/auth";
|
||||
import { useAuthProviders } from "@/client/services/auth/providers";
|
||||
import { useFeatureFlags } from "@/client/services/feature";
|
||||
|
||||
type FormValues = z.infer<typeof loginSchema>;
|
||||
|
||||
export const LoginPage = () => {
|
||||
const { login, loading } = useLogin();
|
||||
|
||||
const { providers } = useAuthProviders();
|
||||
const emailAuthDisabled = !providers?.includes("email");
|
||||
const { flags } = useFeatureFlags();
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
usePasswordToggle(formRef);
|
||||
@ -58,7 +58,7 @@ export const LoginPage = () => {
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">{t`Sign in to your account`}</h2>
|
||||
<h6 className={cn(emailAuthDisabled && "hidden")}>
|
||||
<h6>
|
||||
<span className="opacity-75">{t`Don't have an account?`}</span>
|
||||
<Button asChild variant="link" className="px-1.5">
|
||||
<Link to="/auth/register">
|
||||
@ -69,7 +69,13 @@ export const LoginPage = () => {
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div className={cn(emailAuthDisabled && "hidden")}>
|
||||
{flags.isEmailAuthDisabled && (
|
||||
<Alert variant="error">
|
||||
<AlertTitle>{t`Signing in via email is currently disabled by the administrator.`}</AlertTitle>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className={cn(flags.isEmailAuthDisabled && "pointer-events-none select-none blur-sm")}>
|
||||
<Form {...form}>
|
||||
<form
|
||||
ref={formRef}
|
||||
|
||||
@ -24,17 +24,14 @@ import { Link, useNavigate } from "react-router-dom";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useRegister } from "@/client/services/auth";
|
||||
import { useAuthProviders } from "@/client/services/auth/providers";
|
||||
import { useFeatureFlags } from "@/client/services/feature";
|
||||
|
||||
type FormValues = z.infer<typeof registerSchema>;
|
||||
|
||||
export const RegisterPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { flags } = useFeatureFlags();
|
||||
const { register, loading } = useRegister();
|
||||
const disableSignups = import.meta.env.VITE_DISABLE_SIGNUPS === "true";
|
||||
|
||||
const { providers } = useAuthProviders();
|
||||
const emailAuthDisabled = !providers?.includes("email");
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
usePasswordToggle(formRef);
|
||||
@ -70,7 +67,7 @@ export const RegisterPage = () => {
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">{t`Create a new account`}</h2>
|
||||
<h6 className={cn(emailAuthDisabled && "hidden")}>
|
||||
<h6>
|
||||
<span className="opacity-75">{t`Already have an account?`}</span>
|
||||
<Button asChild variant="link" className="px-1.5">
|
||||
<Link to="/auth/login">
|
||||
@ -80,18 +77,13 @@ export const RegisterPage = () => {
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
{disableSignups && (
|
||||
{flags.isSignupsDisabled && (
|
||||
<Alert variant="error">
|
||||
<AlertTitle>{t`Signups are currently disabled by the administrator.`}</AlertTitle>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
emailAuthDisabled && "hidden",
|
||||
disableSignups && "pointer-events-none blur-sm",
|
||||
)}
|
||||
>
|
||||
<div className={cn(flags.isSignupsDisabled && "pointer-events-none select-none blur-sm")}>
|
||||
<Form {...form}>
|
||||
<form
|
||||
ref={formRef}
|
||||
|
||||
28
apps/client/src/services/feature/flags.ts
Normal file
28
apps/client/src/services/feature/flags.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { FeatureDto } from "@reactive-resume/dto";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { axios } from "@/client/libs/axios";
|
||||
|
||||
export const fetchFeatureFlags = async () => {
|
||||
const response = await axios.get<FeatureDto>(`/feature/flags`);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useFeatureFlags = () => {
|
||||
const {
|
||||
error,
|
||||
isPending: loading,
|
||||
data: flags,
|
||||
} = useQuery({
|
||||
queryKey: ["feature_flags"],
|
||||
queryFn: () => fetchFeatureFlags(),
|
||||
refetchOnMount: "always",
|
||||
initialData: {
|
||||
isSignupsDisabled: false,
|
||||
isEmailAuthDisabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
return { flags, loading, error };
|
||||
};
|
||||
1
apps/client/src/services/feature/index.ts
Normal file
1
apps/client/src/services/feature/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./flags";
|
||||
7
apps/client/src/vite-env.d.ts
vendored
7
apps/client/src/vite-env.d.ts
vendored
@ -1,13 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const appVersion: string;
|
||||
|
||||
interface ImportMetaEnv {
|
||||
VITE_DISABLE_SIGNUPS: string | undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import { AuthModule } from "./auth/auth.module";
|
||||
import { ConfigModule } from "./config/config.module";
|
||||
import { ContributorsModule } from "./contributors/contributors.module";
|
||||
import { DatabaseModule } from "./database/database.module";
|
||||
import { FeatureModule } from "./feature/feature.module";
|
||||
import { HealthModule } from "./health/health.module";
|
||||
import { MailModule } from "./mail/mail.module";
|
||||
import { PrinterModule } from "./printer/printer.module";
|
||||
@ -33,6 +34,7 @@ import { UserModule } from "./user/user.module";
|
||||
ResumeModule,
|
||||
StorageModule,
|
||||
PrinterModule,
|
||||
FeatureModule,
|
||||
TranslationModule,
|
||||
ContributorsModule,
|
||||
|
||||
|
||||
@ -44,17 +44,21 @@ export const configSchema = z.object({
|
||||
.string()
|
||||
.default("false")
|
||||
.transform((s) => s !== "false" && s !== "0"),
|
||||
STORAGE_SKIP_BUCKET_CHECK: z
|
||||
.string()
|
||||
.default("false")
|
||||
.transform((s) => s !== "false" && s !== "0"),
|
||||
|
||||
// Crowdin (Optional)
|
||||
CROWDIN_PROJECT_ID: z.coerce.number().optional(),
|
||||
CROWDIN_PERSONAL_TOKEN: z.string().optional(),
|
||||
|
||||
// Flags (Optional)
|
||||
DISABLE_EMAIL_AUTH: z
|
||||
// Feature Flags (Optional)
|
||||
DISABLE_SIGNUPS: z
|
||||
.string()
|
||||
.default("false")
|
||||
.transform((s) => s !== "false" && s !== "0"),
|
||||
SKIP_STORAGE_BUCKET_CHECK: z
|
||||
DISABLE_EMAIL_AUTH: z
|
||||
.string()
|
||||
.default("false")
|
||||
.transform((s) => s !== "false" && s !== "0"),
|
||||
|
||||
@ -6,6 +6,7 @@ import { ContributorDto } from "@reactive-resume/dto";
|
||||
import { Config } from "../config/schema";
|
||||
|
||||
type GitHubResponse = { id: number; login: string; html_url: string; avatar_url: string }[];
|
||||
|
||||
type CrowdinContributorsResponse = {
|
||||
data: { data: { id: number; username: string; avatarUrl: string } }[];
|
||||
};
|
||||
|
||||
13
apps/server/src/feature/feature.controller.ts
Normal file
13
apps/server/src/feature/feature.controller.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
|
||||
import { FeatureService } from "./feature.service";
|
||||
|
||||
@Controller("feature")
|
||||
export class FeatureController {
|
||||
constructor(private readonly featureService: FeatureService) {}
|
||||
|
||||
@Get("/flags")
|
||||
getFeatureFlags() {
|
||||
return this.featureService.getFeatures();
|
||||
}
|
||||
}
|
||||
11
apps/server/src/feature/feature.module.ts
Normal file
11
apps/server/src/feature/feature.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { FeatureController } from "./feature.controller";
|
||||
import { FeatureService } from "./feature.service";
|
||||
|
||||
@Module({
|
||||
providers: [FeatureService],
|
||||
controllers: [FeatureController],
|
||||
exports: [FeatureService],
|
||||
})
|
||||
export class FeatureModule {}
|
||||
19
apps/server/src/feature/feature.service.ts
Normal file
19
apps/server/src/feature/feature.service.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
import { Config } from "../config/schema";
|
||||
|
||||
@Injectable()
|
||||
export class FeatureService {
|
||||
constructor(private readonly configService: ConfigService<Config>) {}
|
||||
|
||||
getFeatures() {
|
||||
const isSignupsDisabled = this.configService.getOrThrow<boolean>("DISABLE_SIGNUPS");
|
||||
const isEmailAuthDisabled = this.configService.getOrThrow<boolean>("DISABLE_EMAIL_AUTH");
|
||||
|
||||
return {
|
||||
isSignupsDisabled,
|
||||
isEmailAuthDisabled,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -49,14 +49,13 @@ export class StorageService implements OnModuleInit {
|
||||
this.client = this.minioService.client;
|
||||
this.bucketName = this.configService.getOrThrow<string>("STORAGE_BUCKET");
|
||||
|
||||
const skipBucketCheck = this.configService.getOrThrow<boolean>("SKIP_STORAGE_BUCKET_CHECK");
|
||||
const skipBucketCheck = this.configService.getOrThrow<boolean>("STORAGE_SKIP_BUCKET_CHECK");
|
||||
|
||||
if (skipBucketCheck) {
|
||||
this.logger.log("Skipping the verification of whether the storage bucket exists.");
|
||||
this.logger.warn("Make sure that the following paths are publicly accessible: ");
|
||||
this.logger.warn("- /pictures/*");
|
||||
this.logger.warn("- /previews/*");
|
||||
this.logger.warn("- /resumes/*");
|
||||
this.logger.warn("Skipping the verification of whether the storage bucket exists.");
|
||||
this.logger.warn(
|
||||
"Make sure that the following paths are publicly accessible: `/{pictures,previews,resumes}/*`",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user