startx 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +4 -0
- package/apps/cli/src/commands/index.ts +1 -1
- package/apps/cli/src/commands/{common → test}/test.ts +4 -2
- package/apps/cli/tsconfig.json +0 -1
- package/apps/core-server/Dockerfile +5 -4
- package/apps/core-server/package.json +1 -1
- package/apps/core-server/tsconfig.json +1 -1
- package/apps/queue-worker/package.json +1 -1
- package/apps/queue-worker/tsconfig.json +1 -1
- package/apps/startx-cli/dist/index.mjs +68 -53
- package/apps/startx-cli/src/commands/package.ts +453 -0
- package/apps/startx-cli/src/configs/scripts.ts +18 -2
- package/apps/startx-cli/src/index.ts +2 -4
- package/apps/startx-cli/src/types.ts +2 -4
- package/apps/startx-cli/src/utils/inquirer.ts +8 -1
- package/apps/web-client/.dockerignore +4 -0
- package/apps/web-client/app/app.css +1 -0
- package/apps/web-client/app/components.json +23 -0
- package/apps/web-client/app/config/auth/auth-state.ts +59 -0
- package/apps/web-client/app/config/axios-client.ts +87 -0
- package/apps/web-client/app/config/env.ts +5 -0
- package/apps/web-client/app/entry.client.tsx +7 -0
- package/apps/web-client/app/eslint.config.ts +4 -0
- package/apps/web-client/app/root.tsx +77 -0
- package/apps/web-client/app/routes/home.tsx +12 -0
- package/apps/web-client/app/routes.ts +3 -0
- package/apps/web-client/eslint.config.ts +4 -0
- package/apps/web-client/package.json +55 -0
- package/apps/web-client/react-router.config.ts +7 -0
- package/apps/web-client/tsconfig.json +22 -0
- package/apps/web-client/vite-env.d.ts +8 -0
- package/apps/web-client/vite.config.ts +30 -0
- package/biome.json +5 -0
- package/configs/eslint-config/eslint.config.ts +1 -0
- package/configs/eslint-config/src/configs/base.ts +0 -1
- package/configs/eslint-config/src/configs/frontend.ts +1 -1
- package/configs/eslint-config/tsconfig.json +1 -1
- package/configs/typescript-config/tsconfig.frontend.json +1 -1
- package/configs/vitest-config/tsconfig.json +1 -1
- package/package.json +1 -1
- package/packages/@db/drizzle/tsconfig.json +1 -1
- package/packages/@db/sqlite/tsconfig.json +1 -1
- package/packages/@repo/env/package.json +1 -2
- package/packages/@repo/env/src/utils.ts +17 -11
- package/packages/@repo/lib/package.json +3 -1
- package/packages/@repo/lib/src/session-module/i-session.ts +108 -0
- package/packages/@repo/lib/src/session-module/index.ts +8 -111
- package/packages/@repo/lib/src/session-module/redis-session.ts +44 -0
- package/packages/@repo/lib/tsconfig.json +0 -1
- package/packages/@repo/logger/package.json +0 -1
- package/packages/@repo/logger/tsconfig.json +1 -1
- package/packages/@repo/mail/tsconfig.json +1 -1
- package/packages/@repo/redis/tsconfig.json +1 -1
- package/packages/aix/package.json +2 -0
- package/packages/aix/src/providers/ai-interface.ts +4 -4
- package/packages/aix/src/providers/bedrock/bedrock.ts +261 -0
- package/packages/aix/src/providers/default-models.ts +65 -0
- package/packages/aix/src/providers/openai/openai.ts +2 -2
- package/packages/aix/src/providers/providers.ts +11 -0
- package/packages/aix/src/providers/types.ts +1 -1
- package/packages/{constants → common}/package.json +4 -2
- package/packages/{constants/src/index.ts → common/src/constants.ts} +0 -5
- package/packages/common/src/types/users.ts +10 -0
- package/packages/{constants → common}/tsconfig.json +0 -3
- package/packages/ui/components.json +15 -8
- package/packages/ui/package.json +23 -36
- package/packages/ui/src/api/axios/i-client.ts +40 -0
- package/packages/ui/src/api/index.ts +6 -0
- package/packages/ui/src/api/query-provider.tsx +34 -0
- package/packages/ui/src/api/use-api/api-builder.ts +139 -0
- package/packages/ui/src/api/use-api/api-helpers.ts +165 -0
- package/packages/ui/src/api/use-api/api-types.ts +138 -0
- package/packages/ui/src/api/use-api/query-factory.ts +66 -0
- package/packages/ui/src/api/use-api/react-query/types.ts +64 -0
- package/packages/ui/src/api/use-api/react-query/use-api-client.ts +56 -0
- package/packages/ui/src/api/use-api/react-query/use-api.ts +297 -0
- package/packages/ui/src/components/custom/form-wrapper.tsx +113 -160
- package/packages/ui/src/components/custom/grid-component.tsx +4 -4
- package/packages/ui/src/components/custom/hover-tool.tsx +1 -1
- package/packages/ui/src/components/custom/image-picker.tsx +18 -20
- package/packages/ui/src/components/custom/no-content.tsx +6 -16
- package/packages/ui/src/components/custom/page-section.tsx +14 -17
- package/packages/ui/src/components/custom/simple-popover.tsx +5 -9
- package/packages/ui/src/components/custom/theme-provider.tsx +117 -42
- package/packages/ui/src/components/custom/typography.tsx +20 -22
- package/packages/ui/src/components/extensions/timeline.tsx +100 -0
- package/packages/ui/src/components/ui/alert-dialog.tsx +46 -108
- package/packages/ui/src/components/ui/avatar.tsx +79 -42
- package/packages/ui/src/components/ui/badge.tsx +29 -34
- package/packages/ui/src/components/ui/breadcrumb.tsx +65 -81
- package/packages/ui/src/components/ui/button.tsx +80 -80
- package/packages/ui/src/components/ui/card.tsx +48 -69
- package/packages/ui/src/components/ui/carousel.tsx +184 -211
- package/packages/ui/src/components/ui/checkbox.tsx +21 -24
- package/packages/ui/src/components/ui/command.tsx +121 -102
- package/packages/ui/src/components/ui/dialog.tsx +45 -32
- package/packages/ui/src/components/ui/dropdown-menu.tsx +45 -33
- package/packages/ui/src/components/ui/field.tsx +218 -0
- package/packages/ui/src/components/ui/form.tsx +63 -76
- package/packages/ui/src/components/ui/input-group.tsx +137 -0
- package/packages/ui/src/components/ui/input-otp.tsx +60 -50
- package/packages/ui/src/components/ui/input.tsx +16 -15
- package/packages/ui/src/components/ui/label.tsx +14 -17
- package/packages/ui/src/components/ui/multiple-select.tsx +22 -33
- package/packages/ui/src/components/ui/popover.tsx +20 -8
- package/packages/ui/src/components/ui/select.tsx +33 -34
- package/packages/ui/src/components/ui/separator.tsx +8 -8
- package/packages/ui/src/components/ui/sheet.tsx +32 -59
- package/packages/ui/src/components/ui/sidebar.tsx +654 -0
- package/packages/ui/src/components/ui/skeleton.tsx +2 -8
- package/packages/ui/src/components/ui/sonner.tsx +39 -0
- package/packages/ui/src/components/ui/spinner.tsx +6 -13
- package/packages/ui/src/components/ui/switch.tsx +15 -10
- package/packages/ui/src/components/ui/table.tsx +48 -89
- package/packages/ui/src/components/ui/tabs.tsx +37 -15
- package/packages/ui/src/components/ui/textarea.tsx +13 -13
- package/packages/ui/src/components/ui/tooltip.tsx +37 -23
- package/packages/ui/src/{components/hooks → hooks}/event/use-click.tsx +6 -10
- package/packages/ui/src/hooks/time/use-timer.tsx +51 -0
- package/packages/ui/src/hooks/use-media-query.tsx +19 -0
- package/packages/ui/src/hooks/use-mobile.tsx +17 -0
- package/packages/ui/src/{components/hooks → hooks}/use-update-effect.tsx +2 -2
- package/packages/ui/src/lib/utils.ts +113 -0
- package/packages/ui/src/styles/globals.css +311 -0
- package/packages/ui/src/styles/tailwind.css +89 -0
- package/packages/ui/tsconfig.json +7 -9
- package/pnpm-workspace.yaml +74 -64
- package/packages/ui/postcss.config.mjs +0 -9
- package/packages/ui/src/components/extensions/carousel.tsx +0 -392
- package/packages/ui/src/components/hooks/time/useTimer.tsx +0 -51
- package/packages/ui/src/components/hooks/use-media-query.tsx +0 -19
- package/packages/ui/src/components/lib/utils.ts +0 -242
- package/packages/ui/src/components/ui/timeline.tsx +0 -118
- package/packages/ui/src/components/util/n-formattor.ts +0 -22
- package/packages/ui/src/components/util/storage.ts +0 -37
- package/packages/ui/src/globals.css +0 -87
- package/packages/ui/tailwind.config.ts +0 -94
- /package/packages/{constants → common}/eslint.config.ts +0 -0
- /package/packages/{constants → common}/src/api.ts +0 -0
- /package/packages/{constants → common}/src/time.ts +0 -0
- /package/packages/{constants → common}/vitest.config.ts +0 -0
- /package/packages/ui/src/{components/hooks/time/useDebounce.tsx → hooks/time/use-debounce.tsx} +0 -0
- /package/packages/ui/src/{components/hooks/time/useInterval.tsx → hooks/time/use-interval.tsx} +0 -0
- /package/packages/ui/src/{components/hooks/time/useTimeout.tsx → hooks/time/use-timeout.tsx} +0 -0
- /package/packages/ui/src/{components/hooks → hooks}/use-persistent-storage.tsx +0 -0
- /package/packages/ui/src/{components/hooks → hooks}/use-window-dimension.tsx +0 -0
- /package/packages/ui/src/{components/sonner.tsx → sonner.ts} +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { SessionUser } from "@repo/common/types/users";
|
|
2
|
+
import { IAxiosClient } from "@repo/ui/api/axios/i-client";
|
|
3
|
+
import type { AxiosError, InternalAxiosRequestConfig } from "axios";
|
|
4
|
+
import { useAuthStore } from "./auth/auth-state";
|
|
5
|
+
import { ENV } from "./env";
|
|
6
|
+
|
|
7
|
+
type RetryableRequest = InternalAxiosRequestConfig & {
|
|
8
|
+
_retry?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class AxiosClient extends IAxiosClient {
|
|
12
|
+
private refreshPromise: Promise<string> | null = null;
|
|
13
|
+
|
|
14
|
+
async getAccessToken(): Promise<string> {
|
|
15
|
+
const authStore = useAuthStore.getState();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const response = await this.publicClient.get<{
|
|
19
|
+
accessToken: string;
|
|
20
|
+
user: SessionUser;
|
|
21
|
+
}>("/api/auth/token", {
|
|
22
|
+
withCredentials: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
this.accessToken = response.data.accessToken;
|
|
26
|
+
|
|
27
|
+
authStore.updateUser(response.data.user);
|
|
28
|
+
|
|
29
|
+
return this.accessToken;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error("Token refresh failed:", error);
|
|
32
|
+
|
|
33
|
+
this.accessToken = "";
|
|
34
|
+
authStore.reset();
|
|
35
|
+
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async refreshToken(): Promise<string> {
|
|
41
|
+
if (!this.refreshPromise) {
|
|
42
|
+
this.refreshPromise = this.getAccessToken().finally(() => {
|
|
43
|
+
this.refreshPromise = null;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return await this.refreshPromise;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setupInterceptors(): void {
|
|
51
|
+
this.privateClient.interceptors.response.use(
|
|
52
|
+
response => response,
|
|
53
|
+
async (error: AxiosError) => {
|
|
54
|
+
const originalRequest = error.config as RetryableRequest | undefined;
|
|
55
|
+
|
|
56
|
+
if (!originalRequest) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (originalRequest.url?.includes("/api/auth/token")) {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
65
|
+
originalRequest._retry = true;
|
|
66
|
+
|
|
67
|
+
const token = await this.refreshToken();
|
|
68
|
+
|
|
69
|
+
if (!token) {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
originalRequest.headers = originalRequest.headers ?? {};
|
|
74
|
+
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
75
|
+
|
|
76
|
+
return await this.privateClient(originalRequest);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const axiosClient = new AxiosClient(ENV.SERVER_URL, {
|
|
86
|
+
includeDefaultInterceptors: true,
|
|
87
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import "@repo/ui/globals.css";
|
|
2
|
+
import { QueryProvider } from "@repo/ui/api";
|
|
3
|
+
import { ThemeProvider } from "@repo/ui/components/custom/theme-provider";
|
|
4
|
+
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
|
|
5
|
+
import type { Route } from "./+types/root";
|
|
6
|
+
import "./app.css";
|
|
7
|
+
import { AuthStartup } from "./config/auth/auth-state";
|
|
8
|
+
import { ENV } from "./config/env";
|
|
9
|
+
|
|
10
|
+
export const links: Route.LinksFunction = () => [
|
|
11
|
+
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
|
12
|
+
{
|
|
13
|
+
rel: "preconnect",
|
|
14
|
+
href: "https://fonts.gstatic.com",
|
|
15
|
+
crossOrigin: "anonymous",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
rel: "stylesheet",
|
|
19
|
+
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function Layout({ children }: { children: React.ReactNode }) {
|
|
24
|
+
return (
|
|
25
|
+
<html lang="en">
|
|
26
|
+
<head>
|
|
27
|
+
<meta charSet="utf-8" />
|
|
28
|
+
|
|
29
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
30
|
+
<Meta />
|
|
31
|
+
<Links />
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
{children}
|
|
35
|
+
<ScrollRestoration />
|
|
36
|
+
<Scripts />
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default function App() {
|
|
43
|
+
return (
|
|
44
|
+
<QueryProvider mode={ENV.MODE}>
|
|
45
|
+
<ThemeProvider>
|
|
46
|
+
<AuthStartup />
|
|
47
|
+
<Outlet />
|
|
48
|
+
</ThemeProvider>
|
|
49
|
+
</QueryProvider>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
|
54
|
+
let message = "Oops!";
|
|
55
|
+
let details = "An unexpected error occurred.";
|
|
56
|
+
let stack: string | undefined;
|
|
57
|
+
|
|
58
|
+
if (isRouteErrorResponse(error)) {
|
|
59
|
+
message = error.status === 404 ? "404" : "Error";
|
|
60
|
+
details = error.status === 404 ? "The requested page could not be found." : error.statusText || details;
|
|
61
|
+
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
|
62
|
+
details = error.message;
|
|
63
|
+
stack = error.stack;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<main className="pt-16 p-4 container mx-auto">
|
|
68
|
+
<h1>{message}</h1>
|
|
69
|
+
<p>{details}</p>
|
|
70
|
+
{stack ? (
|
|
71
|
+
<pre className="w-full p-4 overflow-x-auto">
|
|
72
|
+
<code>{JSON.stringify(stack)}</code>
|
|
73
|
+
</pre>
|
|
74
|
+
) : null}
|
|
75
|
+
</main>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Route } from "./+types/home";
|
|
2
|
+
export function meta({}: Route.MetaArgs) {
|
|
3
|
+
return [{ title: "Startx Web App" }, { name: "description", content: "" }];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export default function Home() {
|
|
7
|
+
return (
|
|
8
|
+
<div className="flex flex-col w-fit">
|
|
9
|
+
<div>Home</div>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "web-client",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"clean": "rimraf dist .turbo",
|
|
7
|
+
"build": "react-router build",
|
|
8
|
+
"dev": "react-router dev",
|
|
9
|
+
"format": "biome format --write .",
|
|
10
|
+
"format:check": "biome ci .",
|
|
11
|
+
"start": "react-router-serve ./build/server/index.js",
|
|
12
|
+
"typecheck": "react-router typegen && tsc",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"lint": "eslint .",
|
|
15
|
+
"lint:fix": "eslint . --fix"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@dotenvx/dotenvx": "catalog:",
|
|
19
|
+
"@react-router/node": "catalog:",
|
|
20
|
+
"@react-router/serve": "catalog:",
|
|
21
|
+
"@repo/ui": "workspace:*",
|
|
22
|
+
"isbot": "catalog:",
|
|
23
|
+
"react": "catalog:",
|
|
24
|
+
"react-dom": "catalog:",
|
|
25
|
+
"react-router": "catalog:",
|
|
26
|
+
"zustand": "^5.0.13",
|
|
27
|
+
"@repo/common": "workspace:*"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@react-router/dev": "catalog:",
|
|
31
|
+
"@tailwindcss/vite": "catalog:",
|
|
32
|
+
"@types/node": "catalog:",
|
|
33
|
+
"@types/react": "catalog:",
|
|
34
|
+
"@types/react-dom": "catalog:",
|
|
35
|
+
"eslint-config": "workspace:*",
|
|
36
|
+
"tailwindcss": "catalog:",
|
|
37
|
+
"typescript-config": "workspace:^",
|
|
38
|
+
"vite": "catalog:"
|
|
39
|
+
},
|
|
40
|
+
"startx": {
|
|
41
|
+
"gTags": [
|
|
42
|
+
"react",
|
|
43
|
+
"frontend"
|
|
44
|
+
],
|
|
45
|
+
"tags": [
|
|
46
|
+
"react-router"
|
|
47
|
+
],
|
|
48
|
+
"requiredDeps": [
|
|
49
|
+
"@repo/ui"
|
|
50
|
+
],
|
|
51
|
+
"requiredDevDeps": [
|
|
52
|
+
"typescript-config"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "typescript-config/tsconfig.frontend.json",
|
|
3
|
+
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
6
|
+
"types": ["node", "vite/client"],
|
|
7
|
+
"target": "ES2022",
|
|
8
|
+
"module": "ES2022",
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"rootDirs": [".", "./.react-router/types"],
|
|
12
|
+
"paths": {
|
|
13
|
+
"~/*": ["./app/*"]
|
|
14
|
+
},
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"verbatimModuleSyntax": true,
|
|
17
|
+
"noEmit": true,
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"strict": true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { reactRouter } from "@react-router/dev/vite";
|
|
2
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
3
|
+
import { config } from "@dotenvx/dotenvx";
|
|
4
|
+
import { defineConfig } from "vite";
|
|
5
|
+
import path from "path";
|
|
6
|
+
export default defineConfig(() => {
|
|
7
|
+
const envPath = path.resolve(process.cwd(), "../../.env");
|
|
8
|
+
config({ path: envPath, quiet: true });
|
|
9
|
+
|
|
10
|
+
const ENV = process.env;
|
|
11
|
+
|
|
12
|
+
const env = Object.entries(ENV).reduce(
|
|
13
|
+
(acc, [key, value]) => {
|
|
14
|
+
if (!key.startsWith("VITE")) {
|
|
15
|
+
return acc;
|
|
16
|
+
}
|
|
17
|
+
acc[`import.meta.env.${key}`] = JSON.stringify(value);
|
|
18
|
+
return acc;
|
|
19
|
+
},
|
|
20
|
+
{} as Record<string, string>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
plugins: [tailwindcss(), reactRouter()],
|
|
25
|
+
resolve: {
|
|
26
|
+
tsconfigPaths: true,
|
|
27
|
+
},
|
|
28
|
+
define: env,
|
|
29
|
+
};
|
|
30
|
+
});
|
package/biome.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default [{}];
|
|
@@ -15,7 +15,7 @@ export const frontendConfig = tseslint.config(
|
|
|
15
15
|
|
|
16
16
|
// 1. Frontend Ignores
|
|
17
17
|
{
|
|
18
|
-
ignores: ["**/coverage/**", "**/storybook-static/**", "**/*.snap", "**/*.d.ts"],
|
|
18
|
+
ignores: ["**/coverage/**", "**/storybook-static/**", "**/*.snap", "**/*.d.ts", "**/.react-router/**"],
|
|
19
19
|
},
|
|
20
20
|
|
|
21
21
|
// 2. Browser/React Globals & Settings
|
package/package.json
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { config } from "dotenv";
|
|
4
|
-
import { expand } from "dotenv-expand";
|
|
1
|
+
import { config } from "@dotenvx/dotenvx";
|
|
5
2
|
import path from "path";
|
|
6
3
|
import { fileURLToPath } from "url";
|
|
7
4
|
|
|
@@ -24,7 +21,7 @@ export function projectRoot() {
|
|
|
24
21
|
}
|
|
25
22
|
|
|
26
23
|
/**
|
|
27
|
-
* Load .env files with a clear precedence:
|
|
24
|
+
* Load .env files with a clear precedence using dotenvx:
|
|
28
25
|
* - test: .env.test (and optional .env.test.local)
|
|
29
26
|
* - otherwise: .env -> .env.local (local should override .env)
|
|
30
27
|
*
|
|
@@ -33,21 +30,30 @@ export function projectRoot() {
|
|
|
33
30
|
export function loadDotenv(opts?: { root?: string }) {
|
|
34
31
|
const root = opts?.root ?? projectRoot();
|
|
35
32
|
|
|
33
|
+
// Shared options for a cleaner setup
|
|
34
|
+
const baseOptions = { quiet: true, ignore: ["MISSING_ENV_FILE"] };
|
|
36
35
|
if (process.env.NODE_ENV === "test") {
|
|
37
|
-
|
|
36
|
+
config({ path: path.join(root, ".env.test"), ...baseOptions });
|
|
38
37
|
// optional: if you want local test overrides
|
|
39
|
-
|
|
38
|
+
config({ path: path.join(root, ".env.test.local"), override: true, ...baseOptions });
|
|
40
39
|
return;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
// production/dev flow
|
|
44
|
-
|
|
43
|
+
config({ path: path.join(process.cwd(), ".env"), ...baseOptions }); // prod
|
|
44
|
+
|
|
45
45
|
// dev env
|
|
46
|
-
|
|
46
|
+
config({ path: path.join(root, ".env"), ...baseOptions });
|
|
47
|
+
|
|
47
48
|
// .env.local should override the base (for dev machine secrets)
|
|
48
|
-
|
|
49
|
+
config({ path: path.join(root, ".env.local"), override: true, ...baseOptions });
|
|
50
|
+
|
|
49
51
|
// also load .env.${NODE_ENV}.local if you want per-env local overrides:
|
|
50
52
|
if (process.env.NODE_ENV) {
|
|
51
|
-
|
|
53
|
+
config({
|
|
54
|
+
path: path.join(root, `.env.${process.env.NODE_ENV}.local`),
|
|
55
|
+
override: true,
|
|
56
|
+
...baseOptions,
|
|
57
|
+
});
|
|
52
58
|
}
|
|
53
59
|
}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@repo/redis": "workspace:*",
|
|
30
|
+
"@repo/common": "workspace:*",
|
|
30
31
|
"@aws-sdk/client-s3": "catalog:",
|
|
31
32
|
"@repo/mail": "workspace:*",
|
|
32
33
|
"@repo/logger": "workspace:*",
|
|
@@ -48,7 +49,8 @@
|
|
|
48
49
|
"@repo/env",
|
|
49
50
|
"@repo/logger",
|
|
50
51
|
"@repo/mail",
|
|
51
|
-
"@repo/redis"
|
|
52
|
+
"@repo/redis",
|
|
53
|
+
"@repo/constants"
|
|
52
54
|
],
|
|
53
55
|
"requiredDevDeps": [
|
|
54
56
|
"typescript-config"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { SessionUser } from "@repo/common/types/users";
|
|
2
|
+
import { defineEnv } from "@repo/env";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { TokenModule } from "../extra/token-module.js";
|
|
5
|
+
const env = defineEnv({
|
|
6
|
+
SESSION_DURATION: z.number().default(60 * 60 * 6),
|
|
7
|
+
});
|
|
8
|
+
export const constants = {
|
|
9
|
+
sessionDuration: env.SESSION_DURATION,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type TokenPair = {
|
|
13
|
+
accessToken: string;
|
|
14
|
+
refreshToken: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export abstract class IUserSession {
|
|
18
|
+
protected accessTokenKey(key: string) {
|
|
19
|
+
return `access_token:${key}`;
|
|
20
|
+
}
|
|
21
|
+
protected refreshTokenKey(key: string) {
|
|
22
|
+
return `refresh_token:${key}`;
|
|
23
|
+
}
|
|
24
|
+
protected userTokensKey(userId: string) {
|
|
25
|
+
return `session:${userId}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected abstract setSessionData(key: string, data: SessionUser, ttl: number): Promise<void>;
|
|
29
|
+
protected abstract getSessionData(key: string): Promise<SessionUser | null>;
|
|
30
|
+
protected abstract deleteSessionData(key: string): Promise<void>;
|
|
31
|
+
|
|
32
|
+
protected abstract setTokenData(key: string, data: TokenPair, ttl: number): Promise<void>;
|
|
33
|
+
protected abstract getTokenData(key: string): Promise<TokenPair | null>;
|
|
34
|
+
protected abstract deleteTokenData(key: string): Promise<void>;
|
|
35
|
+
|
|
36
|
+
public async getSessionUser(token: string): Promise<SessionUser | null> {
|
|
37
|
+
return await this.getSessionData(this.accessTokenKey(token));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public async startSession(payload: Omit<SessionUser, "accessToken">): Promise<TokenPair> {
|
|
41
|
+
await this.endSession(payload.id);
|
|
42
|
+
|
|
43
|
+
const accessToken = TokenModule.signAccessToken({
|
|
44
|
+
userID: payload.id,
|
|
45
|
+
email: payload.email,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const refreshToken = TokenModule.signRefreshToken({
|
|
49
|
+
userID: payload.id,
|
|
50
|
+
email: payload.email,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const sessionData: SessionUser = { ...payload, accessToken };
|
|
54
|
+
const tokens: TokenPair = { accessToken, refreshToken };
|
|
55
|
+
|
|
56
|
+
await Promise.all([
|
|
57
|
+
this.setSessionData(this.accessTokenKey(accessToken), sessionData, constants.sessionDuration),
|
|
58
|
+
this.setSessionData(payload.id, sessionData, constants.sessionDuration),
|
|
59
|
+
this.setTokenData(this.refreshTokenKey(refreshToken), tokens, constants.sessionDuration),
|
|
60
|
+
this.setTokenData(this.userTokensKey(payload.id), tokens, constants.sessionDuration),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
return tokens;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async checkRefreshToken(refreshToken: string): Promise<TokenPair | null> {
|
|
67
|
+
return await this.getTokenData(this.refreshTokenKey(refreshToken));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async updateAccessToken(payload: Omit<SessionUser, "accessToken">): Promise<string | null> {
|
|
71
|
+
const tokens = await this.getTokens(payload.id);
|
|
72
|
+
if (!tokens) return null;
|
|
73
|
+
|
|
74
|
+
const accessToken = tokens.accessToken;
|
|
75
|
+
const sessionData: SessionUser = { ...payload, accessToken };
|
|
76
|
+
|
|
77
|
+
await Promise.all([
|
|
78
|
+
this.setSessionData(this.accessTokenKey(accessToken), sessionData, constants.sessionDuration),
|
|
79
|
+
this.setSessionData(payload.id, sessionData, constants.sessionDuration),
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
return accessToken;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public async getTokens(userId: string): Promise<TokenPair | null> {
|
|
86
|
+
return await this.getTokenData(this.userTokensKey(userId));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public async logout(accessToken: string): Promise<null> {
|
|
90
|
+
const session = await this.getSessionUser(accessToken);
|
|
91
|
+
if (!session) return null;
|
|
92
|
+
|
|
93
|
+
await this.endSession(session.id);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public async endSession(userId: string): Promise<void> {
|
|
98
|
+
const tokens = await this.getTokens(userId);
|
|
99
|
+
if (!tokens) return;
|
|
100
|
+
|
|
101
|
+
await Promise.all([
|
|
102
|
+
this.deleteTokenData(this.refreshTokenKey(tokens.refreshToken)),
|
|
103
|
+
this.deleteSessionData(this.accessTokenKey(tokens.accessToken)),
|
|
104
|
+
this.deleteTokenData(this.userTokensKey(userId)),
|
|
105
|
+
this.deleteSessionData(userId),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
}
|