Skip to content

Commit d18ef2e

Browse files
committed
feat(feature-flags): fixes AmruthPillai#1592, introduces new flags DISABLE_SIGNUPS and DISABLE_EMAIL_AUTH, renamed STORAGE_SKIP_BUCKET_CHECK
1 parent 1191bbc commit d18ef2e

23 files changed

+1701
-1370
lines changed

.env.example

+4-4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ STORAGE_BUCKET=default
4646
STORAGE_ACCESS_KEY=minioadmin
4747
STORAGE_SECRET_KEY=minioadmin
4848
STORAGE_USE_SSL=false
49+
STORAGE_SKIP_BUCKET_CHECK=false
4950

5051
# Nx Cloud (Optional)
5152
# NX_CLOUD_ACCESS_TOKEN=
@@ -54,10 +55,9 @@ STORAGE_USE_SSL=false
5455
# CROWDIN_PROJECT_ID=
5556
# CROWDIN_PERSONAL_TOKEN=
5657

57-
# Flags (Optional)
58-
# DISABLE_EMAIL_AUTH=true
59-
# VITE_DISABLE_SIGNUPS=false
60-
# SKIP_STORAGE_BUCKET_CHECK=false
58+
# Feature Flags (Optional)
59+
# DISABLE_SIGNUPS=false
60+
# DISABLE_EMAIL_AUTH=false
6161

6262
# GitHub (OAuth, Optional)
6363
# GITHUB_CLIENT_ID=

.ncurc.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
"upgrade": true,
44
"install": "always",
55
"packageManager": "pnpm",
6-
"reject": ["eslint", "@reactive-resume/*"]
6+
"reject": ["eslint", "eslint-plugin-unused-imports", "@reactive-resume/*"]
77
}

apps/client/src/pages/auth/login/page.tsx

+12-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { ArrowRight } from "@phosphor-icons/react";
44
import { loginSchema } from "@reactive-resume/dto";
55
import { usePasswordToggle } from "@reactive-resume/hooks";
66
import {
7+
Alert,
8+
AlertTitle,
79
Button,
810
Form,
911
FormControl,
@@ -22,15 +24,13 @@ import { Link } from "react-router-dom";
2224
import { z } from "zod";
2325

2426
import { useLogin } from "@/client/services/auth";
25-
import { useAuthProviders } from "@/client/services/auth/providers";
27+
import { useFeatureFlags } from "@/client/services/feature";
2628

2729
type FormValues = z.infer<typeof loginSchema>;
2830

2931
export const LoginPage = () => {
3032
const { login, loading } = useLogin();
31-
32-
const { providers } = useAuthProviders();
33-
const emailAuthDisabled = !providers?.includes("email");
33+
const { flags } = useFeatureFlags();
3434

3535
const formRef = useRef<HTMLFormElement>(null);
3636
usePasswordToggle(formRef);
@@ -58,7 +58,7 @@ export const LoginPage = () => {
5858

5959
<div className="space-y-1.5">
6060
<h2 className="text-2xl font-semibold tracking-tight">{t`Sign in to your account`}</h2>
61-
<h6 className={cn(emailAuthDisabled && "hidden")}>
61+
<h6>
6262
<span className="opacity-75">{t`Don't have an account?`}</span>
6363
<Button asChild variant="link" className="px-1.5">
6464
<Link to="/auth/register">
@@ -69,7 +69,13 @@ export const LoginPage = () => {
6969
</h6>
7070
</div>
7171

72-
<div className={cn(emailAuthDisabled && "hidden")}>
72+
{flags.isEmailAuthDisabled && (
73+
<Alert variant="error">
74+
<AlertTitle>{t`Signing in via email is currently disabled by the administrator.`}</AlertTitle>
75+
</Alert>
76+
)}
77+
78+
<div className={cn(flags.isEmailAuthDisabled && "pointer-events-none select-none blur-sm")}>
7379
<Form {...form}>
7480
<form
7581
ref={formRef}

apps/client/src/pages/auth/register/page.tsx

+5-13
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,14 @@ import { Link, useNavigate } from "react-router-dom";
2424
import { z } from "zod";
2525

2626
import { useRegister } from "@/client/services/auth";
27-
import { useAuthProviders } from "@/client/services/auth/providers";
27+
import { useFeatureFlags } from "@/client/services/feature";
2828

2929
type FormValues = z.infer<typeof registerSchema>;
3030

3131
export const RegisterPage = () => {
3232
const navigate = useNavigate();
33+
const { flags } = useFeatureFlags();
3334
const { register, loading } = useRegister();
34-
const disableSignups = import.meta.env.VITE_DISABLE_SIGNUPS === "true";
35-
36-
const { providers } = useAuthProviders();
37-
const emailAuthDisabled = !providers?.includes("email");
3835

3936
const formRef = useRef<HTMLFormElement>(null);
4037
usePasswordToggle(formRef);
@@ -70,7 +67,7 @@ export const RegisterPage = () => {
7067

7168
<div className="space-y-1.5">
7269
<h2 className="text-2xl font-semibold tracking-tight">{t`Create a new account`}</h2>
73-
<h6 className={cn(emailAuthDisabled && "hidden")}>
70+
<h6>
7471
<span className="opacity-75">{t`Already have an account?`}</span>
7572
<Button asChild variant="link" className="px-1.5">
7673
<Link to="/auth/login">
@@ -80,18 +77,13 @@ export const RegisterPage = () => {
8077
</h6>
8178
</div>
8279

83-
{disableSignups && (
80+
{flags.isSignupsDisabled && (
8481
<Alert variant="error">
8582
<AlertTitle>{t`Signups are currently disabled by the administrator.`}</AlertTitle>
8683
</Alert>
8784
)}
8885

89-
<div
90-
className={cn(
91-
emailAuthDisabled && "hidden",
92-
disableSignups && "pointer-events-none blur-sm",
93-
)}
94-
>
86+
<div className={cn(flags.isSignupsDisabled && "pointer-events-none select-none blur-sm")}>
9587
<Form {...form}>
9688
<form
9789
ref={formRef}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { FeatureDto } from "@reactive-resume/dto";
2+
import { useQuery } from "@tanstack/react-query";
3+
4+
import { axios } from "@/client/libs/axios";
5+
6+
export const fetchFeatureFlags = async () => {
7+
const response = await axios.get<FeatureDto>(`/feature/flags`);
8+
9+
return response.data;
10+
};
11+
12+
export const useFeatureFlags = () => {
13+
const {
14+
error,
15+
isPending: loading,
16+
data: flags,
17+
} = useQuery({
18+
queryKey: ["feature_flags"],
19+
queryFn: () => fetchFeatureFlags(),
20+
refetchOnMount: "always",
21+
initialData: {
22+
isSignupsDisabled: false,
23+
isEmailAuthDisabled: false,
24+
},
25+
});
26+
27+
return { flags, loading, error };
28+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./flags";

apps/client/src/vite-env.d.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
/* eslint-disable @typescript-eslint/consistent-type-definitions */
2-
31
/// <reference types="vite/client" />
42

53
declare const appVersion: string;
64

7-
interface ImportMetaEnv {
8-
VITE_DISABLE_SIGNUPS: string | undefined;
9-
}
10-
5+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
116
interface ImportMeta {
127
readonly env: ImportMetaEnv;
138
}

apps/server/src/app.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { AuthModule } from "./auth/auth.module";
1010
import { ConfigModule } from "./config/config.module";
1111
import { ContributorsModule } from "./contributors/contributors.module";
1212
import { DatabaseModule } from "./database/database.module";
13+
import { FeatureModule } from "./feature/feature.module";
1314
import { HealthModule } from "./health/health.module";
1415
import { MailModule } from "./mail/mail.module";
1516
import { PrinterModule } from "./printer/printer.module";
@@ -33,6 +34,7 @@ import { UserModule } from "./user/user.module";
3334
ResumeModule,
3435
StorageModule,
3536
PrinterModule,
37+
FeatureModule,
3638
TranslationModule,
3739
ContributorsModule,
3840

apps/server/src/config/schema.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,21 @@ export const configSchema = z.object({
4444
.string()
4545
.default("false")
4646
.transform((s) => s !== "false" && s !== "0"),
47+
STORAGE_SKIP_BUCKET_CHECK: z
48+
.string()
49+
.default("false")
50+
.transform((s) => s !== "false" && s !== "0"),
4751

4852
// Crowdin (Optional)
4953
CROWDIN_PROJECT_ID: z.coerce.number().optional(),
5054
CROWDIN_PERSONAL_TOKEN: z.string().optional(),
5155

52-
// Flags (Optional)
53-
DISABLE_EMAIL_AUTH: z
56+
// Feature Flags (Optional)
57+
DISABLE_SIGNUPS: z
5458
.string()
5559
.default("false")
5660
.transform((s) => s !== "false" && s !== "0"),
57-
SKIP_STORAGE_BUCKET_CHECK: z
61+
DISABLE_EMAIL_AUTH: z
5862
.string()
5963
.default("false")
6064
.transform((s) => s !== "false" && s !== "0"),

apps/server/src/contributors/contributors.service.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ContributorDto } from "@reactive-resume/dto";
66
import { Config } from "../config/schema";
77

88
type GitHubResponse = { id: number; login: string; html_url: string; avatar_url: string }[];
9+
910
type CrowdinContributorsResponse = {
1011
data: { data: { id: number; username: string; avatarUrl: string } }[];
1112
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Controller, Get } from "@nestjs/common";
2+
3+
import { FeatureService } from "./feature.service";
4+
5+
@Controller("feature")
6+
export class FeatureController {
7+
constructor(private readonly featureService: FeatureService) {}
8+
9+
@Get("/flags")
10+
getFeatureFlags() {
11+
return this.featureService.getFeatures();
12+
}
13+
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Module } from "@nestjs/common";
2+
3+
import { FeatureController } from "./feature.controller";
4+
import { FeatureService } from "./feature.service";
5+
6+
@Module({
7+
providers: [FeatureService],
8+
controllers: [FeatureController],
9+
exports: [FeatureService],
10+
})
11+
export class FeatureModule {}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Injectable } from "@nestjs/common";
2+
import { ConfigService } from "@nestjs/config";
3+
4+
import { Config } from "../config/schema";
5+
6+
@Injectable()
7+
export class FeatureService {
8+
constructor(private readonly configService: ConfigService<Config>) {}
9+
10+
getFeatures() {
11+
const isSignupsDisabled = this.configService.getOrThrow<boolean>("DISABLE_SIGNUPS");
12+
const isEmailAuthDisabled = this.configService.getOrThrow<boolean>("DISABLE_EMAIL_AUTH");
13+
14+
return {
15+
isSignupsDisabled,
16+
isEmailAuthDisabled,
17+
};
18+
}
19+
}

apps/server/src/storage/storage.service.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,13 @@ export class StorageService implements OnModuleInit {
4949
this.client = this.minioService.client;
5050
this.bucketName = this.configService.getOrThrow<string>("STORAGE_BUCKET");
5151

52-
const skipBucketCheck = this.configService.getOrThrow<boolean>("SKIP_STORAGE_BUCKET_CHECK");
52+
const skipBucketCheck = this.configService.getOrThrow<boolean>("STORAGE_SKIP_BUCKET_CHECK");
5353

5454
if (skipBucketCheck) {
55-
this.logger.log("Skipping the verification of whether the storage bucket exists.");
56-
this.logger.warn("Make sure that the following paths are publicly accessible: ");
57-
this.logger.warn("- /pictures/*");
58-
this.logger.warn("- /previews/*");
59-
this.logger.warn("- /resumes/*");
55+
this.logger.warn("Skipping the verification of whether the storage bucket exists.");
56+
this.logger.warn(
57+
"Make sure that the following paths are publicly accessible: `/{pictures,previews,resumes}/*`",
58+
);
6059

6160
return;
6261
}

libs/dto/src/feature/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createZodDto } from "nestjs-zod/dto";
2+
import { z } from "nestjs-zod/z";
3+
4+
export const featureSchema = z.object({
5+
isSignupsDisabled: z.boolean().default(false),
6+
isEmailAuthDisabled: z.boolean().default(false),
7+
});
8+
9+
export class FeatureDto extends createZodDto(featureSchema) {}

libs/dto/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
// @index('./*', f => `export * from "${f.path}";`)
12
export * from "./auth";
23
export * from "./contributors";
4+
export * from "./feature";
35
export * from "./resume";
46
export * from "./secrets";
57
export * from "./statistics";

0 commit comments

Comments
 (0)