@tulip-systems/create-app 0.12.3 → 0.13.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 (190) hide show
  1. package/dist/index.mjs +2866 -2949
  2. package/dist/index.mjs.map +1 -1
  3. package/dist/templates/basic/package.json +37 -38
  4. package/dist/templates/basic/src/app/layout.tsx +20 -10
  5. package/dist/templates/code/package.json +81 -71
  6. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black-italic.otf +0 -0
  7. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black.otf +0 -0
  8. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.otf +0 -0
  9. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.otf +0 -0
  10. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light-italic.otf +0 -0
  11. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light.otf +0 -0
  12. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light-italic.otf +0 -0
  13. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light.otf +0 -0
  14. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.otf +0 -0
  15. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.otf +0 -0
  16. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular-italic.otf +0 -0
  17. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular.otf +0 -0
  18. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin-italic.otf +0 -0
  19. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin.otf +0 -0
  20. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin-italic.otf +0 -0
  21. package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin.otf +0 -0
  22. package/dist/templates/code/public/images/preview/hero-home.webp +0 -0
  23. package/dist/templates/code/public/sw.js +35 -0
  24. package/dist/templates/code/src/app/admin/contacts/_components/grid.tsx +1 -1
  25. package/dist/templates/code/src/app/admin/customers/_config/columns-data.tsx +1 -1
  26. package/dist/templates/code/src/app/admin/employees/_config/columns-data.tsx +1 -1
  27. package/dist/templates/code/src/app/admin/history/[id]/layout.tsx +33 -0
  28. package/dist/templates/code/src/app/admin/history/[id]/loading.tsx +11 -0
  29. package/dist/templates/code/src/app/admin/history/[id]/page.tsx +30 -0
  30. package/dist/templates/code/src/app/admin/history/_components/diff.client.tsx +377 -0
  31. package/dist/templates/code/src/app/admin/history/_components/field-comparison.tsx +153 -0
  32. package/dist/templates/code/src/app/admin/history/_components/tables.tsx +47 -0
  33. package/dist/templates/code/src/app/admin/history/_components/toolbars.tsx +54 -0
  34. package/dist/templates/code/src/app/admin/history/_config/columns-data.tsx +61 -0
  35. package/dist/templates/code/src/app/admin/history/_config/filters.ts +46 -0
  36. package/dist/templates/code/src/app/admin/history/_config/sorting.ts +30 -0
  37. package/dist/templates/code/src/app/admin/history/_lib/helpers.ts +98 -0
  38. package/dist/templates/code/src/app/admin/history/_lib/router.ts +100 -0
  39. package/dist/templates/code/src/app/admin/history/_lib/schema.ts +45 -0
  40. package/dist/templates/code/src/app/admin/history/_lib/service.server.ts +30 -0
  41. package/dist/templates/code/src/app/admin/history/_lib/validators.ts +118 -0
  42. package/dist/templates/code/src/app/admin/initiatives/_config/columns-data.tsx +1 -1
  43. package/dist/templates/code/src/app/admin/notifications/_components/command-toggle-push.tsx +115 -0
  44. package/dist/templates/code/src/app/admin/notifications/_components/tables.tsx +37 -0
  45. package/dist/templates/code/src/app/admin/notifications/_components/toolbars.tsx +21 -0
  46. package/dist/templates/code/src/app/admin/notifications/_config/columns-data.tsx +64 -0
  47. package/dist/templates/code/src/app/admin/notifications/_config/commands.tsx +40 -0
  48. package/dist/templates/code/src/app/admin/notifications/_lib/listeners.server.ts +69 -0
  49. package/dist/templates/code/src/app/admin/notifications/_lib/push.server.ts +80 -0
  50. package/dist/templates/code/src/app/admin/notifications/_lib/router.ts +82 -0
  51. package/dist/templates/code/src/app/admin/notifications/_lib/schema.ts +49 -0
  52. package/dist/templates/code/src/app/admin/notifications/_lib/validators.ts +19 -0
  53. package/dist/templates/code/src/app/admin/notifications/layout.tsx +17 -0
  54. package/dist/templates/code/src/app/admin/notifications/loading.tsx +9 -0
  55. package/dist/templates/code/src/app/admin/notifications/page.tsx +28 -0
  56. package/dist/templates/code/src/app/admin/projects/[id]/(default)/page.tsx +1 -1
  57. package/dist/templates/code/src/app/admin/projects/[id]/timeline/page.tsx +1 -1
  58. package/dist/templates/code/src/app/admin/projects/_config/columns-data.tsx +1 -1
  59. package/dist/templates/code/src/app/admin/projects/_lib/events.ts +64 -0
  60. package/dist/templates/code/src/app/admin/projects/_lib/listeners.server.ts +82 -0
  61. package/dist/templates/code/src/app/admin/projects/_lib/router.ts +145 -20
  62. package/dist/templates/code/src/app/admin/projects/timeline/page.tsx +1 -1
  63. package/dist/templates/code/src/app/admin/prospects/_config/columns-data.tsx +1 -1
  64. package/dist/templates/code/src/app/admin/queue/_components/create-command.tsx +97 -0
  65. package/dist/templates/code/src/app/admin/queue/_components/tables.tsx +37 -0
  66. package/dist/templates/code/src/app/admin/queue/_components/toolbars.tsx +16 -0
  67. package/dist/templates/code/src/app/admin/queue/_config/columns-data.tsx +44 -0
  68. package/dist/templates/code/src/app/admin/queue/_config/commands.tsx +39 -0
  69. package/dist/templates/code/src/app/admin/queue/_lib/queue.ts +27 -0
  70. package/dist/templates/code/src/app/admin/queue/_lib/router.ts +33 -0
  71. package/dist/templates/code/src/app/admin/queue/_lib/schema.ts +12 -0
  72. package/dist/templates/code/src/app/admin/queue/_lib/validators.ts +15 -0
  73. package/dist/templates/code/src/app/admin/quotations/(list)/layout.tsx +24 -0
  74. package/dist/templates/code/src/app/admin/quotations/(list)/page.tsx +23 -0
  75. package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/layout.tsx +40 -0
  76. package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/page.tsx +56 -0
  77. package/dist/templates/code/src/app/admin/quotations/[id]/preview/page.tsx +17 -0
  78. package/dist/templates/code/src/app/admin/quotations/_blocks/commands.tsx +24 -0
  79. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/config.ts +14 -0
  80. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/create.tsx +152 -0
  81. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/inline.tsx +90 -0
  82. package/dist/templates/code/src/app/admin/quotations/_blocks/hero/preview.tsx +90 -0
  83. package/dist/templates/code/src/app/admin/quotations/_blocks/registry.ts +13 -0
  84. package/dist/templates/code/src/app/admin/quotations/_blocks/render.tsx +190 -0
  85. package/dist/templates/code/src/app/admin/quotations/_blocks/styles.ts +24 -0
  86. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/config.ts +13 -0
  87. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/create.tsx +175 -0
  88. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/inline.tsx +93 -0
  89. package/dist/templates/code/src/app/admin/quotations/_blocks/summary/preview.tsx +25 -0
  90. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/config.ts +18 -0
  91. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/create.tsx +177 -0
  92. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/inline.tsx +96 -0
  93. package/dist/templates/code/src/app/admin/quotations/_blocks/totals/preview.tsx +32 -0
  94. package/dist/templates/code/src/app/admin/quotations/_components/create-command.tsx +97 -0
  95. package/dist/templates/code/src/app/admin/quotations/_components/tables.tsx +68 -0
  96. package/dist/templates/code/src/app/admin/quotations/_components/toolbars.tsx +18 -0
  97. package/dist/templates/code/src/app/admin/quotations/_config/columns-data.tsx +35 -0
  98. package/dist/templates/code/src/app/admin/quotations/_config/commands.tsx +71 -0
  99. package/dist/templates/code/src/app/admin/quotations/_config/filters.ts +22 -0
  100. package/dist/templates/code/src/app/admin/quotations/_config/sorting.ts +23 -0
  101. package/dist/templates/code/src/app/admin/quotations/_lib/router.ts +260 -0
  102. package/dist/templates/code/src/app/admin/quotations/_lib/schema.ts +42 -0
  103. package/dist/templates/code/src/app/admin/quotations/_lib/validators.ts +20 -0
  104. package/dist/templates/code/src/app/admin/settings/history/layout.tsx +6 -0
  105. package/dist/templates/code/src/app/admin/settings/history/loading.tsx +5 -0
  106. package/dist/templates/code/src/app/admin/settings/history/page.tsx +13 -0
  107. package/dist/templates/code/src/app/admin/settings/layout.tsx +11 -0
  108. package/dist/templates/code/src/app/admin/settings/queue/loading.tsx +9 -0
  109. package/dist/templates/code/src/app/admin/settings/queue/page.tsx +21 -0
  110. package/dist/templates/code/src/app/admin/tasks/_components/domains-tabs-grid.tsx +1 -1
  111. package/dist/templates/code/src/app/admin/tasks/_config/columns-data.tsx +1 -1
  112. package/dist/templates/code/src/app/admin/tasks/_config/columns-inline.tsx +1 -1
  113. package/dist/templates/code/src/app/admin/tasks/_lib/events.ts +40 -0
  114. package/dist/templates/code/src/app/admin/tasks/_lib/listeners.server.ts +46 -0
  115. package/dist/templates/code/src/app/admin/tasks/_lib/router.ts +222 -90
  116. package/dist/templates/code/src/app/admin/time/_config/columns-data.tsx +1 -1
  117. package/dist/templates/code/src/app/admin/time/_config/columns-inline.tsx +1 -1
  118. package/dist/templates/code/src/app/api/[transport]/route.ts +31 -21
  119. package/dist/templates/code/src/app/api/queue/dispatch/route.ts +4 -0
  120. package/dist/templates/code/src/app/auth/forget-password/page.tsx +1 -1
  121. package/dist/templates/code/src/app/auth/layout.tsx +1 -1
  122. package/dist/templates/code/src/app/auth/reset-password/page.tsx +1 -1
  123. package/dist/templates/code/src/app/layout.tsx +25 -13
  124. package/dist/templates/code/src/env.ts +12 -0
  125. package/dist/templates/code/src/lib/config/paths.tsx +16 -0
  126. package/dist/templates/code/src/lib/utils/fonts.ts +102 -0
  127. package/dist/templates/code/src/server/auth/constants.ts +1 -1
  128. package/dist/templates/code/src/server/auth/init.ts +12 -5
  129. package/dist/templates/code/src/server/auth/permissions.ts +7 -0
  130. package/dist/templates/code/src/server/auth/schema.ts +100 -1
  131. package/dist/templates/code/src/server/db/schema.ts +4 -0
  132. package/dist/templates/code/src/server/events/handlers.ts +17 -0
  133. package/dist/templates/code/src/server/events/init.ts +18 -0
  134. package/dist/templates/code/src/server/events/listeners.ts +17 -0
  135. package/dist/templates/code/src/server/queue/handlers.ts +5 -0
  136. package/dist/templates/code/src/server/queue/init.ts +16 -0
  137. package/dist/templates/code/src/server/router/router.ts +23 -0
  138. package/dist/templates/spark/package.json +64 -65
  139. package/dist/templates/spark/src/app/admin/assignments/[id]/(default)/@dashboard/page.tsx +1 -1
  140. package/dist/templates/spark/src/app/admin/assignments/_config/columns-data.tsx +1 -1
  141. package/dist/templates/spark/src/app/admin/companies/_config/columns-data.tsx +1 -1
  142. package/dist/templates/spark/src/app/admin/contacts/_components/grid.tsx +1 -1
  143. package/dist/templates/spark/src/app/admin/customers/_config/columns-data.tsx +1 -1
  144. package/dist/templates/spark/src/app/admin/drive/_config/columns-data.tsx +1 -1
  145. package/dist/templates/spark/src/app/admin/emails/_components/send-email.tsx +1 -1
  146. package/dist/templates/spark/src/app/admin/emails/_config/columns-data.tsx +1 -1
  147. package/dist/templates/spark/src/app/admin/employees/_config/columns-data.tsx +1 -1
  148. package/dist/templates/spark/src/app/admin/executions/[id]/@dashboard/page.tsx +1 -1
  149. package/dist/templates/spark/src/app/admin/executions/_config/columns-data.tsx +1 -1
  150. package/dist/templates/spark/src/app/admin/products/[id]/(preview)/page.tsx +1 -1
  151. package/dist/templates/spark/src/app/admin/products/_config/columns-data.tsx +1 -1
  152. package/dist/templates/spark/src/app/admin/projects/[id]/(dashboard)/page.tsx +1 -1
  153. package/dist/templates/spark/src/app/admin/projects/[id]/messages/page.tsx +1 -1
  154. package/dist/templates/spark/src/app/admin/projects/[id]/statistics/page.tsx +1 -1
  155. package/dist/templates/spark/src/app/admin/projects/_config/columns-data.tsx +1 -1
  156. package/dist/templates/spark/src/app/admin/purchase-order-items/_config/columns-data.tsx +1 -1
  157. package/dist/templates/spark/src/app/admin/purchase-order-items/_config/columns-inline.tsx +1 -1
  158. package/dist/templates/spark/src/app/admin/purchase-order-items/_config/commands.tsx +1 -1
  159. package/dist/templates/spark/src/app/admin/purchase-orders/[id]/(default)/@dashboard/page.tsx +1 -1
  160. package/dist/templates/spark/src/app/admin/purchase-orders/_config/columns-data.tsx +1 -1
  161. package/dist/templates/spark/src/app/admin/quotation-items/_config/commands.tsx +1 -1
  162. package/dist/templates/spark/src/app/admin/quotations/[id]/(default)/@dashboard/page.tsx +1 -1
  163. package/dist/templates/spark/src/app/admin/quotations/_config/columns-data.tsx +1 -1
  164. package/dist/templates/spark/src/app/admin/run-tasks/_config/columns-data.tsx +1 -1
  165. package/dist/templates/spark/src/app/admin/runs/[id]/@dashboard/page.tsx +1 -1
  166. package/dist/templates/spark/src/app/admin/runs/_config/columns-data.tsx +1 -1
  167. package/dist/templates/spark/src/app/admin/sales-invoices/[id]/@dashboard/page.tsx +1 -1
  168. package/dist/templates/spark/src/app/admin/sales-invoices/_config/columns-data.tsx +1 -1
  169. package/dist/templates/spark/src/app/admin/sales-order-executions/_config/columns-data.tsx +1 -1
  170. package/dist/templates/spark/src/app/admin/sales-order-executions/_config/columns-inline.tsx +1 -1
  171. package/dist/templates/spark/src/app/admin/sales-order-executions/_config/commands.tsx +1 -1
  172. package/dist/templates/spark/src/app/admin/sales-order-items/_config/commands.tsx +1 -1
  173. package/dist/templates/spark/src/app/admin/sales-orders/[id]/(default)/@dashboard/page.tsx +1 -1
  174. package/dist/templates/spark/src/app/admin/sales-orders/[id]/(default)/layout.tsx +1 -1
  175. package/dist/templates/spark/src/app/admin/sales-orders/_config/columns-data.tsx +1 -1
  176. package/dist/templates/spark/src/app/admin/schedule/_components/schedule-card.tsx +1 -1
  177. package/dist/templates/spark/src/app/admin/schedule/_components/schedule-month-view.tsx +1 -1
  178. package/dist/templates/spark/src/app/admin/suppliers/_config/columns-data.tsx +1 -1
  179. package/dist/templates/spark/src/app/admin/tasks/_components/subproject-selector.tsx +1 -1
  180. package/dist/templates/spark/src/app/admin/tasks/_config/columns-data.tsx +1 -1
  181. package/dist/templates/spark/src/app/admin/tasks/_config/columns-inline.tsx +1 -1
  182. package/dist/templates/spark/src/app/admin/tasks/_config/commands.tsx +1 -1
  183. package/dist/templates/spark/src/app/admin/technical-sheets/[id]/page.tsx +1 -1
  184. package/dist/templates/spark/src/app/admin/technical-sheets/_config/columns-data.tsx +1 -1
  185. package/dist/templates/spark/src/app/admin/technical-sheets/_config/commands.tsx +1 -1
  186. package/dist/templates/spark/src/app/admin/templates/_config/columns-data.tsx +1 -1
  187. package/dist/templates/spark/src/app/auth/layout.tsx +1 -1
  188. package/dist/templates/spark/src/app/layout.tsx +25 -10
  189. package/dist/templates/spark/src/server/auth/init.ts +1 -0
  190. package/package.json +6 -6
@@ -1,18 +1,18 @@
1
- import { mcpHandler } from "@better-auth/oauth-provider";
1
+ import { requireMcpAuth } from "@better-auth/mcp";
2
+ import { createMcpHandler, McpServer } from "@modelcontextprotocol/server";
2
3
  import { type AnyProcedure, call } from "@orpc/server";
3
- import { createMcpHandler } from "mcp-handler";
4
- import { OAUTH_AUDIENCES, OAUTH_ISSUER, OAUTH_JWKS_URI } from "@/server/auth/constants";
4
+ import {
5
+ OAUTH_AUDIENCE,
6
+ OAUTH_ISSUER,
7
+ OAUTH_JWKS_URI,
8
+ OAUTH_SCOPES,
9
+ } from "@/server/auth/constants";
10
+ import { auth } from "@/server/auth/init";
5
11
  import { type McpProcedure, mcpRouter } from "@/server/mcp/router";
6
12
  import { createContext } from "@/server/router/init";
7
13
 
8
- const handler = mcpHandler(
9
- {
10
- jwksUrl: OAUTH_JWKS_URI,
11
- verifyOptions: {
12
- issuer: OAUTH_ISSUER,
13
- audience: OAUTH_AUDIENCES,
14
- },
15
- },
14
+ const POST = requireMcpAuth(
15
+ auth,
16
16
  (req, jwt) => {
17
17
  const transport = new URL(req.url).pathname.split("/").filter(Boolean).pop();
18
18
 
@@ -22,8 +22,12 @@ const handler = mcpHandler(
22
22
  });
23
23
  }
24
24
 
25
- return createMcpHandler(
26
- (server) => {
25
+ const mcpServerHandler = createMcpHandler(
26
+ () => {
27
+ const server = new McpServer({
28
+ name: "tulip-demo-code",
29
+ version: "1.0.0",
30
+ });
27
31
  const context = createContext({ headers: req.headers, jwt });
28
32
 
29
33
  const entries = Object.entries(mcpRouter) as Array<[string, McpProcedure]>;
@@ -33,7 +37,7 @@ const handler = mcpHandler(
33
37
 
34
38
  const definition = procedure["~orpc"];
35
39
 
36
- server.registerTool<never, Procedure["~orpc"]["inputSchema"]>(
40
+ server.registerTool(
37
41
  procedureName,
38
42
  {
39
43
  title: definition.meta.title,
@@ -44,18 +48,24 @@ const handler = mcpHandler(
44
48
  call(procedure as AnyProcedure, input, { context })) as never,
45
49
  );
46
50
  });
51
+
52
+ return server;
47
53
  },
48
54
  {
49
- serverInfo: {
50
- name: "tulip-demo-code",
51
- version: "1.0.0",
52
- },
53
- verboseLogs: true,
55
+ legacy: "reject",
54
56
  },
55
- )(req);
57
+ );
58
+
59
+ return mcpServerHandler.fetch(req);
60
+ },
61
+ {
62
+ resource: OAUTH_AUDIENCE,
63
+ issuer: OAUTH_ISSUER,
64
+ jwksUrl: OAUTH_JWKS_URI,
65
+ challengeScopes: [...OAUTH_SCOPES],
56
66
  },
57
67
  );
58
68
 
59
69
  export const maxDuration = 60;
60
70
 
61
- export { handler as DELETE, handler as GET, handler as POST };
71
+ export { POST };
@@ -0,0 +1,4 @@
1
+ import { queueHandlers } from "@/server/queue/handlers";
2
+ import { queue } from "@/server/queue/init";
3
+
4
+ export const POST = queue.handleCallback({ handlers: queueHandlers });
@@ -8,8 +8,8 @@ import {
8
8
  ForgetPasswordTitle,
9
9
  ForgetPasswordView,
10
10
  } from "@tulip-systems/auth/components/client";
11
+ import { Link } from "@tulip-systems/core/navigation";
11
12
  import type { Metadata } from "next";
12
- import Link from "next/link";
13
13
 
14
14
  export const metadata: Metadata = {
15
15
  title: "Wachtwoord vergeten",
@@ -6,7 +6,7 @@ import {
6
6
  AuthLayoutImageFooter,
7
7
  AuthLayoutImageHeader,
8
8
  } from "@tulip-systems/auth/components";
9
- import Link from "next/link";
9
+ import { Link } from "@tulip-systems/core/navigation";
10
10
  import type { PropsWithChildren } from "react";
11
11
  import { config } from "@/lib/config/base";
12
12
  import { authClient } from "@/server/auth/client";
@@ -10,8 +10,8 @@ import {
10
10
  ResetPasswordTitle,
11
11
  ResetPasswordView,
12
12
  } from "@tulip-systems/auth/components/client";
13
+ import { Link } from "@tulip-systems/core/navigation";
13
14
  import type { Metadata } from "next";
14
- import Link from "next/link";
15
15
  import { Suspense } from "react";
16
16
 
17
17
  export const metadata: Metadata = {
@@ -2,26 +2,38 @@ import "./globals.css";
2
2
  import "../server/router/register";
3
3
 
4
4
  import { ColorThemeProvider, Providers } from "@tulip-systems/app/components/client";
5
- import {
6
- ColorThemeScript,
7
- generateRootLayoutMetadata,
8
- RootLayout,
9
- } from "@tulip-systems/app/components/server";
5
+ import { registerFonts, rootLayoutViewport } from "@tulip-systems/app/components/server";
10
6
  import { DriveViewScript } from "@tulip-systems/drive/server";
7
+ import { cn } from "@tulip-systems/ui/cn";
8
+ import type { Metadata } from "next";
11
9
  import type { PropsWithChildren } from "react";
12
10
  import { config } from "@/lib/config/base";
13
11
 
14
- export const { metadata, viewport } = generateRootLayoutMetadata(config);
12
+ export const viewport = rootLayoutViewport;
13
+
14
+ export const metadata: Metadata = {
15
+ description: "Developed by NewCode",
16
+ appleWebApp: {
17
+ capable: true,
18
+ statusBarStyle: "black-translucent",
19
+ title: config.general.name,
20
+ },
21
+ };
22
+
23
+ const colorTheme = "sage";
15
24
 
16
25
  export default function Layout({ children }: PropsWithChildren) {
17
26
  return (
18
- <RootLayout>
19
- <ColorThemeScript />
20
- <DriveViewScript />
27
+ <html lang="nl" suppressHydrationWarning data-theme={colorTheme}>
28
+ <title>{config.general.name}</title>
29
+
30
+ <body className={cn("bg-background font-sans antialiased", registerFonts())}>
31
+ <DriveViewScript />
21
32
 
22
- <ColorThemeProvider defaultTheme="tulip">
23
- <Providers>{children}</Providers>
24
- </ColorThemeProvider>
25
- </RootLayout>
33
+ <ColorThemeProvider theme={colorTheme}>
34
+ <Providers>{children}</Providers>
35
+ </ColorThemeProvider>
36
+ </body>
37
+ </html>
26
38
  );
27
39
  }
@@ -11,6 +11,16 @@ export const env = createEnv({
11
11
  S3_ENDPOINT: z.string().min(1),
12
12
  S3_ACCESS_KEY_ID: z.string().min(1),
13
13
  S3_SECRET_ACCESS_KEY: z.string().min(1),
14
+ QSTASH_URL: z.string().min(1),
15
+ QSTASH_TOKEN: z.string().min(1),
16
+ QSTASH_CURRENT_SIGNING_KEY: z.string().min(1),
17
+ QSTASH_NEXT_SIGNING_KEY: z.string().min(1),
18
+ QSTASH_DEV: z
19
+ .enum(["true", "false"])
20
+ .transform((value) => value === "true")
21
+ .optional(),
22
+ WEB_PUSH_PRIVATE_KEY: z.string().min(1).optional(),
23
+ WEB_PUSH_SUBJECT: z.string().min(1).optional(),
14
24
  GOOGLE_SERVICE_ACCOUNT_EMAIL: z.string().min(1),
15
25
  GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY: z
16
26
  .string()
@@ -19,8 +29,10 @@ export const env = createEnv({
19
29
  },
20
30
  client: {
21
31
  NEXT_PUBLIC_HOST: z.string().min(1),
32
+ NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY: z.string().min(1).optional(),
22
33
  },
23
34
  experimental__runtimeEnv: {
24
35
  NEXT_PUBLIC_HOST: process.env.NEXT_PUBLIC_HOST ?? process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL,
36
+ NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY: process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY,
25
37
  },
26
38
  });
@@ -11,10 +11,12 @@ import {
11
11
  } from "@tulip-systems/app/components/client";
12
12
  import { Sidebar, SidebarContent, SidebarHeader, SidebarRail } from "@tulip-systems/ui/sidebar";
13
13
  import {
14
+ BellIcon,
14
15
  BotIcon,
15
16
  FoldersIcon,
16
17
  HomeIcon,
17
18
  ListTodoIcon,
19
+ ReceiptTextIcon,
18
20
  Table2Icon,
19
21
  TimerIcon,
20
22
  UserSearchIcon,
@@ -47,6 +49,13 @@ export function AdminSidebar() {
47
49
  </PathLink>
48
50
  </Path>
49
51
 
52
+ <Path segment="notifications">
53
+ <PathLink href="/admin/notifications">
54
+ <BellIcon />
55
+ Notifications
56
+ </PathLink>
57
+ </Path>
58
+
50
59
  <Path permission={{ drive: ["view"] }} segment="drive">
51
60
  <PathLink href="/admin/drive">
52
61
  <FoldersIcon />
@@ -140,6 +149,13 @@ export function AdminSidebar() {
140
149
  Prospects
141
150
  </PathLink>
142
151
  </Path>
152
+
153
+ <Path segment="quotations">
154
+ <PathLink href="/admin/quotations">
155
+ <ReceiptTextIcon />
156
+ Offertes
157
+ </PathLink>
158
+ </Path>
143
159
  </PathGroupItems>
144
160
  </PathGroup>
145
161
 
@@ -0,0 +1,102 @@
1
+ import localFont from "next/font/local";
2
+
3
+ /**
4
+ * Neue Haas Grotesk Display Pro — de sans-font van de app (`--font-sans`).
5
+ *
6
+ * Belangrijk: deze font gebruikt een niet-standaard weight-schaal. Elke `weight`
7
+ * hieronder is gelijkgesteld aan de échte `usWeightClass` van het bestand, zodat
8
+ * numerieke gewichten in de browser exact zo resolven als in Figma:
9
+ *
10
+ * 250 UltraThin · 300 Thin · 350 XLight · 400 Light · 450 Roman ·
11
+ * 500 Medium · 700 Bold · 900 Black
12
+ *
13
+ * De typography-schaal gebruikt 400 (→ Light) en 500 (→ Medium) — dit matcht
14
+ * `font-weight: 400/500` uit de Figma-specs (waar 400 óók de Light-cut is, niet
15
+ * de Roman). De Roman-cut (55) zit bewust op 450 en is dus niet `font-normal`.
16
+ */
17
+ export const fontNeueHaas = localFont({
18
+ variable: "--font-sans",
19
+ display: "swap",
20
+ src: [
21
+ {
22
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin.otf",
23
+ weight: "250",
24
+ style: "normal",
25
+ },
26
+ {
27
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin-italic.otf",
28
+ weight: "250",
29
+ style: "italic",
30
+ },
31
+ {
32
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin.otf",
33
+ weight: "300",
34
+ style: "normal",
35
+ },
36
+ {
37
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin-italic.otf",
38
+ weight: "300",
39
+ style: "italic",
40
+ },
41
+ {
42
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light.otf",
43
+ weight: "350",
44
+ style: "normal",
45
+ },
46
+ {
47
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light-italic.otf",
48
+ weight: "350",
49
+ style: "italic",
50
+ },
51
+ {
52
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-light.otf",
53
+ weight: "400",
54
+ style: "normal",
55
+ },
56
+ {
57
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-light-italic.otf",
58
+ weight: "400",
59
+ style: "italic",
60
+ },
61
+ {
62
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular.otf",
63
+ weight: "450",
64
+ style: "normal",
65
+ },
66
+ {
67
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular-italic.otf",
68
+ weight: "450",
69
+ style: "italic",
70
+ },
71
+ {
72
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.otf",
73
+ weight: "500",
74
+ style: "normal",
75
+ },
76
+ {
77
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.otf",
78
+ weight: "500",
79
+ style: "italic",
80
+ },
81
+ {
82
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.otf",
83
+ weight: "700",
84
+ style: "normal",
85
+ },
86
+ {
87
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.otf",
88
+ weight: "700",
89
+ style: "italic",
90
+ },
91
+ {
92
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-black.otf",
93
+ weight: "900",
94
+ style: "normal",
95
+ },
96
+ {
97
+ path: "../../../public/fonts/neue-haas-grotesk/neue-haas-grotesk-black-italic.otf",
98
+ weight: "900",
99
+ style: "italic",
100
+ },
101
+ ],
102
+ });
@@ -10,7 +10,7 @@ export const OAUTH_SCOPES = ["openid", "profile", "email", "offline_access", "mc
10
10
  /**
11
11
  * This constant defines the audience URLs for the OAuth protected resource.
12
12
  */
13
- export const OAUTH_AUDIENCES = [url("/api/mcp")];
13
+ export const OAUTH_AUDIENCE = url("/api/mcp");
14
14
  /**
15
15
  * This constant defines the issuer URLs for the OAuth authorization server.
16
16
  */
@@ -1,4 +1,6 @@
1
- import { oauthProvider } from "@better-auth/oauth-provider";
1
+ import { cimd } from "@better-auth/cimd";
2
+ import { fetchClientMetadataResource } from "@better-auth/cimd/node";
3
+ import { mcp } from "@better-auth/mcp";
2
4
  import { passkey } from "@better-auth/passkey";
3
5
  import { authAdditionalFields, authCookiePrefix } from "@tulip-systems/auth";
4
6
  import { ForgetPasswordEmail } from "@tulip-systems/auth/react-email";
@@ -9,7 +11,7 @@ import { admin, emailOTP, jwt } from "better-auth/plugins";
9
11
  import { config } from "@/lib/config/base";
10
12
  import { db } from "../db/init";
11
13
  import { email } from "../emails/init";
12
- import { OAUTH_AUDIENCES, OAUTH_SCOPES } from "./constants";
14
+ import { OAUTH_AUDIENCE, OAUTH_SCOPES } from "./constants";
13
15
  import { ac, roles } from "./permissions";
14
16
 
15
17
  export const auth = betterAuth({
@@ -23,7 +25,8 @@ export const auth = betterAuth({
23
25
  "*.tulip.systems",
24
26
  "*.vercel.app",
25
27
  "*.localhost:*",
26
- "localhost:7459",
28
+ "*.localhost",
29
+ "localhost:*",
27
30
  ],
28
31
  },
29
32
  database: drizzleAdapter(db, {
@@ -63,15 +66,19 @@ export const auth = betterAuth({
63
66
  },
64
67
  }),
65
68
  jwt(),
66
- oauthProvider({
69
+ mcp({
67
70
  loginPage: "/auth/login",
68
71
  consentPage: "/auth/consent",
72
+ resource: OAUTH_AUDIENCE,
69
73
  scopes: [...OAUTH_SCOPES],
70
- validAudiences: [...OAUTH_AUDIENCES],
71
74
  allowDynamicClientRegistration: true,
72
75
  allowUnauthenticatedClientRegistration: true,
73
76
  clientRegistrationDefaultScopes: [...OAUTH_SCOPES],
74
77
  }),
78
+ cimd({
79
+ fetchClientMetadataResource,
80
+ metadataProfile: "mcp-2026-07-28",
81
+ }),
75
82
  ],
76
83
  });
77
84
 
@@ -4,6 +4,7 @@ import { adminAc, createAccessControl, createStatements } from "@tulip-systems/a
4
4
  * Statements
5
5
  */
6
6
  const statement = createStatements({
7
+ history: ["view"],
7
8
  developer: ["debug"],
8
9
  dashboard: ["view.budget"],
9
10
  drive: ["view", "create", "update", "archive", "delete"],
@@ -11,6 +12,7 @@ const statement = createStatements({
11
12
  customer: ["view", "create", "update", "archive", "delete"],
12
13
  task: ["view", "create", "update", "delete"],
13
14
  project: ["view", "view.budget", "view.drive", "create", "update", "archive", "delete"],
15
+ quotation: ["view", "create", "update", "delete"],
14
16
  initiative: ["view", "view.budget", "view.stats", "create", "update", "delete"],
15
17
  domain: ["view", "create", "update", "delete"],
16
18
  time: ["view", "create", "update", "delete"],
@@ -26,6 +28,7 @@ export const ac = createAccessControl(statement);
26
28
  * Admin role
27
29
  */
28
30
  const developer = ac.newRole({
31
+ history: ["view"],
29
32
  developer: ["debug"],
30
33
  dashboard: ["view.budget"],
31
34
  drive: ["view", "create", "update", "archive", "delete"],
@@ -33,6 +36,7 @@ const developer = ac.newRole({
33
36
  customer: ["view", "create", "update", "archive", "delete"],
34
37
  task: ["view", "create", "update", "delete"],
35
38
  project: ["view", "view.budget", "view.drive", "create", "update", "archive", "delete"],
39
+ quotation: ["view", "create", "update", "delete"],
36
40
  initiative: ["view", "view.budget", "view.stats", "create", "update", "delete"],
37
41
  domain: ["view", "create", "update", "delete"],
38
42
  time: ["view", "create", "update", "delete"],
@@ -44,12 +48,14 @@ const developer = ac.newRole({
44
48
  * Owner role
45
49
  */
46
50
  const admin = ac.newRole({
51
+ history: ["view"],
47
52
  dashboard: ["view.budget"],
48
53
  drive: ["view", "create", "update", "archive", "delete"],
49
54
  prospect: ["view", "create", "update", "archive", "delete"],
50
55
  customer: ["view", "create", "update", "archive", "delete"],
51
56
  task: ["view", "create", "update", "delete"],
52
57
  project: ["view", "view.budget", "view.drive", "create", "update", "archive", "delete"],
58
+ quotation: ["view", "create", "update", "delete"],
53
59
  initiative: ["view", "view.budget", "view.stats", "create", "update", "delete"],
54
60
  domain: ["view", "create", "update", "delete"],
55
61
  time: ["view", "create", "update", "delete"],
@@ -65,6 +71,7 @@ const worker = ac.newRole({
65
71
  customer: ["view"],
66
72
  task: ["view", "create", "update", "delete"],
67
73
  project: ["view", "view.drive", "create", "update"],
74
+ quotation: ["view", "create", "update"],
68
75
  initiative: ["view", "view.stats", "update"],
69
76
  domain: ["view", "create", "update", "delete"],
70
77
  time: ["view", "create", "update", "delete"],
@@ -1,5 +1,5 @@
1
1
  import { relations, type SQL, sql } from "drizzle-orm";
2
- import { index, pgTable } from "drizzle-orm/pg-core";
2
+ import { index, pgTable, uniqueIndex } from "drizzle-orm/pg-core";
3
3
  import { baseColumns } from "@/server/db/helpers";
4
4
 
5
5
  /**
@@ -130,6 +130,8 @@ const jwkss = pgTable("jwkss", (t) => ({
130
130
  publicKey: t.text().notNull(),
131
131
  privateKey: t.text().notNull(),
132
132
  expiresAt: t.timestamp(),
133
+ alg: t.text(),
134
+ crv: t.text(),
133
135
  }));
134
136
 
135
137
  /**
@@ -141,11 +143,13 @@ const oauthClients = pgTable(
141
143
  ...baseColumns,
142
144
  clientId: t.text().notNull().unique(),
143
145
  clientSecret: t.text(),
146
+ clientDiscoveryId: t.text(),
144
147
  disabled: t.boolean().default(false),
145
148
  skipConsent: t.boolean(),
146
149
  enableEndSession: t.boolean(),
147
150
  subjectType: t.text(),
148
151
  scopes: t.text().array(),
152
+ clientCredentialsScopes: t.text().array().default([]),
149
153
  userId: t.uuid().references(() => users.id, { onDelete: "cascade" }),
150
154
  createdAt: t.timestamp(),
151
155
  updatedAt: t.timestamp(),
@@ -160,12 +164,18 @@ const oauthClients = pgTable(
160
164
  softwareStatement: t.text(),
161
165
  redirectUris: t.text().array().notNull(),
162
166
  postLogoutRedirectUris: t.text().array(),
167
+ backchannelLogoutUri: t.text(),
168
+ backchannelLogoutSessionRequired: t.boolean(),
163
169
  tokenEndpointAuthMethod: t.text(),
170
+ applicationType: t.text(),
171
+ jwks: t.text(),
172
+ jwksUri: t.text(),
164
173
  grantTypes: t.text().array(),
165
174
  responseTypes: t.text().array(),
166
175
  public: t.boolean(),
167
176
  type: t.text(),
168
177
  requirePKCE: t.boolean(),
178
+ dpopBoundAccessTokens: t.boolean().default(false),
169
179
  referenceId: t.text(),
170
180
  metadata: t.jsonb(),
171
181
  }),
@@ -180,6 +190,68 @@ const oauthClientsRelations = relations(oauthClients, ({ one, many }) => ({
180
190
  oauthRefreshTokens: many(oauthRefreshTokens),
181
191
  oauthAccessTokens: many(oauthAccessTokens),
182
192
  oauthConsents: many(oauthConsents),
193
+ oauthClientResources: many(oauthClientResources),
194
+ }));
195
+
196
+ /**
197
+ * The oauthResources table stores protected resource identifiers and per-resource token policy.
198
+ */
199
+ const oauthResources = pgTable("oauth_resources", (t) => ({
200
+ ...baseColumns,
201
+ identifier: t.text().notNull().unique(),
202
+ name: t.text().notNull(),
203
+ accessTokenTtl: t.integer(),
204
+ refreshTokenTtl: t.integer(),
205
+ signingAlgorithm: t.text(),
206
+ signingKeyId: t.text(),
207
+ allowedScopes: t.text().array(),
208
+ customClaims: t.jsonb(),
209
+ dpopBoundAccessTokensRequired: t.boolean().default(false),
210
+ disabled: t.boolean().default(false),
211
+ policyVersion: t.integer().default(1),
212
+ metadata: t.jsonb(),
213
+ }));
214
+
215
+ const oauthResourcesRelations = relations(oauthResources, ({ many }) => ({
216
+ oauthClientResources: many(oauthClientResources),
217
+ }));
218
+
219
+ /**
220
+ * The oauthClientResources table links OAuth clients to protected resources.
221
+ */
222
+ const oauthClientResources = pgTable(
223
+ "oauth_client_resources",
224
+ (t) => ({
225
+ ...baseColumns,
226
+ clientId: t
227
+ .text()
228
+ .notNull()
229
+ .references(() => oauthClients.clientId, { onDelete: "cascade" }),
230
+ resourceId: t
231
+ .text()
232
+ .notNull()
233
+ .references(() => oauthResources.identifier, { onDelete: "cascade" }),
234
+ metadata: t.jsonb(),
235
+ }),
236
+ (table) => [
237
+ index("oauthClientResources_clientId_idx").on(table.clientId),
238
+ index("oauthClientResources_resourceId_idx").on(table.resourceId),
239
+ uniqueIndex("oauthClientResources_clientId_resourceId_idx").on(
240
+ table.clientId,
241
+ table.resourceId,
242
+ ),
243
+ ],
244
+ );
245
+
246
+ const oauthClientResourcesRelations = relations(oauthClientResources, ({ one }) => ({
247
+ oauthClients: one(oauthClients, {
248
+ fields: [oauthClientResources.clientId],
249
+ references: [oauthClients.clientId],
250
+ }),
251
+ oauthResources: one(oauthResources, {
252
+ fields: [oauthClientResources.resourceId],
253
+ references: [oauthResources.identifier],
254
+ }),
183
255
  }));
184
256
 
185
257
  /**
@@ -200,9 +272,16 @@ const oauthRefreshTokens = pgTable(
200
272
  .notNull()
201
273
  .references(() => users.id, { onDelete: "cascade" }),
202
274
  referenceId: t.text(),
275
+ authorizationCodeId: t.text(),
276
+ resources: t.text().array(),
277
+ requestedUserInfoClaims: t.text().array(),
203
278
  expiresAt: t.timestamp().notNull(),
204
279
  revoked: t.timestamp(),
280
+ rotatedAt: t.timestamp(),
281
+ rotationReplayResponse: t.text(),
282
+ rotationReplayExpiresAt: t.timestamp(),
205
283
  authTime: t.timestamp(),
284
+ confirmation: t.jsonb(),
206
285
  scopes: t.text().array().notNull(),
207
286
  }),
208
287
  (table) => [
@@ -243,8 +322,13 @@ const oauthAccessTokens = pgTable(
243
322
  sessionId: t.uuid().references(() => sessions.id, { onDelete: "set null" }),
244
323
  userId: t.uuid().references(() => users.id, { onDelete: "cascade" }),
245
324
  referenceId: t.text(),
325
+ authorizationCodeId: t.text(),
326
+ resources: t.text().array(),
327
+ requestedUserInfoClaims: t.text().array(),
246
328
  refreshId: t.uuid().references(() => oauthRefreshTokens.id, { onDelete: "cascade" }),
247
329
  expiresAt: t.timestamp().notNull(),
330
+ revoked: t.timestamp(),
331
+ confirmation: t.jsonb(),
248
332
  scopes: t.text().array().notNull(),
249
333
  }),
250
334
  (table) => [
@@ -286,6 +370,8 @@ const oauthConsents = pgTable(
286
370
  .references(() => oauthClients.clientId, { onDelete: "cascade" }),
287
371
  userId: t.uuid().references(() => users.id, { onDelete: "cascade" }),
288
372
  referenceId: t.text(),
373
+ resources: t.text().array(),
374
+ requestedUserInfoClaims: t.text().array(),
289
375
  scopes: t.text().array().notNull(),
290
376
  }),
291
377
  (table) => [
@@ -305,6 +391,14 @@ const oauthConsentsRelations = relations(oauthConsents, ({ one }) => ({
305
391
  }),
306
392
  }));
307
393
 
394
+ /**
395
+ * The oauthClientAssertions table stores used client assertion identifiers until they expire.
396
+ */
397
+ const oauthClientAssertions = pgTable("oauth_client_assertions", (t) => ({
398
+ ...baseColumns,
399
+ expiresAt: t.timestamp().notNull(),
400
+ }));
401
+
308
402
  /**
309
403
  * Exports
310
404
  */
@@ -312,10 +406,15 @@ export {
312
406
  jwkss,
313
407
  oauthAccessTokens,
314
408
  oauthAccessTokensRelations,
409
+ oauthClientAssertions,
410
+ oauthClientResources,
411
+ oauthClientResourcesRelations,
315
412
  oauthClients,
316
413
  oauthClientsRelations,
317
414
  oauthConsents,
318
415
  oauthConsentsRelations,
319
416
  oauthRefreshTokens,
320
417
  oauthRefreshTokensRelations,
418
+ oauthResources,
419
+ oauthResourcesRelations,
321
420
  };
@@ -12,7 +12,11 @@ export * from "@/app/admin/customers/_lib/schema";
12
12
  export * from "@/app/admin/domains/_lib/schema";
13
13
  export * from "@/app/admin/employees/_lib/schema";
14
14
  export * from "@/app/admin/initiatives/_lib/schema";
15
+ export * from "@/app/admin/notifications/_lib/schema";
15
16
  export * from "@/app/admin/projects/_lib/schema";
17
+ export * from "@/app/admin/queue/_lib/schema";
18
+ export * from "@/app/admin/quotations/_lib/schema";
16
19
  export * from "@/app/admin/tasks/_lib/schema";
17
20
  export * from "@/app/admin/time/_lib/schema";
18
21
  export * from "@/app/admin/prospects/_lib/schema";
22
+ export * from "@/app/admin/history/_lib/schema";
@@ -0,0 +1,17 @@
1
+ import "server-cli-only";
2
+
3
+ import { createEventDispatchHandler, defineEventDispatchTask } from "@tulip-systems/events/queue";
4
+ import { appEventListeners } from "./listeners";
5
+
6
+ /**
7
+ * Event dispatch task for the application.
8
+ */
9
+ export const eventDispatchTask = defineEventDispatchTask("events.dispatch");
10
+
11
+ /**
12
+ * Event dispatch task handler for the application.
13
+ */
14
+ export const eventDispatchTaskHandler = eventDispatchTask.handle(
15
+ "dispatch-event-listener",
16
+ createEventDispatchHandler({ listeners: appEventListeners }),
17
+ );