@tulip-systems/create-app 0.13.0 → 0.15.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 (30) hide show
  1. package/dist/templates/basic/package.json +5 -5
  2. package/dist/templates/code/package.json +18 -19
  3. package/dist/templates/code/public/sw.js +1 -0
  4. package/dist/templates/code/src/app/admin/notifications/_components/push-subscription-card.tsx +20 -0
  5. package/dist/templates/code/src/app/admin/notifications/_components/tables.tsx +8 -3
  6. package/dist/templates/code/src/app/admin/notifications/_components/toolbars.tsx +35 -10
  7. package/dist/templates/code/src/app/admin/notifications/_components/unread-badge.tsx +17 -0
  8. package/dist/templates/code/src/app/admin/notifications/_config/columns-data.tsx +52 -43
  9. package/dist/templates/code/src/app/admin/notifications/_config/commands.tsx +43 -7
  10. package/dist/templates/code/src/app/admin/notifications/_lib/listeners.server.ts +126 -52
  11. package/dist/templates/code/src/app/admin/notifications/_lib/router.ts +53 -53
  12. package/dist/templates/code/src/app/admin/notifications/_lib/schema.ts +57 -17
  13. package/dist/templates/code/src/app/admin/notifications/layout.tsx +10 -2
  14. package/dist/templates/code/src/app/admin/notifications/page.tsx +7 -10
  15. package/dist/templates/code/src/app/admin/projects/_lib/notifications.ts +53 -0
  16. package/dist/templates/code/src/app/admin/settings/layout.tsx +5 -0
  17. package/dist/templates/code/src/app/admin/settings/notifications/page.tsx +59 -0
  18. package/dist/templates/code/src/app/admin/tasks/_lib/notifications.ts +83 -0
  19. package/dist/templates/code/src/app/api/notifications/[id]/open/route.ts +42 -0
  20. package/dist/templates/code/src/app/globals.css +1 -0
  21. package/dist/templates/code/src/lib/config/paths.tsx +6 -0
  22. package/dist/templates/code/src/server/auth/init.ts +1 -0
  23. package/dist/templates/code/src/server/notifications/channels.ts +57 -0
  24. package/dist/templates/code/src/server/notifications/definitions.ts +20 -0
  25. package/dist/templates/code/src/server/notifications/init.ts +49 -0
  26. package/dist/templates/spark/package.json +13 -13
  27. package/package.json +1 -1
  28. package/dist/templates/code/src/app/admin/notifications/_components/command-toggle-push.tsx +0 -115
  29. package/dist/templates/code/src/app/admin/notifications/_lib/push.server.ts +0 -80
  30. package/dist/templates/code/src/app/admin/notifications/_lib/validators.ts +0 -19
@@ -0,0 +1,49 @@
1
+ import "server-cli-only";
2
+
3
+ import type { Permission } from "@tulip-systems/auth/permissions";
4
+ import { notificationsDrizzleAdapter } from "@tulip-systems/notifications/drizzle";
5
+ import { Notifications } from "@tulip-systems/notifications/server";
6
+ import { inArray } from "drizzle-orm";
7
+ import * as schema from "@/app/admin/notifications/_lib/schema";
8
+ import { auth } from "@/server/auth/init";
9
+ import type { AccessControl } from "@/server/auth/permissions";
10
+ import { users } from "@/server/auth/schema";
11
+ import { db } from "@/server/db/init";
12
+ import { notificationEmailChannel, notificationWebPushChannel } from "./channels";
13
+ import { notificationDefinitions } from "./definitions";
14
+
15
+ declare module "@tulip-systems/notifications" {
16
+ interface NotificationRegister {
17
+ permission: Permission<AccessControl>;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Initializes the notifications service for the application. Notifications that require a
23
+ * permission only reach users whose role grants it, checked by better-auth like the router's
24
+ * permission middleware.
25
+ */
26
+ export const notifications = Notifications.init({
27
+ database: notificationsDrizzleAdapter({ db, schema }),
28
+ channels: [notificationWebPushChannel, notificationEmailChannel],
29
+ defaultPreferences: {
30
+ types: {
31
+ "project.created": { email: false },
32
+ "project.status-changed": { email: false },
33
+ "task.created": { email: false },
34
+ },
35
+ },
36
+ definitions: notificationDefinitions,
37
+ recipients: (ids) =>
38
+ db
39
+ .select({ id: users.id, name: users.name, email: users.email })
40
+ .from(users)
41
+ .where(inArray(users.id, ids)),
42
+ authorize: async (recipient, permission) => {
43
+ const { success } = await auth.api.userHasPermission({
44
+ body: { userId: recipient.id, permissions: permission },
45
+ });
46
+
47
+ return success;
48
+ },
49
+ });
@@ -52,19 +52,19 @@
52
52
  "@tiptap/extension-text-style": "catalog:",
53
53
  "@tiptap/extension-underline": "catalog:",
54
54
  "@tiptap/extensions": "catalog:",
55
- "@tulip-systems/app": "0.13.0",
56
- "@tulip-systems/auth": "0.13.0",
57
- "@tulip-systems/commands": "0.13.0",
58
- "@tulip-systems/core": "0.13.0",
59
- "@tulip-systems/data-tables": "0.13.0",
60
- "@tulip-systems/drive": "0.13.0",
61
- "@tulip-systems/drive-local": "0.13.0",
62
- "@tulip-systems/editor": "0.13.0",
63
- "@tulip-systems/editor-storage": "0.13.0",
64
- "@tulip-systems/inline": "0.13.0",
65
- "@tulip-systems/query": "0.13.0",
66
- "@tulip-systems/storage": "0.13.0",
67
- "@tulip-systems/ui": "0.13.0",
55
+ "@tulip-systems/app": "0.15.0",
56
+ "@tulip-systems/auth": "0.15.0",
57
+ "@tulip-systems/commands": "0.15.0",
58
+ "@tulip-systems/core": "0.15.0",
59
+ "@tulip-systems/data-tables": "0.15.0",
60
+ "@tulip-systems/drive": "0.15.0",
61
+ "@tulip-systems/drive-local": "0.15.0",
62
+ "@tulip-systems/editor": "0.15.0",
63
+ "@tulip-systems/editor-storage": "0.15.0",
64
+ "@tulip-systems/inline": "0.15.0",
65
+ "@tulip-systems/query": "0.15.0",
66
+ "@tulip-systems/storage": "0.15.0",
67
+ "@tulip-systems/ui": "0.15.0",
68
68
  "@vercel/functions": "^3.9.0",
69
69
  "@zip.js/zip.js": "^2.8.34",
70
70
  "better-auth": "catalog:",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tulip-systems/create-app",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "AGPL-3.0",
@@ -1,115 +0,0 @@
1
- "use client";
2
-
3
- import { useSuspenseQuery } from "@tanstack/react-query";
4
- import { CommandClick, CommandLabel } from "@tulip-systems/commands/client";
5
- import { toast } from "@tulip-systems/ui/sonner";
6
- import { BellIcon, BellOffIcon } from "lucide-react";
7
- import { orpc } from "@/server/router/client";
8
-
9
- const notificationPushSubscriptionQueryKey = ["notifications", "push-subscription"] as const;
10
-
11
- export function CommandTogglePush() {
12
- const { data: config } = useSuspenseQuery(orpc.notifications.getPushConfig.queryOptions());
13
- const { data } = useSuspenseQuery({
14
- queryKey: notificationPushSubscriptionQueryKey,
15
- queryFn: async () => {
16
- const supported =
17
- typeof window !== "undefined" &&
18
- "serviceWorker" in navigator &&
19
- "PushManager" in window &&
20
- "Notification" in window;
21
-
22
- if (!supported) return { supported, enabled: false };
23
-
24
- const registration = await navigator.serviceWorker.getRegistration("/sw.js");
25
- const subscription = await registration?.pushManager.getSubscription();
26
-
27
- return { supported, enabled: Boolean(subscription) };
28
- },
29
- refetchOnMount: "always",
30
- staleTime: 0,
31
- });
32
-
33
- return (
34
- <CommandClick
35
- label={data.enabled ? "Disable push" : "Enable push"}
36
- disabled={!data.supported}
37
- variables={{ ...data, config }}
38
- mutation={{
39
- mutationFn: async (variables) => {
40
- if (variables.enabled) {
41
- const registration = await navigator.serviceWorker.getRegistration("/sw.js");
42
- const subscription = await registration?.pushManager.getSubscription();
43
-
44
- if (subscription) {
45
- await orpc.notifications.unsubscribePush.call({ endpoint: subscription.endpoint });
46
- await subscription.unsubscribe();
47
- }
48
-
49
- return false;
50
- }
51
-
52
- if (!variables.supported) throw new Error("This browser does not support Web Push");
53
- if (!variables.config.configured || !variables.config.publicKey) {
54
- throw new Error("Web Push is not configured for this environment");
55
- }
56
-
57
- if (Notification.permission === "denied") {
58
- throw new Error("Notifications are blocked for this site");
59
- }
60
-
61
- const [registration, permission] = await Promise.all([
62
- navigator.serviceWorker.register("/sw.js"),
63
- Notification.requestPermission(),
64
- ]);
65
-
66
- if (permission !== "granted") {
67
- throw new Error("Notification permission was not granted");
68
- }
69
-
70
- const publicKey = variables.config.publicKey;
71
- const padding = "=".repeat((4 - (publicKey.length % 4)) % 4);
72
- const base64 = `${publicKey}${padding}`.replace(/-/g, "+").replace(/_/g, "/");
73
- const applicationServerKey = Uint8Array.from(window.atob(base64), (char) =>
74
- char.charCodeAt(0),
75
- );
76
- const subscription = await registration.pushManager.subscribe({
77
- userVisibleOnly: true,
78
- applicationServerKey,
79
- });
80
- const subscriptionJson = subscription.toJSON();
81
-
82
- if (!subscriptionJson.keys?.p256dh || !subscriptionJson.keys.auth) {
83
- await subscription.unsubscribe();
84
- throw new Error("Browser returned an invalid push subscription");
85
- }
86
-
87
- await orpc.notifications.subscribePush.call({
88
- endpoint: subscription.endpoint,
89
- expirationTime: subscription.expirationTime,
90
- keys: {
91
- p256dh: subscriptionJson.keys.p256dh,
92
- auth: subscriptionJson.keys.auth,
93
- },
94
- });
95
-
96
- return true;
97
- },
98
- meta: { invalidates: [notificationPushSubscriptionQueryKey] },
99
- onSuccess: (enabled) => {
100
- toast.success(
101
- enabled ? "Browser notifications enabled" : "Browser notifications disabled",
102
- );
103
- },
104
- onError: (error) => {
105
- toast.error(
106
- error instanceof Error ? error.message : "Could not update push notifications",
107
- );
108
- },
109
- }}
110
- >
111
- {data.enabled ? <BellOffIcon className="size-4" /> : <BellIcon className="size-4" />}
112
- <CommandLabel />
113
- </CommandClick>
114
- );
115
- }
@@ -1,80 +0,0 @@
1
- import "server-cli-only";
2
-
3
- import { eq } from "drizzle-orm";
4
- import webpush from "web-push";
5
- import { env } from "@/env";
6
- import { db } from "@/server/db/init";
7
- import { notificationPushSubscriptions } from "./schema";
8
- import type { Notification } from "./validators";
9
-
10
- const publicKey = env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY;
11
- const privateKey = env.WEB_PUSH_PRIVATE_KEY;
12
- const subject = env.WEB_PUSH_SUBJECT;
13
-
14
- const isWebPushConfigured = Boolean(publicKey && privateKey && subject);
15
-
16
- if (publicKey && privateKey && subject) {
17
- webpush.setVapidDetails(subject, publicKey, privateKey);
18
- }
19
-
20
- /**
21
- * Returns the browser-facing Web Push configuration.
22
- *
23
- * The client uses this to know whether push is available in the current
24
- * environment and which public VAPID key to pass to the browser subscription.
25
- */
26
- export function getWebPushConfig() {
27
- return {
28
- publicKey: publicKey ?? null,
29
- configured: isWebPushConfigured,
30
- };
31
- }
32
-
33
- /**
34
- * Sends a notification row to every stored browser push subscription.
35
- *
36
- * This is a best-effort side effect: if VAPID keys are not configured, it no-ops
37
- * so database notifications keep working. Expired browser subscriptions are
38
- * deleted when the push service reports them as gone.
39
- */
40
- export async function sendPushNotification(notification: Notification) {
41
- if (!isWebPushConfigured) return;
42
-
43
- const subscriptions = await db.query.notificationPushSubscriptions.findMany();
44
- if (subscriptions.length === 0) return;
45
-
46
- const payload = JSON.stringify({
47
- title: notification.title,
48
- body: notification.body,
49
- url: notification.sourceHref ?? "/admin/notifications",
50
- notificationId: notification.id,
51
- });
52
-
53
- await Promise.all(
54
- subscriptions.map(async (subscription) => {
55
- try {
56
- await webpush.sendNotification(
57
- {
58
- endpoint: subscription.endpoint,
59
- keys: {
60
- p256dh: subscription.p256dh,
61
- auth: subscription.auth,
62
- },
63
- },
64
- payload,
65
- );
66
- } catch (error) {
67
- const statusCode = (error as { statusCode?: number }).statusCode;
68
-
69
- if (statusCode === 404 || statusCode === 410) {
70
- await db
71
- .delete(notificationPushSubscriptions)
72
- .where(eq(notificationPushSubscriptions.endpoint, subscription.endpoint));
73
- return;
74
- }
75
-
76
- console.error("[notifications] Failed to send Web Push notification", error);
77
- }
78
- }),
79
- );
80
- }
@@ -1,19 +0,0 @@
1
- import { z } from "zod";
2
- import type { notifications } from "./schema";
3
-
4
- /**
5
- * Notification
6
- */
7
- export type Notification = typeof notifications.$inferSelect;
8
-
9
- /**
10
- * Push Subscription Schema
11
- */
12
- export const pushSubscriptionSchema = z.object({
13
- endpoint: z.url(),
14
- expirationTime: z.number().nullable().optional(),
15
- keys: z.object({
16
- p256dh: z.string().min(1),
17
- auth: z.string().min(1),
18
- }),
19
- });