create-m5kdev 0.35.0 → 0.37.2
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/package.json +3 -3
- package/templates/minimal-app/.dockerignore.tpl +6 -0
- package/templates/minimal-app/apps/e2e/playwright.config.ts.tpl +3 -0
- package/templates/minimal-app/apps/e2e/servers.ts.tpl +4 -0
- package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +2 -34
- package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +225 -5
- package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +47 -1
- package/templates/minimal-app/apps/e2e/tests/mcp-helpers.ts.tpl +406 -0
- package/templates/minimal-app/apps/e2e/tests/mcp.spec.ts.tpl +108 -0
- package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +290 -8
- package/templates/minimal-app/apps/expo/app.json.tpl +1 -0
- package/templates/minimal-app/apps/expo/package.json.tpl +4 -0
- package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +4 -1
- package/templates/minimal-app/apps/expo/src/app/notifications.tsx.tpl +70 -0
- package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +8 -16
- package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +4 -1
- package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +11 -2
- package/templates/minimal-app/apps/landing/vite.config.ts.tpl +2 -3
- package/templates/minimal-app/apps/server/AGENTS.md.tpl +10 -0
- package/templates/minimal-app/apps/server/db.e2e.ts.tpl +6 -40
- package/templates/minimal-app/apps/server/db.ts.tpl +6 -42
- package/templates/minimal-app/apps/server/drizzle/{0000_wealthy_absorbing_man.sql → 0000_whole_echo.sql} +102 -3
- package/templates/minimal-app/apps/server/drizzle/0001_yummy_spectrum.sql +26 -0
- package/templates/minimal-app/apps/server/drizzle/0002_crazy_lilith.sql +47 -0
- package/templates/minimal-app/apps/server/drizzle/0003_freezing_fantastic_four.sql +4 -0
- package/templates/minimal-app/apps/server/drizzle/0004_slow_ezekiel_stane.sql +99 -0
- package/templates/minimal-app/apps/server/drizzle/0005_wonderful_the_professor.sql +2 -0
- package/templates/minimal-app/apps/server/drizzle/0006_lazy_blue_marvel.sql +159 -0
- package/templates/minimal-app/apps/server/drizzle/0007_abnormal_ultimo.sql +2 -0
- package/templates/minimal-app/apps/server/drizzle/0008_steady_nekra.sql +87 -0
- package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +2625 -1780
- package/templates/minimal-app/apps/server/drizzle/meta/0001_snapshot.json.tpl +2671 -1796
- package/templates/minimal-app/apps/server/drizzle/meta/0002_snapshot.json.tpl +2716 -1818
- package/templates/minimal-app/apps/server/drizzle/meta/0003_snapshot.json.tpl +715 -164
- package/templates/minimal-app/apps/server/drizzle/meta/0004_snapshot.json.tpl +2498 -0
- package/templates/minimal-app/apps/server/drizzle/meta/0005_snapshot.json.tpl +2484 -0
- package/templates/minimal-app/apps/server/drizzle/meta/0006_snapshot.json.tpl +3603 -0
- package/templates/minimal-app/apps/server/drizzle/meta/0007_snapshot.json.tpl +3620 -0
- package/templates/minimal-app/apps/server/drizzle/meta/0008_snapshot.json.tpl +3620 -0
- package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +44 -9
- package/templates/minimal-app/apps/server/src/app.ts.tpl +12 -2
- package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +2 -2
- package/templates/minimal-app/apps/server/src/modules/posts/posts.mcp.ts.tpl +17 -0
- package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +11 -0
- package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +14 -5
- package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl +13 -36
- package/templates/minimal-app/apps/server/src/schema.ts.tpl +16 -2
- package/templates/minimal-app/apps/shared/package.json.tpl +1 -0
- package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +1 -1
- package/templates/minimal-app/apps/shared/src/modules/notification/notification.constants.ts.tpl +5 -0
- package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +1 -1
- package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +17 -1
- package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +10 -8
- package/templates/minimal-app/apps/webapp/src/modules/notification/NotificationPreferencesRoute.tsx.tpl +29 -0
- package/templates/minimal-app/apps/webapp/src/modules/notification/StarterWebPush.tsx.tpl +44 -0
- package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +8 -0
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +14 -13
- package/templates/minimal-app/template.manifest.json +9 -0
- package/templates/minimal-app/apps/server/drizzle/0001_third_ink.sql +0 -1
- package/templates/minimal-app/apps/server/drizzle/0002_concerned_proudstar.sql +0 -3
- package/templates/minimal-app/apps/server/drizzle/0003_silly_the_liberteens.sql +0 -27
|
@@ -5,30 +5,65 @@
|
|
|
5
5
|
{
|
|
6
6
|
"idx": 0,
|
|
7
7
|
"version": "6",
|
|
8
|
-
"when":
|
|
9
|
-
"tag": "
|
|
8
|
+
"when": 1787759631723,
|
|
9
|
+
"tag": "0000_whole_echo",
|
|
10
10
|
"breakpoints": true
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"idx": 1,
|
|
14
14
|
"version": "6",
|
|
15
|
-
"when":
|
|
16
|
-
"tag": "
|
|
15
|
+
"when": 1788197370969,
|
|
16
|
+
"tag": "0001_yummy_spectrum",
|
|
17
17
|
"breakpoints": true
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
"idx": 2,
|
|
21
21
|
"version": "6",
|
|
22
|
-
"when":
|
|
23
|
-
"tag": "
|
|
22
|
+
"when": 1788197859737,
|
|
23
|
+
"tag": "0002_crazy_lilith",
|
|
24
24
|
"breakpoints": true
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"idx": 3,
|
|
28
28
|
"version": "6",
|
|
29
|
-
"when":
|
|
30
|
-
"tag": "
|
|
29
|
+
"when": 1788198203749,
|
|
30
|
+
"tag": "0003_freezing_fantastic_four",
|
|
31
|
+
"breakpoints": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"idx": 4,
|
|
35
|
+
"version": "6",
|
|
36
|
+
"when": 1788198833389,
|
|
37
|
+
"tag": "0004_slow_ezekiel_stane",
|
|
38
|
+
"breakpoints": true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"idx": 5,
|
|
42
|
+
"version": "6",
|
|
43
|
+
"when": 1788253467922,
|
|
44
|
+
"tag": "0005_wonderful_the_professor",
|
|
45
|
+
"breakpoints": true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"idx": 6,
|
|
49
|
+
"version": "6",
|
|
50
|
+
"when": 1788787832546,
|
|
51
|
+
"tag": "0006_lazy_blue_marvel",
|
|
52
|
+
"breakpoints": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"idx": 7,
|
|
56
|
+
"version": "6",
|
|
57
|
+
"when": 1788788533642,
|
|
58
|
+
"tag": "0007_abnormal_ultimo",
|
|
59
|
+
"breakpoints": true
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"idx": 8,
|
|
63
|
+
"version": "6",
|
|
64
|
+
"when": 1788789159968,
|
|
65
|
+
"tag": "0008_steady_nekra",
|
|
31
66
|
"breakpoints": true
|
|
32
67
|
}
|
|
33
68
|
]
|
|
34
|
-
}
|
|
69
|
+
}
|
|
@@ -10,6 +10,9 @@ import { EmailPreviewModule } from "@m5kdev/backend/modules/email/email.preview.
|
|
|
10
10
|
// m5k:files:start
|
|
11
11
|
import { FileModule } from "@m5kdev/backend/modules/file/file.module";
|
|
12
12
|
// m5k:files:end
|
|
13
|
+
// m5k:mcp:start
|
|
14
|
+
import { McpModule } from "@m5kdev/backend/modules/mcp/mcp.module";
|
|
15
|
+
// m5k:mcp:end
|
|
13
16
|
// m5k:notifications:start
|
|
14
17
|
import { NotificationModule } from "@m5kdev/backend/modules/notification/notification.module";
|
|
15
18
|
// m5k:notifications:end
|
|
@@ -23,6 +26,9 @@ import {
|
|
|
23
26
|
APP_NAME,
|
|
24
27
|
APP_ROLES_CONFIG,
|
|
25
28
|
} from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
|
|
29
|
+
// m5k:notifications:start
|
|
30
|
+
import { NOTIFICATION_KINDS } from "{{PACKAGE_SCOPE}}/shared/modules/notification/notification.constants";
|
|
31
|
+
// m5k:notifications:end
|
|
26
32
|
// m5k:ai:start
|
|
27
33
|
import { mastra, openrouter } from "./modules/conversation/conversation.mastra";
|
|
28
34
|
// m5k:ai:end
|
|
@@ -91,7 +97,7 @@ export const builtBackendApp = createBackendApp(
|
|
|
91
97
|
outputDirectory: process.env.EMAIL_OUTPUT_DIRECTORY ?? ".emails",
|
|
92
98
|
},
|
|
93
99
|
auth: {
|
|
94
|
-
factory({ db, services, appConfig, i18n }) {
|
|
100
|
+
factory({ db, services, appConfig, i18n, mcp }) {
|
|
95
101
|
return createBetterAuth({
|
|
96
102
|
orm: db.orm,
|
|
97
103
|
schema: db.schema,
|
|
@@ -100,6 +106,7 @@ export const builtBackendApp = createBackendApp(
|
|
|
100
106
|
},
|
|
101
107
|
app: appConfig,
|
|
102
108
|
i18n,
|
|
109
|
+
mcp,
|
|
103
110
|
config: {
|
|
104
111
|
waitlist: enableWaitlist,
|
|
105
112
|
provisionedAccountEmailDomain:
|
|
@@ -115,6 +122,9 @@ export const builtBackendApp = createBackendApp(
|
|
|
115
122
|
[
|
|
116
123
|
new EmailModule(templates as never),
|
|
117
124
|
new AuthModule(),
|
|
125
|
+
// m5k:mcp:start
|
|
126
|
+
new McpModule(),
|
|
127
|
+
// m5k:mcp:end
|
|
118
128
|
// m5k:ai:start
|
|
119
129
|
new AIModule({
|
|
120
130
|
libs: { mastra, openrouter },
|
|
@@ -138,7 +148,7 @@ export const builtBackendApp = createBackendApp(
|
|
|
138
148
|
new DemoWorkflowModule(),
|
|
139
149
|
// m5k:workflows:end
|
|
140
150
|
// m5k:notifications:start
|
|
141
|
-
new NotificationModule(),
|
|
151
|
+
new NotificationModule({ kinds: NOTIFICATION_KINDS }),
|
|
142
152
|
// m5k:notifications:end
|
|
143
153
|
new PostsModule(),
|
|
144
154
|
new EmailPreviewModule({ allowDelete: process.env.NODE_ENV !== "production" }),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { members, organizations,
|
|
1
|
+
import { members, organizations, users } from "@m5kdev/backend/modules/auth/auth.db";
|
|
2
2
|
import { integer, sqliteTable as table, text } from "drizzle-orm/sqlite-core";
|
|
3
3
|
import { v4 as uuidv4 } from "uuid";
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ export const posts = table("posts", {
|
|
|
13
13
|
organizationId: text("organization_id").references(() => organizations.id, {
|
|
14
14
|
onDelete: "set null",
|
|
15
15
|
}),
|
|
16
|
-
teamId: text("team_id")
|
|
16
|
+
teamId: text("team_id"),
|
|
17
17
|
title: text("title").notNull(),
|
|
18
18
|
slug: text("slug").notNull().unique(),
|
|
19
19
|
excerpt: text("excerpt"),
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineMcpCall } from "@m5kdev/backend/modules/mcp/mcp.define";
|
|
2
|
+
import type { McpOrganizationCallDefinition } from "@m5kdev/backend/modules/mcp/mcp.types";
|
|
3
|
+
import { postSchemas } from "./posts.dto";
|
|
4
|
+
import type { PostsService } from "./posts.service";
|
|
5
|
+
|
|
6
|
+
export function createPostsMcp(posts: PostsService): Record<string, McpOrganizationCallDefinition> {
|
|
7
|
+
return {
|
|
8
|
+
"list-posts": defineMcpCall()
|
|
9
|
+
.description("List posts in the selected Organization")
|
|
10
|
+
.input(postSchemas.input.list)
|
|
11
|
+
.handle(async (input, actor) => posts.list(input, { actor, user: { id: actor.userId } })),
|
|
12
|
+
"create-post": defineMcpCall()
|
|
13
|
+
.description("Create a draft post in the selected Organization")
|
|
14
|
+
.input(postSchemas.input.create)
|
|
15
|
+
.handle(async (input, actor) => posts.create(input, { actor, user: { id: actor.userId } })),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// biome-ignore-all assist/source/organizeImports: feature-gated posts.mcp import stays in marker blocks
|
|
1
2
|
import { createBackendRouterMap } from "@m5kdev/backend/app";
|
|
2
3
|
import {
|
|
3
4
|
BaseModule,
|
|
@@ -10,6 +11,10 @@ import { postsGrants } from "./posts.grants";
|
|
|
10
11
|
import { PostsRepository } from "./posts.repository";
|
|
11
12
|
import { PostsService } from "./posts.service";
|
|
12
13
|
import { createPostsTRPC } from "./posts.trpc";
|
|
14
|
+
// m5k:mcp:start
|
|
15
|
+
import type { ModuleMcpContext } from "@m5kdev/backend/modules/base/base.module";
|
|
16
|
+
import { createPostsMcp } from "./posts.mcp";
|
|
17
|
+
// m5k:mcp:end
|
|
13
18
|
|
|
14
19
|
type PostsModuleDeps = Record<string, never>;
|
|
15
20
|
type PostsModuleTables = typeof postsTables;
|
|
@@ -54,4 +59,10 @@ export class PostsModule extends BaseModule<
|
|
|
54
59
|
override trpc({ trpc, services }: ModuleTRPCContext<PostsModuleDeps, PostsModuleServices>) {
|
|
55
60
|
return createBackendRouterMap("posts", createPostsTRPC(trpc, services.posts));
|
|
56
61
|
}
|
|
62
|
+
|
|
63
|
+
// m5k:mcp:start
|
|
64
|
+
override mcp({ services }: ModuleMcpContext<PostsModuleDeps, PostsModuleServices>) {
|
|
65
|
+
return createPostsMcp(services.posts);
|
|
66
|
+
}
|
|
67
|
+
// m5k:mcp:end
|
|
57
68
|
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
import type { AuthenticatedActor } from "@m5kdev/backend/base/base.actor";
|
|
2
|
+
import type { ServiceProcedureContext } from "@m5kdev/backend/base/base.procedure";
|
|
1
3
|
import { BasePermissionService } from "@m5kdev/backend/modules/base/base.service";
|
|
2
4
|
import { serializeSpanValue, withSpan } from "@m5kdev/backend/utils/telemetry";
|
|
3
|
-
import type { Context } from "@m5kdev/backend/utils/trpc";
|
|
4
5
|
import { err, ok } from "neverthrow";
|
|
5
6
|
import { postSchemas } from "./posts.dto";
|
|
6
7
|
import type { PostsRepository } from "./posts.repository";
|
|
7
8
|
import { createExcerpt, slugify } from "./posts.utils";
|
|
8
9
|
|
|
10
|
+
export interface PostsProcedureContext extends ServiceProcedureContext {
|
|
11
|
+
actor: AuthenticatedActor;
|
|
12
|
+
user: { id: string };
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
export class PostsService extends BasePermissionService<
|
|
10
16
|
{ posts: PostsRepository },
|
|
11
17
|
Record<string, never>,
|
|
12
|
-
|
|
18
|
+
PostsProcedureContext
|
|
13
19
|
> {
|
|
14
20
|
readonly list = this.procedure("list")
|
|
15
21
|
.input(postSchemas.input.list)
|
|
@@ -37,6 +43,9 @@ export class PostsService extends BasePermissionService<
|
|
|
37
43
|
.output(postSchemas.output.single)
|
|
38
44
|
.access({
|
|
39
45
|
action: "write",
|
|
46
|
+
entities: ({ ctx }) => ({
|
|
47
|
+
organizationId: ctx.actor.organizationId,
|
|
48
|
+
}),
|
|
40
49
|
})
|
|
41
50
|
.handle(async ({ input, ctx }) => {
|
|
42
51
|
const uniqueSlug = await this.repository.posts.resolveUniqueSlug({
|
|
@@ -47,10 +56,10 @@ export class PostsService extends BasePermissionService<
|
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
return this.repository.posts.create({
|
|
50
|
-
authorUserId: ctx.
|
|
59
|
+
authorUserId: ctx.actor.userId,
|
|
51
60
|
memberId: ctx.actor.memberId ?? null,
|
|
52
|
-
organizationId: ctx.
|
|
53
|
-
teamId:
|
|
61
|
+
organizationId: ctx.actor.organizationId,
|
|
62
|
+
teamId: null,
|
|
54
63
|
title: input.title.trim(),
|
|
55
64
|
slug: uniqueSlug.value,
|
|
56
65
|
excerpt: createExcerpt(input.excerpt, input.content),
|
package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type ModuleExpressContext,
|
|
4
4
|
type TableMap,
|
|
5
5
|
} from "@m5kdev/backend/modules/base/base.module";
|
|
6
|
-
import { and, desc, eq, isNotNull } from "drizzle-orm";
|
|
6
|
+
import { and, desc, eq, isNotNull, isNull } from "drizzle-orm";
|
|
7
7
|
import type { Request, Response } from "express";
|
|
8
8
|
import { v4 as uuidv4 } from "uuid";
|
|
9
9
|
|
|
@@ -77,13 +77,14 @@ export class TestHarnessModule extends BaseModule<
|
|
|
77
77
|
type: db.schema.organizations.type,
|
|
78
78
|
parentId: db.schema.organizations.parentId,
|
|
79
79
|
role: db.schema.members.role,
|
|
80
|
+
memberId: db.schema.members.id,
|
|
80
81
|
})
|
|
81
82
|
.from(db.schema.members)
|
|
82
83
|
.innerJoin(
|
|
83
84
|
db.schema.organizations,
|
|
84
85
|
eq(db.schema.members.organizationId, db.schema.organizations.id)
|
|
85
86
|
)
|
|
86
|
-
.where(eq(db.schema.members.userId, user.id));
|
|
87
|
+
.where(and(eq(db.schema.members.userId, user.id), isNull(db.schema.members.deletedAt)));
|
|
87
88
|
|
|
88
89
|
const [credentialAccount] = await db.orm
|
|
89
90
|
.select({ id: db.schema.accounts.id })
|
|
@@ -99,21 +100,16 @@ export class TestHarnessModule extends BaseModule<
|
|
|
99
100
|
|
|
100
101
|
const accountClaims = await db.orm
|
|
101
102
|
.select({
|
|
102
|
-
id: db.schema.
|
|
103
|
-
status: db.schema.
|
|
104
|
-
claimUserId: db.schema.
|
|
105
|
-
claimedEmail: db.schema.
|
|
106
|
-
claimedAt: db.schema.
|
|
107
|
-
expiresAt: db.schema.
|
|
103
|
+
id: db.schema.accountClaims.id,
|
|
104
|
+
status: db.schema.accountClaims.status,
|
|
105
|
+
claimUserId: db.schema.accountClaims.claimUserId,
|
|
106
|
+
claimedEmail: db.schema.accountClaims.claimedEmail,
|
|
107
|
+
claimedAt: db.schema.accountClaims.claimedAt,
|
|
108
|
+
expiresAt: db.schema.accountClaims.expiresAt,
|
|
108
109
|
})
|
|
109
|
-
.from(db.schema.
|
|
110
|
-
.where(
|
|
111
|
-
|
|
112
|
-
eq(db.schema.waitlist.type, "ACCOUNT_CLAIM"),
|
|
113
|
-
eq(db.schema.waitlist.claimUserId, user.id)
|
|
114
|
-
)
|
|
115
|
-
)
|
|
116
|
-
.orderBy(desc(db.schema.waitlist.createdAt));
|
|
110
|
+
.from(db.schema.accountClaims)
|
|
111
|
+
.where(eq(db.schema.accountClaims.claimUserId, user.id))
|
|
112
|
+
.orderBy(desc(db.schema.accountClaims.createdAt));
|
|
117
113
|
|
|
118
114
|
const [latestSession] = await db.orm
|
|
119
115
|
.select({
|
|
@@ -123,7 +119,6 @@ export class TestHarnessModule extends BaseModule<
|
|
|
123
119
|
activeOrganizationRole: db.schema.sessions.activeOrganizationRole,
|
|
124
120
|
activeOrganizationMemberId: db.schema.sessions.activeOrganizationMemberId,
|
|
125
121
|
activeOrganizationType: db.schema.sessions.activeOrganizationType,
|
|
126
|
-
activeTeamId: db.schema.sessions.activeTeamId,
|
|
127
122
|
impersonatedBy: db.schema.sessions.impersonatedBy,
|
|
128
123
|
createdAt: db.schema.sessions.createdAt,
|
|
129
124
|
})
|
|
@@ -172,7 +167,6 @@ export class TestHarnessModule extends BaseModule<
|
|
|
172
167
|
const now = new Date();
|
|
173
168
|
const userId = uuidv4();
|
|
174
169
|
const organizationId = uuidv4();
|
|
175
|
-
const teamId = uuidv4();
|
|
176
170
|
const claimId = createClaim ? uuidv4() : null;
|
|
177
171
|
|
|
178
172
|
await db.orm.transaction(async (tx) => {
|
|
@@ -202,26 +196,9 @@ export class TestHarnessModule extends BaseModule<
|
|
|
202
196
|
createdAt: now,
|
|
203
197
|
});
|
|
204
198
|
|
|
205
|
-
await tx.insert(db.schema.teams).values({
|
|
206
|
-
id: teamId,
|
|
207
|
-
name: "Editorial",
|
|
208
|
-
organizationId,
|
|
209
|
-
createdAt: now,
|
|
210
|
-
updatedAt: now,
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
await tx.insert(db.schema.teamMembers).values({
|
|
214
|
-
id: uuidv4(),
|
|
215
|
-
teamId,
|
|
216
|
-
userId,
|
|
217
|
-
role: "owner",
|
|
218
|
-
createdAt: now,
|
|
219
|
-
});
|
|
220
|
-
|
|
221
199
|
if (claimId) {
|
|
222
|
-
await tx.insert(db.schema.
|
|
200
|
+
await tx.insert(db.schema.accountClaims).values({
|
|
223
201
|
id: claimId,
|
|
224
|
-
type: "ACCOUNT_CLAIM",
|
|
225
202
|
status: "INVITED",
|
|
226
203
|
claimUserId: userId,
|
|
227
204
|
code: uuidv4(),
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
// biome-ignore-all assist/source/organizeImports: feature-gated table exports stay in marker blocks
|
|
2
2
|
export {
|
|
3
3
|
accountClaimMagicLinks,
|
|
4
|
+
accountClaims,
|
|
4
5
|
accounts,
|
|
5
6
|
apikeys,
|
|
6
7
|
invitations,
|
|
7
8
|
members,
|
|
8
9
|
organizations,
|
|
9
10
|
sessions,
|
|
10
|
-
teamMembers,
|
|
11
|
-
teams,
|
|
12
11
|
users,
|
|
13
12
|
verifications,
|
|
14
13
|
waitlist,
|
|
15
14
|
} from "@m5kdev/backend/modules/auth/auth.db";
|
|
15
|
+
// m5k:mcp:start
|
|
16
|
+
export {
|
|
17
|
+
jwks,
|
|
18
|
+
oauthAccessTokens,
|
|
19
|
+
oauthClientAssertions,
|
|
20
|
+
oauthClientResources,
|
|
21
|
+
oauthClients,
|
|
22
|
+
oauthConsents,
|
|
23
|
+
oauthRefreshTokens,
|
|
24
|
+
oauthResources,
|
|
25
|
+
} from "@m5kdev/backend/modules/auth/auth.oauth.db";
|
|
26
|
+
export { mcpAllowlistEntries } from "@m5kdev/backend/modules/mcp/mcp.db";
|
|
27
|
+
// m5k:mcp:end
|
|
16
28
|
// m5k:ai:start
|
|
17
29
|
export { aiUsage, chats } from "@m5kdev/backend/modules/ai/ai.db";
|
|
18
30
|
// m5k:ai:end
|
|
@@ -22,7 +34,9 @@ export { files } from "@m5kdev/backend/modules/file/file.db";
|
|
|
22
34
|
// m5k:notifications:start
|
|
23
35
|
export {
|
|
24
36
|
notificationDevices,
|
|
37
|
+
notificationPreferences,
|
|
25
38
|
notificationSendLogs,
|
|
39
|
+
notifications,
|
|
26
40
|
} from "@m5kdev/backend/modules/notification/notification.db";
|
|
27
41
|
// m5k:notifications:end
|
|
28
42
|
// m5k:workflows:start
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"./modules/app/*": "./src/modules/app/*.ts",
|
|
22
22
|
"./modules/conversation/*": "./src/modules/conversation/*.ts",
|
|
23
23
|
"./modules/demo-workflow/*": "./src/modules/demo-workflow/*.ts",
|
|
24
|
+
"./modules/notification/*": "./src/modules/notification/*.ts",
|
|
24
25
|
"./modules/posts/*": "./src/modules/posts/*.ts"
|
|
25
26
|
}
|
|
26
27
|
}
|
|
@@ -18,7 +18,7 @@ export const APP_ROLES_CONFIG = defineAuthRoles({
|
|
|
18
18
|
organization: {
|
|
19
19
|
roles: ["member", "admin", "owner"],
|
|
20
20
|
managerRoles: ["admin", "owner"],
|
|
21
|
-
assignableRoles: ["member", "admin"
|
|
21
|
+
assignableRoles: ["member", "admin"],
|
|
22
22
|
defaultRole: "member",
|
|
23
23
|
},
|
|
24
24
|
team: {
|
package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/** Re-export shared
|
|
1
|
+
/** Re-export shared Notification contracts from commons. */
|
|
2
2
|
export * from "@m5kdev/commons/modules/notification/notification.constants";
|
|
3
3
|
export * from "@m5kdev/commons/modules/notification/notification.schema";
|
|
@@ -7,8 +7,11 @@ import { AppSidebarHeader } from "@m5kdev/web-ui/modules/app/components/AppSideb
|
|
|
7
7
|
import { AppSidebarUser } from "@m5kdev/web-ui/modules/app/components/AppSidebarUser";
|
|
8
8
|
import { AuthOrganizationSelect } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationSelect";
|
|
9
9
|
import { AuthUtilityImpersonationBanner } from "@m5kdev/web-ui/modules/auth/components/AuthUtilityImpersonationBanner";
|
|
10
|
+
// m5k:notifications:start
|
|
11
|
+
import { NotificationSidebarInbox } from "@m5kdev/web-ui/modules/notification/components/NotificationSidebarInbox";
|
|
12
|
+
// m5k:notifications:end
|
|
10
13
|
import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
|
|
11
|
-
import { FileIcon, UsersIcon } from "lucide-react";
|
|
14
|
+
import { BellIcon, FileIcon, UsersIcon } from "lucide-react";
|
|
12
15
|
// m5k:ai:start
|
|
13
16
|
import { SparklesIcon } from "lucide-react";
|
|
14
17
|
// m5k:ai:end
|
|
@@ -21,6 +24,9 @@ import { WorkflowIcon } from "lucide-react";
|
|
|
21
24
|
import { useState } from "react";
|
|
22
25
|
import { useTranslation } from "react-i18next";
|
|
23
26
|
import { useNavigate } from "react-router";
|
|
27
|
+
// m5k:notifications:start
|
|
28
|
+
import { StarterWebPush } from "./modules/notification/StarterWebPush";
|
|
29
|
+
// m5k:notifications:end
|
|
24
30
|
|
|
25
31
|
export function Layout() {
|
|
26
32
|
const { data: session } = useSession();
|
|
@@ -42,6 +48,9 @@ export function Layout() {
|
|
|
42
48
|
header: <AppSidebarHeader logo={{ src: "/logo.svg", alt: APP_NAME }} title={APP_NAME} />,
|
|
43
49
|
content: (
|
|
44
50
|
<>
|
|
51
|
+
{/* m5k:notifications:start */}
|
|
52
|
+
<NotificationSidebarInbox enablePush={<StarterWebPush />} />
|
|
53
|
+
{/* m5k:notifications:end */}
|
|
45
54
|
<div className="px-2 group-data-[collapsible=icon]:hidden">
|
|
46
55
|
<AuthOrganizationSelect />
|
|
47
56
|
</div>
|
|
@@ -73,6 +82,13 @@ export function Layout() {
|
|
|
73
82
|
link: "/workflows",
|
|
74
83
|
},
|
|
75
84
|
// m5k:workflows:end
|
|
85
|
+
// m5k:notifications:start
|
|
86
|
+
{
|
|
87
|
+
label: t("layout.navigation.notifications"),
|
|
88
|
+
icon: <BellIcon />,
|
|
89
|
+
link: "/notifications",
|
|
90
|
+
},
|
|
91
|
+
// m5k:notifications:end
|
|
76
92
|
{
|
|
77
93
|
label: t("layout.navigation.members"),
|
|
78
94
|
icon: <UsersIcon />,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// biome-ignore-all assist/source/organizeImports: feature-gated route imports stay in marker blocks
|
|
2
2
|
import { syncI18nLocale } from "@m5kdev/frontend/modules/app/utils/locale";
|
|
3
3
|
import { AuthAdminRouter } from "@m5kdev/web-ui/modules/auth/components/AuthAdminRouter";
|
|
4
|
-
import { AuthOrganizationAcceptInvitationRoute } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationAcceptInvitationRoute";
|
|
5
4
|
import { AuthOrganizationRouter } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationRouter";
|
|
6
5
|
import { AuthPublicRouter } from "@m5kdev/web-ui/modules/auth/components/AuthPublicRouter";
|
|
7
6
|
import { AuthUserRouter } from "@m5kdev/web-ui/modules/auth/components/AuthUserRouter";
|
|
@@ -20,6 +19,9 @@ import { FilesRoute } from "@/modules/files/FilesRoute";
|
|
|
20
19
|
// m5k:workflows:start
|
|
21
20
|
import { WorkflowsRoute } from "@/modules/workflows/WorkflowsRoute";
|
|
22
21
|
// m5k:workflows:end
|
|
22
|
+
// m5k:notifications:start
|
|
23
|
+
import { NotificationPreferencesRoute } from "@/modules/notification/NotificationPreferencesRoute";
|
|
24
|
+
// m5k:notifications:end
|
|
23
25
|
import { Layout } from "./Layout";
|
|
24
26
|
|
|
25
27
|
const preferenceSchema = z.object({
|
|
@@ -68,13 +70,6 @@ export function Router() {
|
|
|
68
70
|
onLocaleChange: syncI18nLocale,
|
|
69
71
|
})}
|
|
70
72
|
|
|
71
|
-
{/* public: a logged-out invitee must reach this route so it can send
|
|
72
|
-
them to signup — inside ProtectedRoutes they'd bounce to /login */}
|
|
73
|
-
<Route
|
|
74
|
-
path="/organization/accept-invitation"
|
|
75
|
-
element={<AuthOrganizationAcceptInvitationRoute />}
|
|
76
|
-
/>
|
|
77
|
-
|
|
78
73
|
<Route
|
|
79
74
|
element={
|
|
80
75
|
<ProtectedRoutes>
|
|
@@ -93,6 +88,13 @@ export function Router() {
|
|
|
93
88
|
// m5k:workflows:start
|
|
94
89
|
<Route key="workflows" path="workflows" element={<WorkflowsRoute />} />,
|
|
95
90
|
// m5k:workflows:end
|
|
91
|
+
// m5k:notifications:start
|
|
92
|
+
<Route
|
|
93
|
+
key="notifications"
|
|
94
|
+
path="notifications"
|
|
95
|
+
element={<NotificationPreferencesRoute />}
|
|
96
|
+
/>,
|
|
97
|
+
// m5k:notifications:end
|
|
96
98
|
]}
|
|
97
99
|
|
|
98
100
|
{AuthUserRouter({
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Card } from "@heroui/react";
|
|
2
|
+
import { NotificationPreferences } from "@m5kdev/web-ui/modules/notification/components/NotificationPreferences";
|
|
3
|
+
import { NOTIFICATION_KINDS } from "{{PACKAGE_SCOPE}}/shared/modules/notification/notification.constants";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { StarterWebPush } from "./StarterWebPush";
|
|
6
|
+
|
|
7
|
+
export function NotificationPreferencesRoute() {
|
|
8
|
+
const { t } = useTranslation("{{APP_SLUG}}");
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="grid gap-6 p-4" data-testid="notification-preferences-route">
|
|
12
|
+
<div className="justify-self-start">
|
|
13
|
+
<StarterWebPush />
|
|
14
|
+
</div>
|
|
15
|
+
<Card>
|
|
16
|
+
<Card.Header className="flex flex-col gap-1">
|
|
17
|
+
<p className="text-xs font-medium uppercase tracking-wide text-muted">
|
|
18
|
+
{t("notifications.hero.eyebrow")}
|
|
19
|
+
</p>
|
|
20
|
+
<Card.Title>{t("notifications.hero.title")}</Card.Title>
|
|
21
|
+
<Card.Description>{t("notifications.hero.body")}</Card.Description>
|
|
22
|
+
</Card.Header>
|
|
23
|
+
<Card.Content>
|
|
24
|
+
<NotificationPreferences kinds={NOTIFICATION_KINDS} offerEmail />
|
|
25
|
+
</Card.Content>
|
|
26
|
+
</Card>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Button } from "@heroui/react";
|
|
2
|
+
import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
|
|
3
|
+
import { useWebPush } from "@m5kdev/web-ui/hooks/useWebPush";
|
|
4
|
+
import { BellRingIcon } from "lucide-react";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import { useTranslation } from "react-i18next";
|
|
7
|
+
import { useTRPC } from "../../utils/trpc";
|
|
8
|
+
|
|
9
|
+
export function StarterWebPush(): ReactNode {
|
|
10
|
+
const { t } = useTranslation("{{APP_SLUG}}");
|
|
11
|
+
const { data: session } = useSession();
|
|
12
|
+
const trpc = useTRPC();
|
|
13
|
+
const webPush = useWebPush({
|
|
14
|
+
enabled: Boolean(session?.user?.id),
|
|
15
|
+
messages: {
|
|
16
|
+
unsupported: t("layout.push.unsupported"),
|
|
17
|
+
denied: t("layout.push.denied"),
|
|
18
|
+
noVapid: t("layout.push.noVapid"),
|
|
19
|
+
badSubscription: t("layout.push.badSubscription"),
|
|
20
|
+
failed: t("layout.push.failed"),
|
|
21
|
+
enabled: t("layout.push.enabled"),
|
|
22
|
+
},
|
|
23
|
+
vapidPublicKeyQuery: trpc.notification.vapidPublicKey.queryOptions(),
|
|
24
|
+
registerDeviceMutation: trpc.notification.registerDevice.mutationOptions(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!session?.user?.id || webPush.flowStatus === "done") return null;
|
|
28
|
+
|
|
29
|
+
const label = webPush.permission === "denied" ? t("layout.push.denied") : t("layout.push.cta");
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Button
|
|
33
|
+
size="sm"
|
|
34
|
+
variant="secondary"
|
|
35
|
+
isDisabled={!webPush.canSubscribe || webPush.isWorking}
|
|
36
|
+
onPress={() => {
|
|
37
|
+
void webPush.subscribe();
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<BellRingIcon className="size-4" />
|
|
41
|
+
{label}
|
|
42
|
+
</Button>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"organization": "Organization",
|
|
20
20
|
"account": "Account",
|
|
21
21
|
"members": "Members",
|
|
22
|
+
"notifications": "Notifications",
|
|
22
23
|
"childOrgs": "Child orgs",
|
|
23
24
|
"orgPreferences": "Org preferences",
|
|
24
25
|
"profile": "Profile",
|
|
@@ -184,6 +185,13 @@
|
|
|
184
185
|
"failed": "Upload failed."
|
|
185
186
|
}
|
|
186
187
|
},
|
|
188
|
+
"notifications": {
|
|
189
|
+
"hero": {
|
|
190
|
+
"eyebrow": "This membership",
|
|
191
|
+
"title": "Notification preferences",
|
|
192
|
+
"body": "Mute channels for this organization. Other organizations keep their own settings."
|
|
193
|
+
}
|
|
194
|
+
},
|
|
187
195
|
"auth": {
|
|
188
196
|
"header": {
|
|
189
197
|
"eyebrow": "Framework Starter",
|
|
@@ -7,17 +7,18 @@ catalogs:
|
|
|
7
7
|
"@heroui/react": 3.0.3
|
|
8
8
|
"@heroui/styles": 3.0.3
|
|
9
9
|
"@libsql/client": 0.17.4
|
|
10
|
-
"@m5kdev/backend": 0.
|
|
11
|
-
"@m5kdev/commons": 0.
|
|
12
|
-
"@m5kdev/config": 0.
|
|
13
|
-
"@m5kdev/email": 0.
|
|
14
|
-
"@m5kdev/
|
|
15
|
-
"@m5kdev/
|
|
16
|
-
"@m5kdev/module-
|
|
17
|
-
"@m5kdev/module-
|
|
18
|
-
"@m5kdev/module-
|
|
19
|
-
"@m5kdev/module-
|
|
20
|
-
"@m5kdev/
|
|
10
|
+
"@m5kdev/backend": 0.37.2
|
|
11
|
+
"@m5kdev/commons": 0.37.2
|
|
12
|
+
"@m5kdev/config": 0.37.2
|
|
13
|
+
"@m5kdev/email": 0.37.2
|
|
14
|
+
"@m5kdev/expo": 0.37.2
|
|
15
|
+
"@m5kdev/frontend": 0.37.2
|
|
16
|
+
"@m5kdev/module-clay": 0.37.2
|
|
17
|
+
"@m5kdev/module-docx": 0.37.2
|
|
18
|
+
"@m5kdev/module-pdf": 0.37.2
|
|
19
|
+
"@m5kdev/module-social": 0.37.2
|
|
20
|
+
"@m5kdev/module-video": 0.37.2
|
|
21
|
+
"@m5kdev/web-ui": 0.37.2
|
|
21
22
|
"@mastra/core": 1.51.0
|
|
22
23
|
"@openrouter/ai-sdk-provider": 3.0.0
|
|
23
24
|
"@opentelemetry/api": 1.9.0
|
|
@@ -34,7 +35,7 @@ catalogs:
|
|
|
34
35
|
"@types/react-dom": 19.2.3
|
|
35
36
|
"@vitejs/plugin-react": 4.2.0
|
|
36
37
|
babel-plugin-react-compiler: 1.0.0
|
|
37
|
-
better-auth: 1.
|
|
38
|
+
better-auth: 1.7.2
|
|
38
39
|
dotenv: 16.6.1
|
|
39
40
|
drizzle-kit: 0.31.10
|
|
40
41
|
drizzle-orm: 0.45.2
|
|
@@ -66,5 +67,5 @@ catalogs:
|
|
|
66
67
|
vite: 7.0.4
|
|
67
68
|
vite-plugin-i18next-loader: 3.1.2
|
|
68
69
|
vite-tsconfig-paths: 5.1.4
|
|
69
|
-
zod: 4.
|
|
70
|
+
zod: 4.4.3
|
|
70
71
|
catalogMode: strict
|