create-nexwire 4.2.5 → 4.3.0

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 (85) hide show
  1. package/README.md +20 -8
  2. package/express/.env +1 -1
  3. package/express/package.json +1 -1
  4. package/express/src/config/database.ts +48 -29
  5. package/express/src/config/index.ts +83 -83
  6. package/express/src/config/openapi.ts +6 -24
  7. package/express/src/config/queue.ts +30 -30
  8. package/express/src/config/validate.ts +50 -40
  9. package/express/src/database/migrations/sqlite/0000_init.sql +47 -47
  10. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  11. package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
  12. package/express/src/framework/cache/cache.ts +40 -8
  13. package/express/src/framework/circuit-breaker/cb.ts +88 -60
  14. package/express/src/framework/database/connection.ts +52 -27
  15. package/express/src/framework/events/dispatcher.ts +61 -49
  16. package/express/src/framework/facade.ts +46 -3
  17. package/express/src/framework/http/app.ts +28 -22
  18. package/express/src/framework/http/logger.ts +27 -16
  19. package/express/src/framework/http/openapi.ts +9 -11
  20. package/express/src/framework/modules/routes.ts +26 -21
  21. package/express/src/framework/notification/channels/broadcast.ts +15 -0
  22. package/express/src/framework/notification/channels/mail.ts +40 -0
  23. package/express/src/framework/notification/contracts.ts +29 -0
  24. package/express/src/framework/notification/index.ts +19 -94
  25. package/express/src/framework/notification/registry.ts +15 -0
  26. package/express/src/framework/providers/adapter.ts +27 -17
  27. package/express/src/framework/queue/queue.ts +43 -44
  28. package/express/src/framework/realtime/broadcast.ts +29 -5
  29. package/express/src/framework/realtime/socket.ts +149 -119
  30. package/express/src/framework/redis/client.ts +148 -151
  31. package/express/src/framework/runtime/server-adapter.ts +39 -34
  32. package/express/src/framework/scheduler/lock.ts +128 -116
  33. package/express/src/framework/server.ts +33 -49
  34. package/express/src/framework/session/session.ts +69 -14
  35. package/express/src/framework/storage/storage.ts +744 -785
  36. package/express/src/framework/support/mail.ts +15 -7
  37. package/express/src/providers/notification.ts +4 -0
  38. package/express/src/resources/src/layouts/Layout/Header.vue +124 -105
  39. package/express/src/resources/src/plugins/pulse.ts +157 -147
  40. package/express/src/resources/vite.config.ts +54 -43
  41. package/express/src/storage/database/nexwire.sqlite +0 -0
  42. package/express/src/storage/logs/app.log +0 -0
  43. package/express/src/storage/logs/fatal.log +0 -0
  44. package/express/src/types/global.d.ts +32 -26
  45. package/hono/.env +1 -1
  46. package/hono/package.json +1 -1
  47. package/hono/src/config/database.ts +48 -29
  48. package/hono/src/config/index.ts +83 -83
  49. package/hono/src/config/openapi.ts +6 -24
  50. package/hono/src/config/queue.ts +30 -30
  51. package/hono/src/config/validate.ts +50 -40
  52. package/hono/src/database/migrations/sqlite/0000_init.sql +47 -47
  53. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  54. package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
  55. package/hono/src/framework/cache/cache.ts +40 -8
  56. package/hono/src/framework/circuit-breaker/cb.ts +88 -60
  57. package/hono/src/framework/database/connection.ts +52 -27
  58. package/hono/src/framework/events/dispatcher.ts +61 -49
  59. package/hono/src/framework/facade.ts +38 -1
  60. package/hono/src/framework/http/openapi.ts +7 -9
  61. package/hono/src/framework/notification/channels/broadcast.ts +15 -0
  62. package/hono/src/framework/notification/channels/mail.ts +40 -0
  63. package/hono/src/framework/notification/contracts.ts +29 -0
  64. package/hono/src/framework/notification/index.ts +19 -94
  65. package/hono/src/framework/notification/registry.ts +15 -0
  66. package/hono/src/framework/providers/adapter.ts +27 -17
  67. package/hono/src/framework/queue/queue.ts +43 -44
  68. package/hono/src/framework/realtime/broadcast.ts +29 -5
  69. package/hono/src/framework/realtime/socket.ts +217 -196
  70. package/hono/src/framework/redis/client.ts +148 -151
  71. package/hono/src/framework/scheduler/lock.ts +128 -116
  72. package/hono/src/framework/server.ts +27 -29
  73. package/hono/src/framework/session/session.ts +62 -14
  74. package/hono/src/framework/storage/storage.ts +744 -785
  75. package/hono/src/framework/support/mail.ts +56 -48
  76. package/hono/src/providers/notification.ts +4 -0
  77. package/hono/src/resources/src/layouts/Layout/Header.vue +124 -105
  78. package/hono/src/resources/src/plugins/pulse.ts +157 -147
  79. package/hono/src/resources/vite.config.ts +54 -43
  80. package/hono/src/storage/database/nexwire.sqlite +0 -0
  81. package/hono/src/storage/logs/app.log +0 -0
  82. package/hono/src/storage/logs/fatal.log +0 -0
  83. package/hono/src/types/global.d.ts +32 -26
  84. package/package.json +2 -2
  85. package/src/index.js +139 -14
@@ -1,12 +1,19 @@
1
1
  import type { ErrorRequestHandler, RequestHandler } from "express";
2
2
  import createError from "http-errors";
3
3
  import { logger } from "@/framework/support/logger.js";
4
+ import { openApiConfig } from "@/config/index.js";
4
5
 
5
6
  function requestIp(req: Parameters<RequestHandler>[0]) {
6
7
  const forwarded = req.headers["x-forwarded-for"];
7
8
  return String(req.headers["cf-connecting-ip"] || forwarded || req.headers["x-real-ip"] || req.socket.remoteAddress || "");
8
9
  }
9
10
 
11
+ // এই ফাংশনটি Express-এর জন্য তৈরি করা হয়েছে, তাই এটি কিছু অতিরিক্ত পাথও বাদ দেয় যা Hono-তে প্রযোজ্য নয়।
12
+ function shouldSkipHttpLog(pathname: string) {
13
+ // /queues, /queues/..., /favicon.ico বাদ দিন
14
+ return pathname.startsWith("/queues") || pathname === "/favicon.ico" || pathname === openApiConfig.scalar.specUrl || pathname === openApiConfig.scalar.docsPath;
15
+ }
16
+
10
17
  export const loggerMiddleware: RequestHandler = (req, res, next) => {
11
18
  const startedAt = Date.now();
12
19
  const requestId = crypto.randomUUID();
@@ -15,19 +22,22 @@ export const loggerMiddleware: RequestHandler = (req, res, next) => {
15
22
  res.setHeader("X-Request-ID", requestId);
16
23
 
17
24
  res.on("finish", () => {
18
- const pathname = (req.url ?? "").split("?")[0];
19
- if (pathname === "/health" || pathname.startsWith("/queues")) return;
25
+ // req.originalUrl ব্যবহার করুন, কারণ req.url রাউটারের ভেতরে বদলে যায়
26
+ const pathname = (req.originalUrl || req.url || "").split("?")[0];
20
27
 
21
- logger.info("HTTP Request", {
22
- requestId,
23
- method: req.method,
24
- path: pathname,
25
- url: req.originalUrl,
26
- status: res.statusCode,
27
- duration_ms: Date.now() - startedAt,
28
- ip: requestIp(req),
29
- userAgent: req.headers["user-agent"] || null
30
- });
28
+ // shouldSkipHttpLog চেক করুন
29
+ if (!shouldSkipHttpLog(pathname)) {
30
+ logger.info("HTTP Request", {
31
+ requestId,
32
+ method: req.method,
33
+ path: pathname,
34
+ url: req.originalUrl,
35
+ status: res.statusCode,
36
+ duration_ms: Date.now() - startedAt,
37
+ ip: requestIp(req),
38
+ userAgent: req.headers["user-agent"] || null
39
+ });
40
+ }
31
41
  });
32
42
 
33
43
  next();
@@ -45,6 +55,7 @@ export const onError: ErrorRequestHandler = (error, _req, res, _next) => {
45
55
  status,
46
56
  issues: error.error.issues
47
57
  });
58
+
48
59
  res.status(status).json({
49
60
  success: false,
50
61
  error: {
@@ -52,18 +63,18 @@ export const onError: ErrorRequestHandler = (error, _req, res, _next) => {
52
63
  issues: error.error.issues
53
64
  }
54
65
  });
66
+
55
67
  return;
56
68
  }
57
69
 
58
70
  const message = status === 500 && !error?.expose ? "Internal server error" : (error?.message ?? "Internal server error");
59
71
  const stack = process.env.NODE_ENV === "production" ? undefined : error?.stack;
72
+
60
73
  logger.error("Request failed", {
61
74
  status,
62
75
  message,
63
76
  stack: error?.stack
64
77
  });
65
- res.status(status).json({
66
- message,
67
- ...(stack ? { stack } : {})
68
- });
78
+
79
+ res.status(status).json({ message, ...(stack ? { stack } : {}) });
69
80
  };
@@ -28,27 +28,25 @@ export function createOpenApiDocument() {
28
28
  }
29
29
 
30
30
  export function configureOpenApi(app: Express) {
31
- app.get(openApiConfig.scalar.specUrl, (_req, res) => {
31
+ const { specUrl, docsPath, favicon, ...scalarOptions } = openApiConfig.scalar;
32
+
33
+ app.get(specUrl, (_req, res) => {
32
34
  res.json(createOpenApiDocument());
33
35
  });
34
36
 
35
- if (openApiConfig.scalar.favicon) {
37
+ if (favicon) {
36
38
  app.get("/favicon.ico", (_req, res) => {
37
- const faviconPath = path.join(process.cwd(), openApiConfig.scalar.favicon);
39
+ const faviconPath = path.join(process.cwd(), favicon);
38
40
  if (!fs.existsSync(faviconPath)) return res.status(404).end();
39
41
  res.type("image/x-icon").sendFile(faviconPath);
40
42
  });
41
43
  }
42
44
 
43
45
  app.get(
44
- openApiConfig.scalar.docsPath,
46
+ docsPath,
45
47
  apiReference({
46
- url: openApiConfig.scalar.specUrl,
47
- layout: openApiConfig.scalar.layout,
48
- theme: openApiConfig.scalar.theme,
49
- pageTitle: openApiConfig.scalar.pageTitle,
50
- defaultHttpClient: openApiConfig.scalar.defaultHttpClient,
51
- defaultOpenAllTags: openApiConfig.scalar.defaultOpenAllTags
52
- } as any)
48
+ url: specUrl, // Map specUrl to url
49
+ ...scalarOptions // Pass EVERYTHING else automatically!
50
+ } as any) // The `as any` might still be needed if Express types are slightly behind
53
51
  );
54
52
  }
@@ -1,21 +1,26 @@
1
- import type { Express } from "express";
2
- import { registerRouterOpenApi } from "@/framework/http/router.js";
3
- import { discoverModuleFiles, importFile, moduleNameFromPath } from "@/framework/modules/discover.js";
4
-
5
- export async function registerModuleRoutes(app: Express) {
6
- const files = await discoverModuleFiles("**/routes/*.{ts,js}");
7
-
8
- for (const file of files) {
9
- const route = await importFile(file);
10
-
11
- if (!route.default) {
12
- throw new Error(`Route file ${file} has no default export`);
13
- }
14
-
15
- const prefix = `/api/${moduleNameFromPath(file)}`;
16
- registerRouterOpenApi(route.default, prefix);
17
- app.use(prefix, route.default);
18
- }
19
-
20
- return files.length;
21
- }
1
+ import type { Express } from "express";
2
+ import { registerRouterOpenApi } from "@/framework/http/router.js";
3
+ import { discoverModuleFiles, importFile, moduleNameFromPath } from "@/framework/modules/discover.js";
4
+
5
+ export async function registerModuleRoutes(app: Express) {
6
+ const files = await discoverModuleFiles("**/routes/*.{ts,js}");
7
+
8
+ for (const file of files) {
9
+ const route = await importFile(file);
10
+
11
+ if (!route.default) {
12
+ throw new Error(`Route file ${file} has no default export`);
13
+ }
14
+
15
+ // Optional Guard: Ensure it's an Express Router
16
+ if (typeof route.default !== "function" || !route.default.stack) {
17
+ throw new Error(`Route file ${file} must export an express.Router() instance as default`);
18
+ }
19
+
20
+ const prefix = `/api/${moduleNameFromPath(file)}`;
21
+ registerRouterOpenApi(route.default, prefix);
22
+ app.use(prefix, route.default);
23
+ }
24
+
25
+ return files.length;
26
+ }
@@ -0,0 +1,15 @@
1
+ import { dispatchEvent } from "@/framework/events/dispatcher.js";
2
+ import type { NotificationChannel } from "@/framework/notification/contracts.js";
3
+
4
+ export const broadcastChannel: NotificationChannel = {
5
+ name: "broadcast",
6
+ async send(target, notification, options) {
7
+ if (!options.broadcast) return false;
8
+
9
+ await dispatchEvent("notification.created", notification, {
10
+ broadcast: { users: [target.id] }
11
+ });
12
+
13
+ return true;
14
+ }
15
+ };
@@ -0,0 +1,40 @@
1
+ import { mail } from "@/framework/support/mail.js";
2
+ import { resolveAuthProvider } from "@/framework/providers/adapter.js";
3
+ import { redisClientIfReady } from "@/framework/redis/client.js";
4
+ import { dispatchEvent } from "@/framework/events/dispatcher.js";
5
+ import type { NotificationChannel } from "@/framework/notification/contracts.js";
6
+
7
+ function escapeHtml(str: string) {
8
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
9
+ }
10
+
11
+ export const mailChannel: NotificationChannel = {
12
+ name: "mail",
13
+ async send(target, notification, options) {
14
+ if (!options.mail) return false;
15
+
16
+ let to = options.mail.to || null;
17
+ if (!to) {
18
+ try {
19
+ const provider = await resolveAuthProvider();
20
+ if (provider?.getUserContact) {
21
+ const contact = await provider.getUserContact(target.id);
22
+ if (contact?.email) to = contact.email as string;
23
+ }
24
+ } catch { }
25
+ }
26
+
27
+ if (!to) return false;
28
+
29
+ const subject = options.mail.subject || notification.title;
30
+ const html = options.mail.html || `<h2>${escapeHtml(notification.title)}</h2>${notification.body ? `<p>${escapeHtml(notification.body)}</p>` : ""}`;
31
+
32
+ if (redisClientIfReady()) {
33
+ await dispatchEvent("notification:mail", { to, subject, html }, { queue: "mail" });
34
+ } else {
35
+ await mail.sendMail({ to, subject, html });
36
+ }
37
+
38
+ return true;
39
+ }
40
+ };
@@ -0,0 +1,29 @@
1
+ import type { NotifiableTarget, NotificationRecord } from "@/framework/providers/contracts.js";
2
+
3
+ export type NotificationTarget = number | string | NotifiableTarget;
4
+ export type NormalizedTarget = { type: string; id: number | string };
5
+
6
+ export type NotificationOptions = {
7
+ type?: string;
8
+ title: string;
9
+ body?: string;
10
+ link?: string;
11
+ data?: Record<string, unknown>;
12
+
13
+ // Built-in Channels
14
+ broadcast?: boolean;
15
+ mail?: {
16
+ to?: string;
17
+ subject?: string;
18
+ html?: string;
19
+ };
20
+
21
+ // Extensible: Add any custom channel options here (e.g., sms, push, discord)
22
+ sms?: { to?: string; message?: string };
23
+ push?: { token?: string; payload?: any };
24
+ };
25
+
26
+ export interface NotificationChannel {
27
+ name: string;
28
+ send(target: NormalizedTarget, notification: NotificationRecord, options: NotificationOptions): Promise<boolean>;
29
+ }
@@ -1,39 +1,12 @@
1
- import { dispatchEvent } from "@/framework/events/dispatcher.js";
2
- import { resolveAuthProvider, resolveNotificationProvider } from "@/framework/providers/adapter.js";
3
- import type { NotifiableTarget, NotificationRecord } from "@/framework/providers/contracts.js";
4
- import { redisClientIfReady } from "@/framework/redis/client.js";
5
- import { mail } from "@/framework/support/mail.js";
1
+ import type { NormalizedTarget, NotificationOptions, NotificationTarget } from "@/framework/notification/contracts.js";
2
+ import { getChannels } from "@/framework/notification/registry.js";
3
+ import { resolveNotificationProvider } from "@/framework/providers/adapter.js";
4
+ import type { NotificationRecord } from "@/framework/providers/contracts.js";
6
5
 
7
6
  let missingProviderWarned = false;
8
7
 
9
- /** A numeric id (default auth), a string id (UUID/ULID), or an explicit target. */
10
- export type NotificationTarget = number | string | NotifiableTarget;
11
-
12
- /** Extra JSON metadata attached to the notification (e.g. entity IDs, links). */
13
- export type NotificationOptions = {
14
- /** Notification category key (default: "info") */
15
- type?: string;
16
- /** Notification title */
17
- title: string;
18
- /** Notification body text */
19
- body?: string;
20
- /** Optional click-through URL */
21
- link?: string;
22
- /** Arbitrary JSON payload stored in `data` column */
23
- data?: Record<string, unknown>;
24
- /** Whether to broadcast the notification to the user's socket in realtime */
25
- broadcast?: boolean;
26
- /** Email delivery options */
27
- mail?: {
28
- /** Explicit recipient. Skips the auth provider contact lookup. */
29
- to?: string;
30
- subject?: string;
31
- html?: string;
32
- };
33
- };
34
-
35
8
  /** Turn the public target argument into the polymorphic owner pair. */
36
- function normalizeTarget(target: NotificationTarget): Required<Pick<NotifiableTarget, "id">> & { type: string } {
9
+ function normalizeTarget(target: NotificationTarget): NormalizedTarget {
37
10
  if (target !== null && typeof target === "object") {
38
11
  return {
39
12
  type: String(target.type || "user").trim() || "user",
@@ -44,13 +17,10 @@ function normalizeTarget(target: NotificationTarget): Required<Pick<NotifiableTa
44
17
  }
45
18
 
46
19
  /**
47
- * Why: Persists a notification through the registered provider.
48
- * When: A feature wants to record something for a user (or another notifiable).
20
+ * Why: Persists a notification to the database and triggers all active channels.
21
+ * When: A feature wants to record something for a user.
49
22
  * Where: Application code, through the framework facade.
50
- * How: The provider owns storage; broadcasting and email stay here so a custom
51
- * store cannot silently disable them. A missing provider is reported once
52
- * and returns `null` instead of throwing, matching the framework's
53
- * "optional module" behaviour.
23
+ * How: The provider owns database storage. The channels handle delivery (mail, sms, ws).
54
24
  */
55
25
  export async function notify(target: NotificationTarget, options: NotificationOptions): Promise<NotificationRecord | null> {
56
26
  const provider = await resolveNotificationProvider();
@@ -65,6 +35,8 @@ export async function notify(target: NotificationTarget, options: NotificationOp
65
35
  }
66
36
 
67
37
  const normalizedTarget = normalizeTarget(target);
38
+
39
+ // 1. Save to Database via the Provider
68
40
  const normalized = await provider.create(normalizedTarget, {
69
41
  type: options.type,
70
42
  title: options.title,
@@ -73,62 +45,15 @@ export async function notify(target: NotificationTarget, options: NotificationOp
73
45
  data: options.data ?? null
74
46
  });
75
47
 
76
- if (options.broadcast) {
77
- await dispatchEvent("notification.created", normalized, {
78
- broadcast: { users: [normalizedTarget.id] }
79
- });
80
- }
81
-
82
- if (options.mail) {
83
- await sendMailChannel(normalizedTarget, normalized, options);
84
- }
48
+ // 2. Dispatch to all registered channels in parallel
49
+ const channels = getChannels();
50
+ await Promise.allSettled(
51
+ channels.map((channel) =>
52
+ channel.send(normalizedTarget, normalized, options).catch((err) => {
53
+ console.error(`[notification] Channel ${channel.name} failed:`, err);
54
+ })
55
+ )
56
+ );
85
57
 
86
58
  return normalized;
87
59
  }
88
-
89
- /** Deliver an email notification, either queued (if Redis is available) or direct. */
90
- async function sendMailChannel(
91
- target: { type: string; id: number | string },
92
- notification: NotificationRecord,
93
- options: NotificationOptions
94
- ) {
95
- let to: string | null = options.mail?.to || null;
96
-
97
- if (!to) {
98
- try {
99
- const provider = await resolveAuthProvider();
100
- if (provider?.getUserContact) {
101
- const contact = await provider.getUserContact(target.id);
102
- if (contact?.email) to = contact.email as string;
103
- }
104
- } catch {}
105
- }
106
-
107
- if (!to) return;
108
-
109
- const subject = options.mail?.subject || notification.title;
110
- const html = options.mail?.html || defaultMailHtml(notification.title, notification.body);
111
-
112
- if (redisClientIfReady()) {
113
- await dispatchEvent(
114
- "notification:mail",
115
- {
116
- to,
117
- subject,
118
- html
119
- },
120
- { queue: "mail" }
121
- );
122
- } else {
123
- await mail.sendMail({ to, subject, html });
124
- }
125
- }
126
-
127
- /** Generate a basic HTML email body from title and body text. */
128
- function defaultMailHtml(title: string, body: string | null) {
129
- return `<h2>${escapeHtml(title)}</h2>${body ? `<p>${escapeHtml(body)}</p>` : ""}`;
130
- }
131
-
132
- function escapeHtml(str: string) {
133
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
134
- }
@@ -0,0 +1,15 @@
1
+ import type { NotificationChannel } from "@/framework/notification/contracts.js";
2
+
3
+ const channels = new Map<string, NotificationChannel>();
4
+
5
+ export function registerChannel(channel: NotificationChannel) {
6
+ channels.set(channel.name, channel);
7
+ }
8
+
9
+ export function getChannels(): NotificationChannel[] {
10
+ return Array.from(channels.values());
11
+ }
12
+
13
+ export function clearChannels() {
14
+ channels.clear();
15
+ }
@@ -18,6 +18,7 @@ async function loadDefaultProvider(): Promise<AuthProvider | null> {
18
18
  if (defaultProvider) return defaultProvider;
19
19
  if (defaultLoadAttempted) return null;
20
20
  defaultLoadAttempted = true;
21
+
21
22
  try {
22
23
  const userMod = await import("@/modules/auth/database/models/user.js").catch(() => null);
23
24
  if (!userMod?.users || !userMod?.refreshTokens) return null;
@@ -25,22 +26,28 @@ async function loadDefaultProvider(): Promise<AuthProvider | null> {
25
26
  const users = userMod.users;
26
27
  const refreshTokens = userMod.refreshTokens;
27
28
 
28
- defaultProvider = {
29
- async getUserById(id: number | string) {
30
- try {
31
- const user = await (db.query.users.findFirst as any)?.({
32
- where: eq(users.id, id as any),
33
- with: { role: true }
34
- });
35
- if (user) return user;
36
- } catch {}
37
- try {
38
- const rows = await db.select().from(users).where(eq(users.id, id as any));
39
- return rows?.[0] ?? null;
40
- } catch {
41
- return null;
42
- }
43
- },
29
+ // Define the core logic as standalone functions to avoid `this` context issues
30
+ const getUserById = async (id: number | string) => {
31
+ try {
32
+ const user = await (db.query.users.findFirst as any)?.({
33
+ where: eq(users.id, id as any),
34
+ with: { role: true }
35
+ });
36
+ if (user) return user;
37
+ } catch {}
38
+ try {
39
+ const rows = await db
40
+ .select()
41
+ .from(users)
42
+ .where(eq(users.id, id as any));
43
+ return rows?.[0] ?? null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ };
48
+
49
+ const provider: AuthProvider = {
50
+ getUserById,
44
51
  async findRefreshToken(jti: string) {
45
52
  try {
46
53
  const token = await (db.query.refreshTokens.findFirst as any)?.({
@@ -89,7 +96,8 @@ async function loadDefaultProvider(): Promise<AuthProvider | null> {
89
96
  },
90
97
  async getUserContact(userId: number | string) {
91
98
  try {
92
- const user = await this.getUserById(userId);
99
+ // Use the standalone function instead of `this.getUserById`
100
+ const user = await getUserById(userId);
93
101
  if (!user) return null;
94
102
  return {
95
103
  email: user.email ?? null,
@@ -101,6 +109,8 @@ async function loadDefaultProvider(): Promise<AuthProvider | null> {
101
109
  }
102
110
  }
103
111
  };
112
+
113
+ defaultProvider = provider;
104
114
  return defaultProvider;
105
115
  } catch {
106
116
  return null;