create-nexwire 4.2.3 → 4.2.5
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/express/.vscode/extensions.json +6 -0
- package/express/.vscode/settings.json +37 -26
- package/express/biome.json +99 -66
- package/express/package.json +54 -27
- package/express/src/database/migrations/sqlite/0000_init.sql +47 -60
- package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -89
- package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/express/src/database/schema.ts +0 -1
- package/express/src/framework/facade.ts +12 -1
- package/express/src/framework/maker-cli/db/core.mjs +291 -288
- package/express/src/framework/maker-cli/module/core.mjs +125 -87
- package/express/src/framework/maker-cli/module/index.mjs +3 -3
- package/express/src/framework/maker-cli/stubs/notification/controller.ts.stub +61 -62
- package/express/src/framework/maker-cli/stubs/notification/job.ts.stub +11 -13
- package/express/src/framework/maker-cli/stubs/notification/model.ts.stub +29 -0
- package/express/src/framework/maker-cli/stubs/notification/provider.ts.stub +174 -0
- package/express/src/framework/maker-cli/stubs/notification/route.api.ts.stub +11 -10
- package/express/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +1 -1
- package/express/src/framework/maker-cli/stubs/notification/schema.plain.ts.stub +25 -25
- package/express/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -25
- package/express/src/framework/notification/index.ts +49 -87
- package/express/src/framework/providers/adapter.ts +24 -10
- package/express/src/framework/providers/contracts.ts +80 -10
- package/express/src/framework/providers/registry.ts +27 -1
- package/express/src/resources/src/components/Checkbox.vue +32 -29
- package/express/src/resources/src/components/Input.vue +243 -223
- package/express/src/resources/src/components/InputGroup.vue +147 -145
- package/express/src/resources/src/components/InputPasswordToggle.vue +229 -213
- package/express/src/resources/src/components/Modal.vue +71 -55
- package/express/src/resources/src/components/Refresh.vue +19 -0
- package/express/src/resources/src/helpers/assets.ts +51 -0
- package/express/src/resources/src/main.ts +4 -2
- package/express/src/resources/src/pages/auth/forgetPassword.vue +1 -1
- package/express/src/resources/src/pages/auth/login.vue +1 -1
- package/express/src/resources/src/pages/auth/register.vue +1 -1
- package/express/src/resources/src/pages/auth/resetPassword.vue +1 -1
- package/express/src/resources/src/pages/auth/verifyEmail.vue +1 -1
- package/express/src/resources/src/pages/dashboard/index.vue +66 -66
- package/express/src/resources/src/plugins/gum.ts +1 -1
- package/express/src/resources/src/stores/auth.ts +184 -164
- package/express/src/resources/vite.config.ts +9 -2
- package/express/src/storage/database/nexwire.sqlite +0 -0
- package/hono/.vscode/extensions.json +6 -0
- package/hono/.vscode/settings.json +37 -26
- package/hono/biome.json +25 -2
- package/hono/package.json +50 -23
- package/hono/src/database/migrations/sqlite/0000_init.sql +47 -60
- package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -89
- package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/hono/src/database/schema.ts +0 -1
- package/hono/src/framework/facade.ts +12 -1
- package/hono/src/framework/maker-cli/db/core.mjs +291 -288
- package/hono/src/framework/maker-cli/module/core.mjs +93 -52
- package/hono/src/framework/maker-cli/module/index.mjs +3 -3
- package/hono/src/framework/maker-cli/stubs/notification/controller.ts.stub +63 -58
- package/hono/src/framework/maker-cli/stubs/notification/job.ts.stub +10 -12
- package/hono/src/framework/maker-cli/stubs/notification/model.ts.stub +29 -0
- package/hono/src/framework/maker-cli/stubs/notification/provider.ts.stub +174 -0
- package/hono/src/framework/maker-cli/stubs/notification/route.api.ts.stub +11 -10
- package/hono/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +1 -1
- package/hono/src/framework/maker-cli/stubs/notification/schema.plain.ts.stub +25 -25
- package/hono/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -25
- package/hono/src/framework/notification/index.ts +49 -87
- package/hono/src/framework/providers/adapter.ts +24 -10
- package/hono/src/framework/providers/contracts.ts +80 -10
- package/hono/src/framework/providers/registry.ts +27 -1
- package/hono/src/resources/src/components/Checkbox.vue +32 -29
- package/hono/src/resources/src/components/Input.vue +243 -234
- package/hono/src/resources/src/components/InputGroup.vue +147 -149
- package/hono/src/resources/src/components/InputPasswordToggle.vue +229 -222
- package/hono/src/resources/src/components/Modal.vue +71 -55
- package/hono/src/resources/src/components/Refresh.vue +19 -0
- package/hono/src/resources/src/helpers/assets.ts +51 -0
- package/hono/src/resources/src/main.ts +4 -2
- package/hono/src/resources/src/pages/auth/forgetPassword.vue +1 -1
- package/hono/src/resources/src/pages/auth/login.vue +1 -1
- package/hono/src/resources/src/pages/auth/register.vue +1 -1
- package/hono/src/resources/src/pages/auth/resetPassword.vue +1 -1
- package/hono/src/resources/src/pages/auth/verifyEmail.vue +1 -1
- package/hono/src/resources/src/pages/dashboard/index.vue +66 -66
- package/hono/src/resources/src/plugins/gum.ts +1 -1
- package/hono/src/resources/src/stores/auth.ts +184 -164
- package/hono/src/resources/vite.config.ts +9 -2
- package/hono/src/storage/database/nexwire.sqlite +0 -0
- package/package.json +10 -10
- package/express/src/modules/auth/database/models/notification.ts +0 -24
- package/hono/src/modules/auth/database/models/notification.ts +0 -24
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { group } from "@/framework/facade.js";
|
|
2
2
|
import { authMiddleware } from "@/middlewares/auth-middleware.js";
|
|
3
|
-
import { list, unreadCount, markRead, markAllRead, remove } from "../controllers/{{
|
|
3
|
+
import { list, unreadCount, markRead, markAllRead, remove } from "../controllers/{{leaf}}.js";
|
|
4
4
|
|
|
5
5
|
export default group()
|
|
6
6
|
.use(authMiddleware)
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { z } from "@/framework/facade.js";
|
|
2
|
-
|
|
3
|
-
export const NotificationSchema = z.object({
|
|
4
|
-
id: z.number(),
|
|
5
|
-
userId: z.number(),
|
|
6
|
-
type: z.string(),
|
|
7
|
-
title: z.string(),
|
|
8
|
-
body: z.string().nullable(),
|
|
9
|
-
data: z.any().nullable(),
|
|
10
|
-
link: z.string().nullable(),
|
|
11
|
-
readAt: z.string().nullable(),
|
|
12
|
-
createdAt: z.string(),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export const UnreadCountSchema = z.object({
|
|
16
|
-
count: z.number(),
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export const PaginatedNotificationsSchema = z.object({
|
|
20
|
-
data: z.array(NotificationSchema),
|
|
21
|
-
total: z.number(),
|
|
22
|
-
unread: z.number(),
|
|
23
|
-
page: z.number(),
|
|
24
|
-
perPage: z.number(),
|
|
25
|
-
});
|
|
1
|
+
import { z } from "@/framework/facade.js";
|
|
26
2
|
|
|
3
|
+
export const NotificationSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
notifiableType: z.string(),
|
|
6
|
+
notifiableId: z.string(),
|
|
7
|
+
type: z.string(),
|
|
8
|
+
title: z.string(),
|
|
9
|
+
body: z.string().nullable(),
|
|
10
|
+
data: z.any().nullable(),
|
|
11
|
+
link: z.string().nullable(),
|
|
12
|
+
readAt: z.string().nullable(),
|
|
13
|
+
createdAt: z.string()
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const UnreadCountSchema = z.object({
|
|
17
|
+
count: z.number()
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const PaginatedNotificationsSchema = z.object({
|
|
21
|
+
data: z.array(NotificationSchema),
|
|
22
|
+
total: z.number(),
|
|
23
|
+
unread: z.number(),
|
|
24
|
+
page: z.number(),
|
|
25
|
+
perPage: z.number()
|
|
26
|
+
});
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { z } from "
|
|
2
|
-
|
|
3
|
-
export const NotificationSchema = z.object({
|
|
4
|
-
id: z.number(),
|
|
5
|
-
userId: z.number(),
|
|
6
|
-
type: z.string(),
|
|
7
|
-
title: z.string(),
|
|
8
|
-
body: z.string().nullable(),
|
|
9
|
-
data: z.any().nullable(),
|
|
10
|
-
link: z.string().nullable(),
|
|
11
|
-
readAt: z.string().nullable(),
|
|
12
|
-
createdAt: z.string(),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export const UnreadCountSchema = z.object({
|
|
16
|
-
count: z.number(),
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export const PaginatedNotificationsSchema = z.object({
|
|
20
|
-
data: z.array(NotificationSchema),
|
|
21
|
-
total: z.number(),
|
|
22
|
-
unread: z.number(),
|
|
23
|
-
page: z.number(),
|
|
24
|
-
perPage: z.number(),
|
|
25
|
-
});
|
|
1
|
+
import { z } from "@/framework/facade.js";
|
|
26
2
|
|
|
3
|
+
export const NotificationSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
notifiableType: z.string(),
|
|
6
|
+
notifiableId: z.string(),
|
|
7
|
+
type: z.string(),
|
|
8
|
+
title: z.string(),
|
|
9
|
+
body: z.string().nullable(),
|
|
10
|
+
data: z.any().nullable(),
|
|
11
|
+
link: z.string().nullable(),
|
|
12
|
+
readAt: z.string().nullable(),
|
|
13
|
+
createdAt: z.string()
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const UnreadCountSchema = z.object({
|
|
17
|
+
count: z.number()
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const PaginatedNotificationsSchema = z.object({
|
|
21
|
+
data: z.array(NotificationSchema),
|
|
22
|
+
total: z.number(),
|
|
23
|
+
unread: z.number(),
|
|
24
|
+
page: z.number(),
|
|
25
|
+
perPage: z.number()
|
|
26
|
+
});
|
|
@@ -1,27 +1,13 @@
|
|
|
1
|
-
import { eq } from "drizzle-orm";
|
|
2
|
-
import { db } from "@/framework/database/connection.js";
|
|
3
1
|
import { dispatchEvent } from "@/framework/events/dispatcher.js";
|
|
4
|
-
import { resolveAuthProvider } from "@/framework/providers/adapter.js";
|
|
2
|
+
import { resolveAuthProvider, resolveNotificationProvider } from "@/framework/providers/adapter.js";
|
|
3
|
+
import type { NotifiableTarget, NotificationRecord } from "@/framework/providers/contracts.js";
|
|
5
4
|
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
6
5
|
import { mail } from "@/framework/support/mail.js";
|
|
7
6
|
|
|
8
|
-
let
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
async function loadNotificationModel() {
|
|
13
|
-
if (modelsLoaded) return true;
|
|
14
|
-
if (modelsLoadAttempted) return false;
|
|
15
|
-
modelsLoadAttempted = true;
|
|
16
|
-
try {
|
|
17
|
-
const notificationMod = await import("@/modules/auth/database/models/notification.js").catch(() => null);
|
|
18
|
-
if (notificationMod?.notifications) notifications = notificationMod.notifications;
|
|
19
|
-
modelsLoaded = !!notifications;
|
|
20
|
-
return modelsLoaded;
|
|
21
|
-
} catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
7
|
+
let missingProviderWarned = false;
|
|
8
|
+
|
|
9
|
+
/** A numeric id (default auth), a string id (UUID/ULID), or an explicit target. */
|
|
10
|
+
export type NotificationTarget = number | string | NotifiableTarget;
|
|
25
11
|
|
|
26
12
|
/** Extra JSON metadata attached to the notification (e.g. entity IDs, links). */
|
|
27
13
|
export type NotificationOptions = {
|
|
@@ -46,38 +32,55 @@ export type NotificationOptions = {
|
|
|
46
32
|
};
|
|
47
33
|
};
|
|
48
34
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
35
|
+
/** Turn the public target argument into the polymorphic owner pair. */
|
|
36
|
+
function normalizeTarget(target: NotificationTarget): Required<Pick<NotifiableTarget, "id">> & { type: string } {
|
|
37
|
+
if (target !== null && typeof target === "object") {
|
|
38
|
+
return {
|
|
39
|
+
type: String(target.type || "user").trim() || "user",
|
|
40
|
+
id: target.id
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { type: "user", id: target as number | string };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Why: Persists a notification through the registered provider.
|
|
48
|
+
* When: A feature wants to record something for a user (or another notifiable).
|
|
49
|
+
* 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.
|
|
54
|
+
*/
|
|
55
|
+
export async function notify(target: NotificationTarget, options: NotificationOptions): Promise<NotificationRecord | null> {
|
|
56
|
+
const provider = await resolveNotificationProvider();
|
|
57
|
+
if (!provider) {
|
|
58
|
+
if (!missingProviderWarned) {
|
|
59
|
+
missingProviderWarned = true;
|
|
60
|
+
console.warn(
|
|
61
|
+
'[notification] skipped: no notification provider is registered. Run "bun maker module:make-notification notification" then "bun maker module:migrate notification", or register a provider in src/providers/.'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
56
64
|
return null;
|
|
57
65
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
|
|
67
|
+
const normalizedTarget = normalizeTarget(target);
|
|
68
|
+
const normalized = await provider.create(normalizedTarget, {
|
|
69
|
+
type: options.type,
|
|
61
70
|
title: options.title,
|
|
62
|
-
body: options.body
|
|
63
|
-
link: options.link
|
|
64
|
-
data: options.data
|
|
71
|
+
body: options.body ?? null,
|
|
72
|
+
link: options.link ?? null,
|
|
73
|
+
data: options.data ?? null
|
|
65
74
|
});
|
|
66
75
|
|
|
67
|
-
const insertedId = Number((insertResult as any)[0]?.insertId ?? (insertResult as any).insertId ?? (insertResult as any).lastInsertRowid);
|
|
68
|
-
if (!insertedId) throw new Error("Failed to create notification");
|
|
69
|
-
|
|
70
|
-
const [row] = await db.select().from(notifications).where(eq(notifications.id, insertedId));
|
|
71
|
-
const normalized = normalize(row);
|
|
72
|
-
|
|
73
76
|
if (options.broadcast) {
|
|
74
77
|
await dispatchEvent("notification.created", normalized, {
|
|
75
|
-
broadcast: { users: [
|
|
78
|
+
broadcast: { users: [normalizedTarget.id] }
|
|
76
79
|
});
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
if (options.mail) {
|
|
80
|
-
await sendMailChannel(
|
|
83
|
+
await sendMailChannel(normalizedTarget, normalized, options);
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
return normalized;
|
|
@@ -85,38 +88,18 @@ export async function notify(userId: number, options: NotificationOptions) {
|
|
|
85
88
|
|
|
86
89
|
/** Deliver an email notification, either queued (if Redis is available) or direct. */
|
|
87
90
|
async function sendMailChannel(
|
|
88
|
-
|
|
89
|
-
notification:
|
|
91
|
+
target: { type: string; id: number | string },
|
|
92
|
+
notification: NotificationRecord,
|
|
90
93
|
options: NotificationOptions
|
|
91
94
|
) {
|
|
92
|
-
let to: string | null =
|
|
93
|
-
let name: string | null = null;
|
|
95
|
+
let to: string | null = options.mail?.to || null;
|
|
94
96
|
|
|
95
97
|
if (!to) {
|
|
96
98
|
try {
|
|
97
99
|
const provider = await resolveAuthProvider();
|
|
98
100
|
if (provider?.getUserContact) {
|
|
99
|
-
const contact = await provider.getUserContact(
|
|
100
|
-
if (contact?.email)
|
|
101
|
-
to = contact.email as string;
|
|
102
|
-
name = (contact.name as string) || null;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
} catch {}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (!to) {
|
|
109
|
-
try {
|
|
110
|
-
const userMod = await import("@/modules/auth/database/models/user.js").catch(() => null);
|
|
111
|
-
if (userMod?.users) {
|
|
112
|
-
const [user] = await db
|
|
113
|
-
.select({ email: userMod.users.email, name: (userMod.users as any).name })
|
|
114
|
-
.from(userMod.users)
|
|
115
|
-
.where(eq(userMod.users.id, userId));
|
|
116
|
-
if (user?.email) {
|
|
117
|
-
to = user.email as string;
|
|
118
|
-
name = (user as any).name || null;
|
|
119
|
-
}
|
|
101
|
+
const contact = await provider.getUserContact(target.id);
|
|
102
|
+
if (contact?.email) to = contact.email as string;
|
|
120
103
|
}
|
|
121
104
|
} catch {}
|
|
122
105
|
}
|
|
@@ -149,24 +132,3 @@ function defaultMailHtml(title: string, body: string | null) {
|
|
|
149
132
|
function escapeHtml(str: string) {
|
|
150
133
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
151
134
|
}
|
|
152
|
-
|
|
153
|
-
/** Convert a DB row into the normalized response shape, parsing the `data` JSON column. */
|
|
154
|
-
function normalize(row: any) {
|
|
155
|
-
let parsed: unknown = null;
|
|
156
|
-
if (row.data) {
|
|
157
|
-
try {
|
|
158
|
-
parsed = JSON.parse(row.data);
|
|
159
|
-
} catch {}
|
|
160
|
-
}
|
|
161
|
-
return {
|
|
162
|
-
id: row.id,
|
|
163
|
-
userId: row.userId,
|
|
164
|
-
type: row.type,
|
|
165
|
-
title: row.title,
|
|
166
|
-
body: row.body,
|
|
167
|
-
data: parsed,
|
|
168
|
-
link: row.link,
|
|
169
|
-
readAt: row.readAt?.toISOString() || null,
|
|
170
|
-
createdAt: row.createdAt.toISOString()
|
|
171
|
-
};
|
|
172
|
-
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { eq } from "drizzle-orm";
|
|
2
2
|
import { db } from "@/framework/database/connection.js";
|
|
3
|
-
import type { AuthProvider } from "./contracts.js";
|
|
3
|
+
import type { AuthProvider, NotificationProvider } from "./contracts.js";
|
|
4
4
|
import { loadProviders } from "./loader.js";
|
|
5
|
-
import { getAuthProvider } from "./registry.js";
|
|
5
|
+
import { getAuthProvider, getNotificationProvider } from "./registry.js";
|
|
6
6
|
|
|
7
7
|
let defaultProvider: AuthProvider | null = null;
|
|
8
8
|
let defaultLoadAttempted = false;
|
|
@@ -26,16 +26,16 @@ async function loadDefaultProvider(): Promise<AuthProvider | null> {
|
|
|
26
26
|
const refreshTokens = userMod.refreshTokens;
|
|
27
27
|
|
|
28
28
|
defaultProvider = {
|
|
29
|
-
async getUserById(id: number) {
|
|
29
|
+
async getUserById(id: number | string) {
|
|
30
30
|
try {
|
|
31
31
|
const user = await (db.query.users.findFirst as any)?.({
|
|
32
|
-
where: eq(users.id, id),
|
|
32
|
+
where: eq(users.id, id as any),
|
|
33
33
|
with: { role: true }
|
|
34
34
|
});
|
|
35
35
|
if (user) return user;
|
|
36
36
|
} catch {}
|
|
37
37
|
try {
|
|
38
|
-
const rows = await db.select().from(users).where(eq(users.id, id));
|
|
38
|
+
const rows = await db.select().from(users).where(eq(users.id, id as any));
|
|
39
39
|
return rows?.[0] ?? null;
|
|
40
40
|
} catch {
|
|
41
41
|
return null;
|
|
@@ -55,14 +55,14 @@ async function loadDefaultProvider(): Promise<AuthProvider | null> {
|
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
|
-
async deleteRefreshToken(tokenId: number) {
|
|
58
|
+
async deleteRefreshToken(tokenId: number | string) {
|
|
59
59
|
try {
|
|
60
|
-
await db.delete(refreshTokens).where(eq(refreshTokens.id, tokenId));
|
|
60
|
+
await db.delete(refreshTokens).where(eq(refreshTokens.id, tokenId as any));
|
|
61
61
|
} catch {}
|
|
62
62
|
},
|
|
63
|
-
async revokeRefreshToken(tokenId: number) {
|
|
63
|
+
async revokeRefreshToken(tokenId: number | string) {
|
|
64
64
|
try {
|
|
65
|
-
await db.delete(refreshTokens).where(eq(refreshTokens.id, tokenId));
|
|
65
|
+
await db.delete(refreshTokens).where(eq(refreshTokens.id, tokenId as any));
|
|
66
66
|
} catch {}
|
|
67
67
|
},
|
|
68
68
|
async getAuthContext(user: any, payload?: any) {
|
|
@@ -87,7 +87,7 @@ async function loadDefaultProvider(): Promise<AuthProvider | null> {
|
|
|
87
87
|
...user
|
|
88
88
|
};
|
|
89
89
|
},
|
|
90
|
-
async getUserContact(userId: number) {
|
|
90
|
+
async getUserContact(userId: number | string) {
|
|
91
91
|
try {
|
|
92
92
|
const user = await this.getUserById(userId);
|
|
93
93
|
if (!user) return null;
|
|
@@ -123,3 +123,17 @@ export async function resolveAuthProvider(): Promise<AuthProvider | null> {
|
|
|
123
123
|
if (def) return def;
|
|
124
124
|
return null;
|
|
125
125
|
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Why: Resolves the notification storage provider.
|
|
129
|
+
* When: `notify()` persists a notification; the HTTP handlers read it back.
|
|
130
|
+
* Where: Notification runtime and the generated notification controller.
|
|
131
|
+
* How: Provider files are loaded first, so a project can replace the store from
|
|
132
|
+
* `src/providers/` or a module provider folder. The notification module is
|
|
133
|
+
* opt-in, so an absent provider is a normal state: callers warn once
|
|
134
|
+
* (runtime) or answer 503 (HTTP) instead of throwing.
|
|
135
|
+
*/
|
|
136
|
+
export async function resolveNotificationProvider(): Promise<NotificationProvider | null> {
|
|
137
|
+
await loadProviders();
|
|
138
|
+
return getNotificationProvider();
|
|
139
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Why: Defines the shapes a pluggable provider has to satisfy.
|
|
3
|
-
* When: A developer replaces a framework default (auth
|
|
3
|
+
* When: A developer replaces a framework default (auth, notifications, payments later).
|
|
4
4
|
* Where: Imported through the framework facade, never from framework internals.
|
|
5
5
|
* How: Interfaces stay deliberately loose so custom user shapes pass through.
|
|
6
6
|
*/
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
/** Registry key of the built-in auth provider contract. */
|
|
9
9
|
export const AUTH_PROVIDER_KEY = "auth";
|
|
10
10
|
|
|
11
|
+
/** Registry key of the built-in notification storage provider. */
|
|
12
|
+
export const NOTIFICATION_PROVIDER_KEY = "notification";
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* Registry key for any provider implementation.
|
|
13
16
|
*
|
|
@@ -15,25 +18,25 @@ export const AUTH_PROVIDER_KEY = "auth";
|
|
|
15
18
|
* still accepting any custom key, so a project can register its own contracts
|
|
16
19
|
* (payments, otp, storage, ...) without the framework having to know them.
|
|
17
20
|
*/
|
|
18
|
-
export type ProviderKey = typeof AUTH_PROVIDER_KEY | (string & {});
|
|
21
|
+
export type ProviderKey = typeof AUTH_PROVIDER_KEY | typeof NOTIFICATION_PROVIDER_KEY | (string & {});
|
|
19
22
|
|
|
20
23
|
export interface AuthProviderUser {
|
|
21
|
-
id: number;
|
|
24
|
+
id: number | string;
|
|
22
25
|
email?: string | null;
|
|
23
26
|
[key: string]: any;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface AuthProviderRefreshToken {
|
|
27
|
-
id: number;
|
|
30
|
+
id: number | string;
|
|
28
31
|
jti: string;
|
|
29
32
|
revoked?: boolean | null;
|
|
30
33
|
expiresAt?: Date | string | null;
|
|
31
|
-
userId?: number | null;
|
|
34
|
+
userId?: number | string | null;
|
|
32
35
|
[key: string]: any;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export interface AuthUserContext {
|
|
36
|
-
id: number;
|
|
39
|
+
id: number | string;
|
|
37
40
|
email?: string | null;
|
|
38
41
|
roleId?: number | null;
|
|
39
42
|
role?: string | null;
|
|
@@ -47,10 +50,77 @@ export interface AuthUserContact {
|
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
export interface AuthProvider {
|
|
50
|
-
getUserById(id: number): Promise<AuthProviderUser | null>;
|
|
53
|
+
getUserById(id: number | string): Promise<AuthProviderUser | null>;
|
|
51
54
|
findRefreshToken(jti: string): Promise<AuthProviderRefreshToken | null>;
|
|
52
|
-
revokeRefreshToken?(tokenId: number): Promise<void>;
|
|
53
|
-
deleteRefreshToken?(tokenId: number): Promise<void>;
|
|
55
|
+
revokeRefreshToken?(tokenId: number | string): Promise<void>;
|
|
56
|
+
deleteRefreshToken?(tokenId: number | string): Promise<void>;
|
|
54
57
|
getAuthContext?(user: AuthProviderUser, payload?: any): Promise<AuthUserContext | any>;
|
|
55
|
-
getUserContact?(userId: number): Promise<AuthUserContact | null>;
|
|
58
|
+
getUserContact?(userId: number | string): Promise<AuthUserContact | null>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Who a notification belongs to. `id` accepts a numeric auth id and a string
|
|
63
|
+
* id (UUID, ULID, ...) so replacing auth does not force a notification change.
|
|
64
|
+
* The provider always stores the id as text.
|
|
65
|
+
*/
|
|
66
|
+
export interface NotifiableTarget {
|
|
67
|
+
/** Notifiable class. Defaults to "user". */
|
|
68
|
+
type?: string;
|
|
69
|
+
id: number | string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Payload accepted by `NotificationProvider.create()`. */
|
|
73
|
+
export interface NotificationCreateInput {
|
|
74
|
+
type?: string;
|
|
75
|
+
title: string;
|
|
76
|
+
body?: string | null;
|
|
77
|
+
link?: string | null;
|
|
78
|
+
data?: Record<string, unknown> | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Normalized notification returned by every provider operation. */
|
|
82
|
+
export interface NotificationRecord {
|
|
83
|
+
id: string;
|
|
84
|
+
notifiableType: string;
|
|
85
|
+
notifiableId: string;
|
|
86
|
+
type: string;
|
|
87
|
+
title: string;
|
|
88
|
+
body: string | null;
|
|
89
|
+
data: Record<string, unknown> | null;
|
|
90
|
+
link: string | null;
|
|
91
|
+
readAt: string | null;
|
|
92
|
+
createdAt: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface NotificationListOptions {
|
|
96
|
+
page?: number;
|
|
97
|
+
perPage?: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface NotificationPage {
|
|
101
|
+
data: NotificationRecord[];
|
|
102
|
+
total: number;
|
|
103
|
+
unread: number;
|
|
104
|
+
page: number;
|
|
105
|
+
perPage: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Why: Keeps notification storage replaceable without touching `notify()` or
|
|
110
|
+
* the generated HTTP routes.
|
|
111
|
+
* When: The default `src/modules/notification` store is replaced by a custom
|
|
112
|
+
* implementation (a different table, another service, ...).
|
|
113
|
+
* Where: Registered through `registerNotificationProvider()`.
|
|
114
|
+
* How: The framework only reads through this contract; mail and realtime
|
|
115
|
+
* delivery stay in the runtime so a storage swap cannot disable them.
|
|
116
|
+
*/
|
|
117
|
+
export interface NotificationProvider {
|
|
118
|
+
create(target: NotifiableTarget, input: NotificationCreateInput): Promise<NotificationRecord>;
|
|
119
|
+
list(target: NotifiableTarget, options?: NotificationListOptions): Promise<NotificationPage>;
|
|
120
|
+
unreadCount(target: NotifiableTarget): Promise<number>;
|
|
121
|
+
markRead(target: NotifiableTarget, id: number | string): Promise<boolean>;
|
|
122
|
+
markAllRead(target: NotifiableTarget): Promise<number>;
|
|
123
|
+
remove(target: NotifiableTarget, id: number | string): Promise<boolean>;
|
|
124
|
+
/** Optional bulk cleanup when a notifiable target is deleted. */
|
|
125
|
+
deleteForTarget?(target: NotifiableTarget): Promise<number>;
|
|
56
126
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AUTH_PROVIDER_KEY,
|
|
3
|
+
type AuthProvider,
|
|
4
|
+
NOTIFICATION_PROVIDER_KEY,
|
|
5
|
+
type NotificationProvider,
|
|
6
|
+
type ProviderKey
|
|
7
|
+
} from "./contracts.js";
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* Why: Holds the app-wide provider implementations in one place.
|
|
@@ -60,3 +66,23 @@ export function hasAuthProvider(): boolean {
|
|
|
60
66
|
export function clearAuthProvider(): void {
|
|
61
67
|
clearProvider(AUTH_PROVIDER_KEY);
|
|
62
68
|
}
|
|
69
|
+
|
|
70
|
+
/** Registers the storage provider `notify()` and `/api/notification/*` use. */
|
|
71
|
+
export function registerNotificationProvider(provider: NotificationProvider): NotificationProvider {
|
|
72
|
+
return registerProvider(NOTIFICATION_PROVIDER_KEY, provider);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Returns the registered notification provider, or null when none was registered. */
|
|
76
|
+
export function getNotificationProvider(): NotificationProvider | null {
|
|
77
|
+
return getProvider<NotificationProvider>(NOTIFICATION_PROVIDER_KEY);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Reports whether a notification provider is registered. */
|
|
81
|
+
export function hasNotificationProvider(): boolean {
|
|
82
|
+
return hasProvider(NOTIFICATION_PROVIDER_KEY);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Removes the registered notification provider. Mainly useful in tests. */
|
|
86
|
+
export function clearNotificationProvider(): void {
|
|
87
|
+
clearProvider(NOTIFICATION_PROVIDER_KEY);
|
|
88
|
+
}
|
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="form-check mb-0" :class="props.topclass">
|
|
3
|
-
<input
|
|
4
|
-
:id="String(props.id)"
|
|
5
|
-
v-model="model"
|
|
6
|
-
class="form-check-input shadow-none
|
|
7
|
-
type="checkbox"
|
|
8
|
-
v-bind="$attrs"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
<template>
|
|
2
|
+
<div class="form-check mb-0" :class="props.topclass">
|
|
3
|
+
<input
|
|
4
|
+
:id="String(props.id)"
|
|
5
|
+
v-model="model"
|
|
6
|
+
class="form-check-input shadow-none"
|
|
7
|
+
type="checkbox"
|
|
8
|
+
v-bind="$attrs"
|
|
9
|
+
/>
|
|
10
|
+
<label v-if="props.label" class="form-check-label" :for="String(props.id)">{{
|
|
11
|
+
props.label
|
|
12
|
+
}}</label>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
defineOptions({ name: "Checkbox", inheritAttrs: false });
|
|
18
|
+
|
|
19
|
+
interface CheckboxProps {
|
|
20
|
+
id?: string | number;
|
|
21
|
+
label?: string;
|
|
22
|
+
topclass?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const props = defineProps<CheckboxProps>();
|
|
26
|
+
|
|
27
|
+
type CheckboxModel = boolean | Array<string | number>;
|
|
28
|
+
|
|
29
|
+
const model = defineModel<CheckboxModel>({ default: false });
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<style lang="scss" scoped></style>
|