startx 1.0.1 → 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.
Files changed (147) hide show
  1. package/.dockerignore +4 -0
  2. package/apps/cli/src/commands/index.ts +1 -1
  3. package/apps/cli/src/commands/{common → test}/test.ts +4 -2
  4. package/apps/cli/tsconfig.json +0 -1
  5. package/apps/core-server/Dockerfile +5 -4
  6. package/apps/core-server/package.json +1 -1
  7. package/apps/core-server/tsconfig.json +1 -1
  8. package/apps/queue-worker/package.json +1 -1
  9. package/apps/queue-worker/tsconfig.json +1 -1
  10. package/apps/startx-cli/dist/index.mjs +68 -53
  11. package/apps/startx-cli/src/commands/package.ts +453 -0
  12. package/apps/startx-cli/src/configs/scripts.ts +18 -2
  13. package/apps/startx-cli/src/index.ts +2 -4
  14. package/apps/startx-cli/src/types.ts +2 -4
  15. package/apps/startx-cli/src/utils/inquirer.ts +8 -1
  16. package/apps/web-client/.dockerignore +4 -0
  17. package/apps/web-client/app/app.css +1 -0
  18. package/apps/web-client/app/components.json +23 -0
  19. package/apps/web-client/app/config/auth/auth-state.ts +59 -0
  20. package/apps/web-client/app/config/axios-client.ts +87 -0
  21. package/apps/web-client/app/config/env.ts +5 -0
  22. package/apps/web-client/app/entry.client.tsx +7 -0
  23. package/apps/web-client/app/eslint.config.ts +4 -0
  24. package/apps/web-client/app/root.tsx +77 -0
  25. package/apps/web-client/app/routes/home.tsx +12 -0
  26. package/apps/web-client/app/routes.ts +3 -0
  27. package/apps/web-client/eslint.config.ts +4 -0
  28. package/apps/web-client/package.json +55 -0
  29. package/apps/web-client/react-router.config.ts +7 -0
  30. package/apps/web-client/tsconfig.json +22 -0
  31. package/apps/web-client/vite-env.d.ts +8 -0
  32. package/apps/web-client/vite.config.ts +30 -0
  33. package/biome.json +5 -0
  34. package/configs/eslint-config/eslint.config.ts +1 -0
  35. package/configs/eslint-config/src/configs/base.ts +0 -1
  36. package/configs/eslint-config/src/configs/frontend.ts +1 -1
  37. package/configs/eslint-config/tsconfig.json +1 -1
  38. package/configs/typescript-config/tsconfig.frontend.json +1 -1
  39. package/configs/vitest-config/tsconfig.json +1 -1
  40. package/package.json +1 -1
  41. package/packages/@db/drizzle/tsconfig.json +1 -1
  42. package/packages/@db/sqlite/tsconfig.json +1 -1
  43. package/packages/@repo/env/package.json +1 -2
  44. package/packages/@repo/env/src/utils.ts +17 -11
  45. package/packages/@repo/lib/package.json +3 -1
  46. package/packages/@repo/lib/src/session-module/i-session.ts +108 -0
  47. package/packages/@repo/lib/src/session-module/index.ts +8 -111
  48. package/packages/@repo/lib/src/session-module/redis-session.ts +44 -0
  49. package/packages/@repo/lib/tsconfig.json +0 -1
  50. package/packages/@repo/logger/package.json +0 -1
  51. package/packages/@repo/logger/tsconfig.json +1 -1
  52. package/packages/@repo/mail/tsconfig.json +1 -1
  53. package/packages/@repo/redis/tsconfig.json +1 -1
  54. package/packages/aix/package.json +2 -0
  55. package/packages/aix/src/providers/ai-interface.ts +4 -4
  56. package/packages/aix/src/providers/bedrock/bedrock.ts +261 -0
  57. package/packages/aix/src/providers/default-models.ts +65 -0
  58. package/packages/aix/src/providers/openai/openai.ts +2 -2
  59. package/packages/aix/src/providers/providers.ts +11 -0
  60. package/packages/aix/src/providers/types.ts +1 -1
  61. package/packages/{constants → common}/package.json +4 -2
  62. package/packages/{constants/src/index.ts → common/src/constants.ts} +0 -5
  63. package/packages/common/src/types/users.ts +10 -0
  64. package/packages/{constants → common}/tsconfig.json +0 -3
  65. package/packages/ui/components.json +15 -8
  66. package/packages/ui/package.json +23 -36
  67. package/packages/ui/src/api/axios/i-client.ts +40 -0
  68. package/packages/ui/src/api/index.ts +6 -0
  69. package/packages/ui/src/api/query-provider.tsx +34 -0
  70. package/packages/ui/src/api/use-api/api-builder.ts +139 -0
  71. package/packages/ui/src/api/use-api/api-helpers.ts +165 -0
  72. package/packages/ui/src/api/use-api/api-types.ts +138 -0
  73. package/packages/ui/src/api/use-api/query-factory.ts +66 -0
  74. package/packages/ui/src/api/use-api/react-query/types.ts +64 -0
  75. package/packages/ui/src/api/use-api/react-query/use-api-client.ts +56 -0
  76. package/packages/ui/src/api/use-api/react-query/use-api.ts +297 -0
  77. package/packages/ui/src/components/custom/form-wrapper.tsx +113 -160
  78. package/packages/ui/src/components/custom/grid-component.tsx +4 -4
  79. package/packages/ui/src/components/custom/hover-tool.tsx +1 -1
  80. package/packages/ui/src/components/custom/image-picker.tsx +18 -20
  81. package/packages/ui/src/components/custom/no-content.tsx +6 -16
  82. package/packages/ui/src/components/custom/page-section.tsx +14 -17
  83. package/packages/ui/src/components/custom/simple-popover.tsx +5 -9
  84. package/packages/ui/src/components/custom/theme-provider.tsx +117 -42
  85. package/packages/ui/src/components/custom/typography.tsx +20 -22
  86. package/packages/ui/src/components/extensions/timeline.tsx +100 -0
  87. package/packages/ui/src/components/ui/alert-dialog.tsx +46 -108
  88. package/packages/ui/src/components/ui/avatar.tsx +79 -42
  89. package/packages/ui/src/components/ui/badge.tsx +29 -34
  90. package/packages/ui/src/components/ui/breadcrumb.tsx +65 -81
  91. package/packages/ui/src/components/ui/button.tsx +80 -80
  92. package/packages/ui/src/components/ui/card.tsx +48 -69
  93. package/packages/ui/src/components/ui/carousel.tsx +184 -211
  94. package/packages/ui/src/components/ui/checkbox.tsx +21 -24
  95. package/packages/ui/src/components/ui/command.tsx +121 -102
  96. package/packages/ui/src/components/ui/dialog.tsx +45 -32
  97. package/packages/ui/src/components/ui/dropdown-menu.tsx +45 -33
  98. package/packages/ui/src/components/ui/field.tsx +218 -0
  99. package/packages/ui/src/components/ui/form.tsx +63 -76
  100. package/packages/ui/src/components/ui/input-group.tsx +137 -0
  101. package/packages/ui/src/components/ui/input-otp.tsx +60 -50
  102. package/packages/ui/src/components/ui/input.tsx +16 -15
  103. package/packages/ui/src/components/ui/label.tsx +14 -17
  104. package/packages/ui/src/components/ui/multiple-select.tsx +22 -33
  105. package/packages/ui/src/components/ui/popover.tsx +20 -8
  106. package/packages/ui/src/components/ui/select.tsx +33 -34
  107. package/packages/ui/src/components/ui/separator.tsx +8 -8
  108. package/packages/ui/src/components/ui/sheet.tsx +32 -59
  109. package/packages/ui/src/components/ui/sidebar.tsx +654 -0
  110. package/packages/ui/src/components/ui/skeleton.tsx +2 -8
  111. package/packages/ui/src/components/ui/sonner.tsx +39 -0
  112. package/packages/ui/src/components/ui/spinner.tsx +6 -13
  113. package/packages/ui/src/components/ui/switch.tsx +15 -10
  114. package/packages/ui/src/components/ui/table.tsx +48 -89
  115. package/packages/ui/src/components/ui/tabs.tsx +37 -15
  116. package/packages/ui/src/components/ui/textarea.tsx +13 -13
  117. package/packages/ui/src/components/ui/tooltip.tsx +37 -23
  118. package/packages/ui/src/{components/hooks → hooks}/event/use-click.tsx +6 -10
  119. package/packages/ui/src/hooks/time/use-timer.tsx +51 -0
  120. package/packages/ui/src/hooks/use-media-query.tsx +19 -0
  121. package/packages/ui/src/hooks/use-mobile.tsx +17 -0
  122. package/packages/ui/src/{components/hooks → hooks}/use-update-effect.tsx +2 -2
  123. package/packages/ui/src/lib/utils.ts +113 -0
  124. package/packages/ui/src/styles/globals.css +311 -0
  125. package/packages/ui/src/styles/tailwind.css +89 -0
  126. package/packages/ui/tsconfig.json +7 -9
  127. package/pnpm-workspace.yaml +74 -64
  128. package/packages/ui/postcss.config.mjs +0 -9
  129. package/packages/ui/src/components/extensions/carousel.tsx +0 -392
  130. package/packages/ui/src/components/hooks/time/useTimer.tsx +0 -51
  131. package/packages/ui/src/components/hooks/use-media-query.tsx +0 -19
  132. package/packages/ui/src/components/lib/utils.ts +0 -242
  133. package/packages/ui/src/components/ui/timeline.tsx +0 -118
  134. package/packages/ui/src/components/util/n-formattor.ts +0 -22
  135. package/packages/ui/src/components/util/storage.ts +0 -37
  136. package/packages/ui/src/globals.css +0 -87
  137. package/packages/ui/tailwind.config.ts +0 -94
  138. /package/packages/{constants → common}/eslint.config.ts +0 -0
  139. /package/packages/{constants → common}/src/api.ts +0 -0
  140. /package/packages/{constants → common}/src/time.ts +0 -0
  141. /package/packages/{constants → common}/vitest.config.ts +0 -0
  142. /package/packages/ui/src/{components/hooks/time/useDebounce.tsx → hooks/time/use-debounce.tsx} +0 -0
  143. /package/packages/ui/src/{components/hooks/time/useInterval.tsx → hooks/time/use-interval.tsx} +0 -0
  144. /package/packages/ui/src/{components/hooks/time/useTimeout.tsx → hooks/time/use-timeout.tsx} +0 -0
  145. /package/packages/ui/src/{components/hooks → hooks}/use-persistent-storage.tsx +0 -0
  146. /package/packages/ui/src/{components/hooks → hooks}/use-window-dimension.tsx +0 -0
  147. /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,5 @@
1
+ export const ENV = {
2
+ SERVER_URL: import.meta.env.VITE_SERVER_URL,
3
+ APP_TITLE: import.meta.env.VITE_APP_TITLE ?? "Web App",
4
+ MODE: (import.meta.env.MODE ?? "development") as "development" | "production" | "staging",
5
+ };
@@ -0,0 +1,7 @@
1
+ import { startTransition } from "react";
2
+ import { hydrateRoot } from "react-dom/client";
3
+ import { HydratedRouter } from "react-router/dom";
4
+
5
+ startTransition(() => {
6
+ hydrateRoot(document, <HydratedRouter />);
7
+ });
@@ -0,0 +1,4 @@
1
+ import { extend } from "eslint-config/extend";
2
+ import { frontendConfig } from "eslint-config/frontend";
3
+
4
+ export default extend(frontendConfig);
@@ -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,3 @@
1
+ import { type RouteConfig, index } from "@react-router/dev/routes";
2
+
3
+ export default [index("routes/home.tsx")] satisfies RouteConfig;
@@ -0,0 +1,4 @@
1
+ import { extend } from "eslint-config/extend";
2
+ import { frontendConfig } from "eslint-config/frontend";
3
+
4
+ export default extend(frontendConfig);
@@ -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,7 @@
1
+ import type { Config } from "@react-router/dev/config";
2
+
3
+ export default {
4
+ // Config options...
5
+ // Server-side render by default, to enable SPA mode set this to `false`
6
+ ssr: false,
7
+ } satisfies Config;
@@ -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,8 @@
1
+ interface ImportMetaEnv {
2
+ readonly VITE_SERVER_URL: string;
3
+ readonly VITE_APP_TITLE: string;
4
+ }
5
+
6
+ interface ImportMeta {
7
+ readonly env: ImportMetaEnv;
8
+ }
@@ -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
@@ -56,5 +56,10 @@
56
56
  "allowComments": true,
57
57
  "allowTrailingCommas": false
58
58
  }
59
+ },
60
+ "css": {
61
+ "parser": {
62
+ "tailwindDirectives": true
63
+ }
59
64
  }
60
65
  }
@@ -0,0 +1 @@
1
+ export default [{}];
@@ -54,7 +54,6 @@ export const baseConfig = tseslint.config(
54
54
  tsconfigRootDir: import.meta.dirname,
55
55
  },
56
56
  },
57
- // ADDED: Settings block to correctly resolve TypeScript imports
58
57
  settings: {
59
58
  "import-x/resolver": {
60
59
  typescript: {
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "extends": "typescript-config/tsconfig.node.json",
3
3
  "compilerOptions": {
4
- "baseUrl": "./src"
4
+ // "baseUrl": "./src"
5
5
  },
6
6
 
7
7
  "include": ["src/**/*.ts", "vitest.config.ts", "plugins.d.ts"]
@@ -9,6 +9,6 @@
9
9
  "incremental": false,
10
10
  "jsx": "react-jsx",
11
11
  "allowSyntheticDefaultImports": true,
12
- "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
12
+ "lib": ["esnext", "dom", "dom.iterable", "scripthost", "es2022"]
13
13
  }
14
14
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "extends": "typescript-config/tsconfig.node.json",
3
3
  "compilerOptions": {
4
- "baseUrl": "./src",
4
+ // "baseUrl": "./src",
5
5
  "allowImportingTsExtensions": true
6
6
  },
7
7
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "startx",
3
3
  "description": "",
4
- "version": "1.0.1",
4
+ "version": "1.0.3",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/avinashid/startx.git"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "extends": "typescript-config/tsconfig.node.json",
3
3
  "compilerOptions": {
4
- "baseUrl": "./src"
4
+ // "baseUrl": "./src"
5
5
  },
6
6
  "include": ["src/**/*.ts"]
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "extends": "typescript-config/tsconfig.node.json",
3
3
  "compilerOptions": {
4
- "baseUrl": "./src"
4
+ // "baseUrl": "./src"
5
5
  },
6
6
  "include": ["src/**/*.ts"]
7
7
  }
@@ -22,8 +22,7 @@
22
22
  "vitest-config": "workspace:*"
23
23
  },
24
24
  "dependencies": {
25
- "dotenv": "catalog:",
26
- "dotenv-expand": "catalog:"
25
+ "@dotenvx/dotenvx": "catalog:"
27
26
  },
28
27
  "startx": {
29
28
  "iTags": [
@@ -1,7 +1,4 @@
1
- process.env.DOTENV_CONFIG_QUIET = "true";
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
- expand(config({ path: path.join(root, ".env.test") }));
36
+ config({ path: path.join(root, ".env.test"), ...baseOptions });
38
37
  // optional: if you want local test overrides
39
- expand(config({ path: path.join(root, ".env.test.local"), override: true }));
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
- expand(config({ path: path.join(process.cwd(), ".env") })); // prod
43
+ config({ path: path.join(process.cwd(), ".env"), ...baseOptions }); // prod
44
+
45
45
  // dev env
46
- expand(config({ path: path.join(root, ".env") }));
46
+ config({ path: path.join(root, ".env"), ...baseOptions });
47
+
47
48
  // .env.local should override the base (for dev machine secrets)
48
- expand(config({ path: path.join(root, ".env.local"), override: true }));
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
- expand(config({ path: path.join(root, `.env.${process.env.NODE_ENV}.local`), override: true }));
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
+ }