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.
- package/apps/core-server/Dockerfile +9 -3
- package/apps/core-server/src/config/custom-type.ts +1 -1
- package/apps/core-server/src/middlewares/auth-middleware.ts +74 -30
- package/apps/queue-worker/Dockerfile +16 -9
- package/apps/queue-worker/package.json +5 -1
- package/apps/queue-worker/src/bullmq/board.ts +28 -0
- package/apps/queue-worker/src/index.ts +2 -0
- package/apps/startx-cli/dist/index.mjs +3 -3
- package/apps/startx-cli/src/configs/scripts.ts +44 -4
- package/apps/web-client/Dockerfile +40 -0
- package/apps/web-client/nginx.conf +20 -0
- package/apps/web-client/package.json +1 -1
- package/apps/web-client/src/app.css +0 -1
- package/assets/avatars/54b19ada-d53e-4ee9-8882-9dfed1bf1396.jpg +0 -0
- package/assets/avatars/ad3bf027-e85b-4cad-ab5f-80a25e37f4cb.jpg +0 -0
- package/assets/avatars/e67eb556-f125-4e24-95ad-8aff21b9926a.jpg +0 -0
- package/package.json +8 -2
- package/packages/@db/drizzle/drizzle.config.ts +1 -1
- package/packages/@db/drizzle/src/index.ts +4 -15
- package/packages/@repo/env/src/default-env.ts +1 -0
- package/packages/@repo/lib/src/cookie-module/cookie-module.ts +94 -38
- package/packages/@repo/lib/src/extra/index.ts +1 -0
- package/packages/@repo/lib/src/extra/token-module.ts +50 -21
- package/packages/@repo/lib/src/mail-module/nodemailer.ts +36 -23
- package/packages/@repo/lib/src/session-module/i-session.ts +132 -59
- package/packages/@repo/lib/src/session-module/index.ts +8 -2
- package/packages/@repo/lib/src/session-module/redis-session.ts +53 -23
- package/packages/@repo/lib/src/validation-module/index.ts +50 -78
- package/packages/@repo/lib/tsconfig.json +2 -1
- package/packages/@repo/model/eslint.config.ts +4 -0
- package/packages/@repo/model/package.json +41 -0
- package/packages/@repo/model/src/index.ts +0 -0
- package/packages/@repo/model/tsconfig.json +7 -0
- package/packages/@repo/model/vitest.config.ts +3 -0
- package/packages/common/src/time.ts +95 -22
- package/packages/queue/src/adapter/bullmq-adapter.ts +138 -47
- package/packages/queue/src/index.ts +3 -0
- package/packages/queue/src/queue-interface.ts +12 -5
- package/packages/queue/src/registry.ts +2 -2
- package/packages/queue/tsconfig.json +1 -1
- package/packages/ui/src/api/use-api/react-query/types.ts +3 -3
- package/packages/ui/src/api/use-api/react-query/use-api.ts +10 -11
- package/pnpm-workspace.yaml +4 -2
- package/turbo.json +21 -1
- package/packages/@repo/lib/src/bucket-module/file-storage.ts +0 -50
- package/packages/@repo/lib/src/bucket-module/index.ts +0 -3
- package/packages/@repo/lib/src/bucket-module/s3-storage.ts +0 -120
- 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": "
|
|
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";
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "startx",
|
|
3
3
|
"description": "",
|
|
4
|
-
"version": "1.0.
|
|
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
|
}
|
|
@@ -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 {
|
|
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
|
|
14
|
-
|
|
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()
|
|
7
|
+
COOKIE_DOMAIN: z.string().optional(),
|
|
7
8
|
});
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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,41 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
14
|
-
const
|
|
26
|
+
|
|
27
|
+
const JWT_CONFIG = {
|
|
28
|
+
algorithm: "HS256" as const,
|
|
29
|
+
};
|
|
15
30
|
|
|
16
31
|
export class TokenModule {
|
|
17
|
-
static
|
|
18
|
-
return jwt.sign(payload,
|
|
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
|
-
|
|
38
|
+
|
|
39
|
+
static verifyAccessToken(token: string): AccessTokenPayload | null {
|
|
21
40
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
41
|
+
return jwt.verify(token, env.ACCESS_TOKEN_SECRET, {
|
|
42
|
+
algorithms: ["HS256"],
|
|
43
|
+
}) as AccessTokenPayload;
|
|
24
44
|
} catch (error) {
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
return jwt.sign(payload,
|
|
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
|
-
|
|
58
|
+
|
|
59
|
+
static verifyRefreshToken(token: string): RefreshTokenPayload | null {
|
|
34
60
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
61
|
+
return jwt.verify(token, env.REFRESH_TOKEN_SECRET, {
|
|
62
|
+
algorithms: ["HS256"],
|
|
63
|
+
}) as RefreshTokenPayload;
|
|
37
64
|
} catch (error) {
|
|
38
|
-
|
|
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 =
|
|
7
|
+
const credentials = {
|
|
8
8
|
SMTP_HOST: z.string(),
|
|
9
|
-
SMTP_PORT: z.coerce.number().default(
|
|
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.
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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:
|
|
28
|
-
pass:
|
|
32
|
+
user: config.SMTP_USER,
|
|
33
|
+
pass: config.SMTP_PASSWORD,
|
|
29
34
|
},
|
|
30
35
|
});
|
|
36
|
+
this.transporters.set(cacheKey, transporter);
|
|
31
37
|
}
|
|
32
|
-
|
|
38
|
+
|
|
39
|
+
return this.transporters.get(cacheKey)!;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
static async verifyConnection(): Promise<
|
|
36
|
-
const
|
|
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(
|
|
48
|
+
logger.info(`SMTP Connection verified successfully for ${config.SMTP_HOST}`);
|
|
49
|
+
return true;
|
|
40
50
|
} catch (error) {
|
|
41
|
-
logger.error(
|
|
42
|
-
|
|
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(
|
|
47
|
-
|
|
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: `"${
|
|
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
|
}
|