startx 1.0.5 → 1.0.9

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 (48) hide show
  1. package/apps/core-server/Dockerfile +9 -3
  2. package/apps/core-server/src/config/custom-type.ts +1 -1
  3. package/apps/core-server/src/middlewares/auth-middleware.ts +74 -30
  4. package/apps/queue-worker/Dockerfile +16 -9
  5. package/apps/queue-worker/package.json +5 -1
  6. package/apps/queue-worker/src/bullmq/board.ts +28 -0
  7. package/apps/queue-worker/src/index.ts +2 -0
  8. package/apps/startx-cli/dist/index.mjs +3 -3
  9. package/apps/startx-cli/src/configs/scripts.ts +44 -4
  10. package/apps/web-client/Dockerfile +40 -0
  11. package/apps/web-client/nginx.conf +20 -0
  12. package/apps/web-client/package.json +1 -1
  13. package/apps/web-client/src/app.css +0 -1
  14. package/assets/avatars/54b19ada-d53e-4ee9-8882-9dfed1bf1396.jpg +0 -0
  15. package/assets/avatars/ad3bf027-e85b-4cad-ab5f-80a25e37f4cb.jpg +0 -0
  16. package/assets/avatars/e67eb556-f125-4e24-95ad-8aff21b9926a.jpg +0 -0
  17. package/package.json +8 -2
  18. package/packages/@db/drizzle/drizzle.config.ts +1 -1
  19. package/packages/@db/drizzle/src/index.ts +4 -15
  20. package/packages/@repo/env/src/default-env.ts +1 -0
  21. package/packages/@repo/lib/src/cookie-module/cookie-module.ts +94 -38
  22. package/packages/@repo/lib/src/extra/index.ts +1 -0
  23. package/packages/@repo/lib/src/extra/token-module.ts +50 -21
  24. package/packages/@repo/lib/src/mail-module/nodemailer.ts +36 -23
  25. package/packages/@repo/lib/src/session-module/i-session.ts +132 -59
  26. package/packages/@repo/lib/src/session-module/index.ts +8 -2
  27. package/packages/@repo/lib/src/session-module/redis-session.ts +53 -23
  28. package/packages/@repo/lib/src/validation-module/index.ts +50 -78
  29. package/packages/@repo/lib/tsconfig.json +2 -1
  30. package/packages/@repo/model/eslint.config.ts +4 -0
  31. package/packages/@repo/model/package.json +41 -0
  32. package/packages/@repo/model/src/index.ts +0 -0
  33. package/packages/@repo/model/tsconfig.json +7 -0
  34. package/packages/@repo/model/vitest.config.ts +3 -0
  35. package/packages/common/src/time.ts +95 -22
  36. package/packages/queue/src/adapter/bullmq-adapter.ts +138 -47
  37. package/packages/queue/src/index.ts +3 -0
  38. package/packages/queue/src/queue-interface.ts +12 -5
  39. package/packages/queue/src/registry.ts +2 -2
  40. package/packages/queue/tsconfig.json +1 -1
  41. package/packages/ui/src/api/use-api/react-query/types.ts +3 -3
  42. package/packages/ui/src/api/use-api/react-query/use-api.ts +10 -11
  43. package/pnpm-workspace.yaml +4 -2
  44. package/turbo.json +21 -1
  45. package/packages/@repo/lib/src/bucket-module/file-storage.ts +0 -50
  46. package/packages/@repo/lib/src/bucket-module/index.ts +0 -3
  47. package/packages/@repo/lib/src/bucket-module/s3-storage.ts +0 -120
  48. package/packages/@repo/lib/src/bucket-module/utils.ts +0 -10
@@ -0,0 +1,40 @@
1
+ # syntax=docker/dockerfile:1
2
+
3
+ FROM node:24-alpine AS base
4
+ RUN corepack enable && corepack prepare pnpm@11.5.1 --activate
5
+
6
+ WORKDIR /app
7
+
8
+ FROM base AS builder
9
+
10
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
11
+
12
+ COPY --parents apps/web-client/package.json ./
13
+ COPY --parents packages/common/package.json ./
14
+ COPY --parents packages/ui/package.json ./
15
+ COPY --parents configs/*/package.json ./
16
+
17
+ RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
18
+ pnpm install --frozen-lockfile
19
+
20
+ COPY apps/web-client/ ./apps/web-client/
21
+ COPY packages/common ./packages/common
22
+ COPY packages/ui ./packages/ui
23
+ COPY configs ./configs
24
+ COPY turbo.json ./
25
+
26
+ # Build the required packages with Turbo cache mounted
27
+ RUN --mount=type=cache,id=turbo,target=/app/.turbo/cache \
28
+ pnpm build --filter=web-client
29
+
30
+ # --- Final production image ---
31
+ FROM nginx:stable-alpine
32
+
33
+
34
+ # Copy built server dist
35
+ COPY --from=builder /app/apps/web-client/build/client /usr/share/nginx/html
36
+ COPY --from=builder /app/apps/web-client/nginx.conf /etc/nginx/conf.d/default.conf
37
+
38
+ EXPOSE 80
39
+
40
+ CMD ["nginx", "-g", "daemon off;"]
@@ -0,0 +1,20 @@
1
+ server {
2
+ listen 80;
3
+ server_name _;
4
+
5
+ root /usr/share/nginx/html;
6
+ index index.html;
7
+
8
+ include /etc/nginx/mime.types;
9
+
10
+ location / {
11
+ try_files $uri $uri/ /index.html;
12
+ }
13
+
14
+ location /assets/ {
15
+ expires 1y;
16
+ add_header Cache-Control "public, immutable";
17
+ }
18
+
19
+ error_page 404 /index.html;
20
+ }
@@ -8,7 +8,7 @@
8
8
  "dev": "react-router dev",
9
9
  "format": "biome format --write .",
10
10
  "format:check": "biome ci .",
11
- "start": "react-router-serve ./build/server/index.js",
11
+ "start": "vite preview",
12
12
  "typecheck": "react-router typegen && tsc",
13
13
  "test": "vitest run",
14
14
  "lint": "eslint .",
@@ -1 +0,0 @@
1
- @import "tailwindcss";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "startx",
3
3
  "description": "",
4
- "version": "1.0.5",
4
+ "version": "1.0.9",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/avinashid/startx.git"
@@ -33,6 +33,12 @@
33
33
  "clean": "turbo clean",
34
34
  "test": "turbo test",
35
35
  "format": "turbo format",
36
- "db:push": "turbo db:push"
36
+ "db:push": "turbo run db:push",
37
+ "db:studio": "turbo run db:studio",
38
+ "db:generate": "turbo run db:generate",
39
+ "db:migrate": "turbo run db:migrate",
40
+ "db:up": "turbo run db:up",
41
+ "db:check": "turbo run db:check",
42
+ "db:pull": "turbo run db:pull"
37
43
  }
38
44
  }
@@ -6,7 +6,7 @@ const env = defineEnv({
6
6
  });
7
7
  export default defineConfig({
8
8
  out: "./drizzle",
9
- schema: "./src/schema/index.ts",
9
+ schema: "./src/schema/**/*.ts",
10
10
  dialect: "postgresql",
11
11
  dbCredentials: {
12
12
  url: env.DATABASE_URL!,
@@ -1,26 +1,15 @@
1
1
  import { defineEnv } from "@repo/env";
2
- import type { ExtractTablesWithRelations } from "drizzle-orm";
3
2
  import { drizzle, type NodePgQueryResultHKT } from "drizzle-orm/node-postgres";
4
- import { type PgTransaction } from "drizzle-orm/pg-core";
5
- import Pg from "pg";
3
+ import { PgAsyncTransaction } from "drizzle-orm/pg-core";
6
4
  import z from "zod";
7
5
 
8
- import * as schema from "./schema/index.js";
9
-
10
6
  const env = defineEnv({
11
7
  DATABASE_URL: z.string(),
12
8
  });
13
- export const client = new Pg.Pool({
14
- connectionString: env.DATABASE_URL,
15
- });
16
- const db = drizzle({ client, schema });
17
- export type DrizzleTransaction = PgTransaction<
18
- NodePgQueryResultHKT,
19
- typeof schema,
20
- ExtractTablesWithRelations<typeof schema>
21
- >;
9
+ export type DrizzleTransaction = PgAsyncTransaction<NodePgQueryResultHKT>;
10
+ const db = drizzle(env.DATABASE_URL);
11
+
22
12
  export type DrizzleDB = typeof db;
23
13
  export { db };
24
14
  export * from "drizzle-orm";
25
15
  export * from "./functions.js";
26
- export * from "./schema/index.js";
@@ -9,4 +9,5 @@ export const ENV = defineEnv({
9
9
  CORS_URL: z.string().optional().default("http://localhost:3000"),
10
10
  PORT: z.string().optional().default("3000"),
11
11
  LOG_LEVEL: z.enum(["error", "warn", "info", "http", "debug"]).default("debug"),
12
+ FILE_STORAGE_PATH: z.string().optional().default("storage"),
12
13
  });
@@ -1,48 +1,104 @@
1
+ import { Time } from "@repo/common/time";
1
2
  import { defineEnv, ENV } from "@repo/env";
2
3
  import type { CookieOptions } from "express";
3
4
  import z from "zod";
4
5
 
5
6
  const credentials = defineEnv({
6
- COOKIE_DOMAIN: z.string().optional().default(".localhost"),
7
+ COOKIE_DOMAIN: z.string().optional(),
7
8
  });
8
9
 
9
- const constants = {
10
- refreshToken: {
11
- cookie: "",
12
- cookieDomain: "",
13
- secureCookie: false,
14
- maxAge: 0,
15
- },
16
- initialize() {
17
- const prodMaxAge = 1000 * 60 * 60 * 24 * 30;
18
- const stagingMaxAge = 1000 * 60 * 60 * 24 * 365;
19
- const prodEnv = ENV.NODE_ENV === "production";
20
- const stagingEnv = ENV.NODE_ENV === "staging";
21
-
22
- if (prodEnv) {
23
- this.refreshToken.cookie = "refresh-token";
24
- this.refreshToken.cookieDomain = credentials.COOKIE_DOMAIN;
25
- this.refreshToken.secureCookie = true;
26
- this.refreshToken.maxAge = prodMaxAge;
27
- } else {
28
- this.refreshToken.cookie = "refresh-token-staging";
29
- this.refreshToken.cookieDomain = stagingEnv
30
- ? credentials.COOKIE_DOMAIN
31
- : credentials.COOKIE_DOMAIN || "localhost";
32
- this.refreshToken.secureCookie = stagingEnv ? true : false;
33
- this.refreshToken.maxAge = stagingMaxAge;
34
- }
35
- },
10
+ type RuntimeEnv = "development" | "staging" | "production";
11
+
12
+ type CookieDescriptor = {
13
+ name: string;
14
+ options: CookieOptions;
36
15
  };
37
- constants.initialize();
38
-
39
- export function getCookieOptions(refreshToken: string): [string, string, CookieOptions] {
40
- const cookieOptions: CookieOptions = {
41
- domain: constants.refreshToken.cookieDomain,
42
- maxAge: constants.refreshToken.maxAge,
43
- sameSite: "lax",
44
- httpOnly: true,
45
- secure: constants.refreshToken.secureCookie,
16
+
17
+ const COOKIE_NAMES = {
18
+ production: "refresh-token",
19
+ nonProduction: "refresh-token-staging",
20
+ } as const;
21
+
22
+ const COOKIE_TTL = {
23
+ development: Time.days(90).milliseconds,
24
+ staging: Time.days(90).milliseconds,
25
+ production: Time.days(30).milliseconds,
26
+ } as const;
27
+
28
+ function getRuntimeEnv(): RuntimeEnv {
29
+ switch (ENV.NODE_ENV) {
30
+ case "production":
31
+ return "production";
32
+ case "staging":
33
+ return "staging";
34
+ default:
35
+ return "development";
36
+ }
37
+ }
38
+
39
+ function resolveCookieDomain(env: RuntimeEnv): string | undefined {
40
+ if (env === "development") {
41
+ return undefined;
42
+ }
43
+
44
+ if (!credentials.COOKIE_DOMAIN) {
45
+ throw new Error("COOKIE_DOMAIN must be configured in staging/production environments");
46
+ }
47
+
48
+ return credentials.COOKIE_DOMAIN;
49
+ }
50
+
51
+ function resolveSameSite(env: RuntimeEnv): CookieOptions["sameSite"] {
52
+ /**
53
+ * If frontend and API are on different origins:
54
+ * use "none" + secure=true
55
+ *
56
+ * Otherwise lax is safer.
57
+ */
58
+ return env === "production" || env === "staging" ? "lax" : "lax";
59
+ }
60
+
61
+ function createRefreshTokenCookie(): CookieDescriptor {
62
+ const env = getRuntimeEnv();
63
+ const secure = env !== "development";
64
+
65
+ return {
66
+ name: env === "production" ? COOKIE_NAMES.production : COOKIE_NAMES.nonProduction,
67
+ options: {
68
+ httpOnly: true,
69
+ secure,
70
+ sameSite: resolveSameSite(env),
71
+ maxAge: COOKIE_TTL[env],
72
+ domain: resolveCookieDomain(env),
73
+ path: "/",
74
+ },
46
75
  };
47
- return [constants.refreshToken.cookie, refreshToken, cookieOptions];
48
76
  }
77
+
78
+ const refreshTokenCookie = createRefreshTokenCookie();
79
+
80
+ export const CookieModule = Object.freeze({
81
+ refreshTokenName(): string {
82
+ return refreshTokenCookie.name;
83
+ },
84
+
85
+ setRefreshToken(token: string): [string, string, CookieOptions] {
86
+ return [refreshTokenCookie.name, token, refreshTokenCookie.options];
87
+ },
88
+
89
+ clearRefreshToken(): [string, string, CookieOptions] {
90
+ return [
91
+ refreshTokenCookie.name,
92
+ "",
93
+ {
94
+ ...refreshTokenCookie.options,
95
+ maxAge: 0,
96
+ expires: new Date(0),
97
+ },
98
+ ];
99
+ },
100
+
101
+ getRefreshTokenOptions(): CookieOptions {
102
+ return refreshTokenCookie.options;
103
+ },
104
+ });
@@ -1 +1,2 @@
1
1
  export * from "./pagination-module.js";
2
+ export * from "./token-module.js"
@@ -1,41 +1,70 @@
1
- import { defineEnv, ENV } from "@repo/env";
2
- import * as jwt from "jsonwebtoken";
1
+ import { Time } from "@repo/common/time";
2
+ import { defineEnv } from "@repo/env";
3
+ import { logger } from "@repo/logger";
4
+ import jwt from "jsonwebtoken";
3
5
  import z from "zod";
4
- export type AuthTokenPayload = {
6
+
7
+ export type AccessTokenPayload = {
8
+ userID: string;
9
+ email: string;
10
+ sessionID: string;
11
+ };
12
+
13
+ export type RefreshTokenPayload = {
5
14
  userID: string;
6
15
  email: string;
16
+ sessionID: string;
17
+ jti: string;
7
18
  };
8
19
 
9
- const credentials = defineEnv({
10
- ACCESS_TOKEN_SECRET: z.string(),
11
- REFRESH_TOKEN_SECRET: z.string(),
20
+ const env = defineEnv({
21
+ ACCESS_TOKEN_SECRET: z.string().min(32),
22
+ REFRESH_TOKEN_SECRET: z.string().min(32),
23
+ ACCESS_TOKEN_EXPIRY: z.coerce.number().default(Time.hours(1).milliseconds),
24
+ REFRESH_TOKEN_EXPIRY: z.coerce.number().default(Time.days(30).milliseconds),
12
25
  });
13
- const accessTokenSecret = credentials.ACCESS_TOKEN_SECRET;
14
- const refreshTokenSecret = credentials.REFRESH_TOKEN_SECRET;
26
+
27
+ const JWT_CONFIG = {
28
+ algorithm: "HS256" as const,
29
+ };
15
30
 
16
31
  export class TokenModule {
17
- static signRefreshToken(payload: AuthTokenPayload) {
18
- return jwt.sign(payload, refreshTokenSecret, { expiresIn: "365d" });
32
+ static signAccessToken(payload: AccessTokenPayload): string {
33
+ return jwt.sign(payload, env.ACCESS_TOKEN_SECRET, {
34
+ ...JWT_CONFIG,
35
+ expiresIn: env.ACCESS_TOKEN_EXPIRY,
36
+ });
19
37
  }
20
- static verifyRefreshToken(refreshToken: string) {
38
+
39
+ static verifyAccessToken(token: string): AccessTokenPayload | null {
21
40
  try {
22
- const payload = jwt.verify(refreshToken, refreshTokenSecret) as AuthTokenPayload;
23
- return payload;
41
+ return jwt.verify(token, env.ACCESS_TOKEN_SECRET, {
42
+ algorithms: ["HS256"],
43
+ }) as AccessTokenPayload;
24
44
  } catch (error) {
25
- console.error(error);
45
+ logger.warn("Access token verification failed", {
46
+ error: error instanceof Error ? error.message : "unknown",
47
+ });
26
48
  return null;
27
49
  }
28
50
  }
29
- static signAccessToken(payload: AuthTokenPayload) {
30
- const expiration = ENV.NODE_ENV === "development" ? "30d" : "1d";
31
- return jwt.sign(payload, accessTokenSecret, { expiresIn: expiration });
51
+
52
+ static signRefreshToken(payload: RefreshTokenPayload): string {
53
+ return jwt.sign(payload, env.REFRESH_TOKEN_SECRET, {
54
+ ...JWT_CONFIG,
55
+ expiresIn: env.REFRESH_TOKEN_EXPIRY,
56
+ });
32
57
  }
33
- static verifyAccessToken(accessToken: string) {
58
+
59
+ static verifyRefreshToken(token: string): RefreshTokenPayload | null {
34
60
  try {
35
- const payload = jwt.verify(accessToken, accessTokenSecret) as AuthTokenPayload;
36
- return payload;
61
+ return jwt.verify(token, env.REFRESH_TOKEN_SECRET, {
62
+ algorithms: ["HS256"],
63
+ }) as RefreshTokenPayload;
37
64
  } catch (error) {
38
- console.error(error);
65
+ logger.warn("Refresh token verification failed", {
66
+ error: error instanceof Error ? error.message : "unknown",
67
+ });
39
68
  return null;
40
69
  }
41
70
  }
@@ -4,50 +4,64 @@ import type { Transporter, SendMailOptions } from "nodemailer";
4
4
  import * as nodemailer from "nodemailer";
5
5
  import z from "zod";
6
6
 
7
- const credentials = defineEnv({
7
+ const credentials = {
8
8
  SMTP_HOST: z.string(),
9
- SMTP_PORT: z.coerce.number().default(587),
9
+ SMTP_PORT: z.coerce.number().default(465),
10
10
  SMTP_USER: z.string(),
11
11
  SMTP_PASSWORD: z.string(),
12
- SMTP_MAIL_ENCRYPTION: z.string().default("tls"),
12
+ SMTP_MAIL_ENCRYPTION: z.enum(["ssl", "tls", "starttls"]).default("ssl"),
13
13
  SMTP_SENDER_MAIL: z.string().email(),
14
14
  SMTP_SENDER_NAME: z.string().default("Startx"),
15
- });
15
+ };
16
+ const _SMTPConfigSchema = z.object(credentials);
17
+
18
+ export type SMTPConfig = z.infer<typeof _SMTPConfigSchema>;
16
19
 
17
20
  class SMTPMailService {
18
- private static transporter: Transporter | null = null;
21
+ private static transporters = new Map<string, Transporter>();
22
+
23
+ private static getTransporter(config: SMTPConfig): Transporter {
24
+ const cacheKey = `${config.SMTP_HOST}:${config.SMTP_USER}`;
19
25
 
20
- private static initializeTransporter(): Transporter {
21
- if (!this.transporter) {
22
- this.transporter = nodemailer.createTransport({
23
- host: credentials.SMTP_HOST,
24
- port: credentials.SMTP_PORT,
25
- secure: credentials.SMTP_MAIL_ENCRYPTION === "ssl",
26
+ if (!this.transporters.has(cacheKey)) {
27
+ const transporter = nodemailer.createTransport({
28
+ host: config.SMTP_HOST,
29
+ port: config.SMTP_PORT,
30
+ secure: config.SMTP_MAIL_ENCRYPTION === "ssl",
26
31
  auth: {
27
- user: credentials.SMTP_USER,
28
- pass: credentials.SMTP_PASSWORD,
32
+ user: config.SMTP_USER,
33
+ pass: config.SMTP_PASSWORD,
29
34
  },
30
35
  });
36
+ this.transporters.set(cacheKey, transporter);
31
37
  }
32
- return this.transporter;
38
+
39
+ return this.transporters.get(cacheKey)!;
33
40
  }
34
41
 
35
- static async verifyConnection(): Promise<void> {
36
- const transporter = this.initializeTransporter();
42
+ static async verifyConnection(customConfig?: SMTPConfig): Promise<boolean> {
43
+ const config = customConfig || defineEnv(credentials);
44
+ const transporter = this.getTransporter(config);
45
+
37
46
  try {
38
47
  await transporter.verify();
39
- logger.info("SMTP Connection verified successfully.");
48
+ logger.info(`SMTP Connection verified successfully for ${config.SMTP_HOST}`);
49
+ return true;
40
50
  } catch (error) {
41
- logger.error("SMTP Connection verification failed:", error);
42
- throw error;
51
+ logger.error(`SMTP Connection verification failed for ${config.SMTP_HOST}:`, error);
52
+ return false;
43
53
  }
44
54
  }
45
55
 
46
- static async sendMail(props: { to: string; subject: string; text: string; html?: string }) {
47
- const transporter = this.initializeTransporter();
56
+ static async sendMail(
57
+ props: { to: string; subject: string; text: string; html?: string },
58
+ customConfig?: SMTPConfig
59
+ ) {
60
+ const config = customConfig || defineEnv(credentials);
61
+ const transporter = this.getTransporter(config);
48
62
 
49
63
  const mailOptions: SendMailOptions = {
50
- from: `"${credentials.SMTP_SENDER_NAME}" <${credentials.SMTP_SENDER_MAIL}>`,
64
+ from: `"${config.SMTP_SENDER_NAME}" <${config.SMTP_SENDER_MAIL}>`,
51
65
  to: props.to,
52
66
  subject: props.subject,
53
67
  text: props.text,
@@ -60,7 +74,6 @@ class SMTPMailService {
60
74
  return info;
61
75
  } catch (error) {
62
76
  logger.error("Error sending email:", error);
63
- // Retain the original error context for debugging
64
77
  throw new Error("Failed to send email", { cause: error });
65
78
  }
66
79
  }