lytx 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +37 -0
- package/README.md +486 -0
- package/alchemy.run.ts +155 -0
- package/cli/bootstrap-admin.ts +284 -0
- package/cli/deploy-staging.ts +692 -0
- package/cli/import-events.ts +628 -0
- package/cli/import-sites.ts +518 -0
- package/cli/index.ts +609 -0
- package/cli/init-db.ts +269 -0
- package/cli/migrate-to-durable-objects.ts +564 -0
- package/cli/migration-worker.ts +300 -0
- package/cli/performance-test.ts +588 -0
- package/cli/pg/client.ts +4 -0
- package/cli/pg/new-site.ts +153 -0
- package/cli/rollback-durable-objects.ts +622 -0
- package/cli/seed-data.ts +459 -0
- package/cli/setup.js +18 -0
- package/cli/setup.ts +463 -0
- package/cli/validate-migration.ts +200 -0
- package/cli/wrangler-migration.jsonc +28 -0
- package/db/adapter.ts +166 -0
- package/db/analytics_engine/client.ts +0 -0
- package/db/analytics_engine/sites.ts +0 -0
- package/db/client.ts +16 -0
- package/db/d1/client.ts +8 -0
- package/db/d1/drizzle.config.ts +35 -0
- package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
- package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
- package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
- package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
- package/db/d1/migrations/0004_mute_stardust.sql +1 -0
- package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
- package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
- package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
- package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
- package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
- package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
- package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
- package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
- package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
- package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
- package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
- package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
- package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
- package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
- package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
- package/db/d1/migrations/meta/_journal.json +76 -0
- package/db/d1/schema.ts +407 -0
- package/db/d1/sites.ts +374 -0
- package/db/d1/teamAiUsage.ts +101 -0
- package/db/d1/teams.ts +127 -0
- package/db/durable/drizzle.config.ts +8 -0
- package/db/durable/durableObjectClient.ts +480 -0
- package/db/durable/events.ts +100 -0
- package/db/durable/migrations/0000_fair_bucky.sql +38 -0
- package/db/durable/migrations/meta/0000_snapshot.json +278 -0
- package/db/durable/migrations/meta/_journal.json +13 -0
- package/db/durable/migrations/migrations.js +10 -0
- package/db/durable/schema.ts +5 -0
- package/db/durable/siteDurableObject.ts +1352 -0
- package/db/durable/types.ts +53 -0
- package/db/postgres/client.ts +13 -0
- package/db/postgres/drizzle.config.ts +12 -0
- package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
- package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
- package/db/postgres/migrations/meta/_journal.json +13 -0
- package/db/postgres/schema.ts +145 -0
- package/db/postgres/sites.ts +118 -0
- package/db/tranformReports.ts +595 -0
- package/db/types.ts +55 -0
- package/endpoints/api_worker.tsx +1854 -0
- package/endpoints/site_do_worker.ts +11 -0
- package/index.d.ts +63 -0
- package/index.ts +83 -0
- package/lib/auth.ts +279 -0
- package/lib/geojson/world_countries.json +45307 -0
- package/lib/random_name.ts +41 -0
- package/lib/sendMail.ts +252 -0
- package/package.json +142 -0
- package/public/favicon.ico +0 -0
- package/public/images/android-chrome-192x192.png +0 -0
- package/public/images/android-chrome-512x512.png +0 -0
- package/public/images/apple-touch-icon.png +0 -0
- package/public/images/favicon-16x16.png +0 -0
- package/public/images/favicon-32x32.png +0 -0
- package/public/images/lytx_dark_dashboard.png +0 -0
- package/public/images/lytx_light_dashboard.png +0 -0
- package/public/images/safari-pinned-tab.svg +4 -0
- package/public/logo.png +0 -0
- package/public/site.webmanifest +26 -0
- package/public/sw.js +107 -0
- package/src/Document.tsx +86 -0
- package/src/api/ai_api.ts +1156 -0
- package/src/api/authMiddleware.ts +45 -0
- package/src/api/auth_api.ts +465 -0
- package/src/api/event_labels_api.ts +193 -0
- package/src/api/events_api.ts +210 -0
- package/src/api/queueWorker.ts +303 -0
- package/src/api/reports_api.ts +278 -0
- package/src/api/seed_api.ts +288 -0
- package/src/api/sites_api.ts +904 -0
- package/src/api/tag_api.ts +458 -0
- package/src/api/tag_api_v2.ts +289 -0
- package/src/api/team_api.ts +456 -0
- package/src/app/Dashboard.tsx +1339 -0
- package/src/app/Events.tsx +974 -0
- package/src/app/Explore.tsx +312 -0
- package/src/app/Layout.tsx +58 -0
- package/src/app/Settings.tsx +1302 -0
- package/src/app/components/DashboardCard.tsx +118 -0
- package/src/app/components/EditableCell.tsx +123 -0
- package/src/app/components/EventForm.tsx +93 -0
- package/src/app/components/MarketingFooter.tsx +49 -0
- package/src/app/components/MarketingNav.tsx +150 -0
- package/src/app/components/Nav.tsx +755 -0
- package/src/app/components/NewSiteSetup.tsx +298 -0
- package/src/app/components/SQLEditor.tsx +740 -0
- package/src/app/components/SiteSelector.tsx +126 -0
- package/src/app/components/SiteTag.tsx +42 -0
- package/src/app/components/SiteTagInstallCard.tsx +241 -0
- package/src/app/components/WorldMapCard.tsx +337 -0
- package/src/app/components/charts/ChartComponents.tsx +1481 -0
- package/src/app/components/charts/EventFunnel.tsx +45 -0
- package/src/app/components/charts/EventSummary.tsx +194 -0
- package/src/app/components/charts/SankeyFlows.tsx +72 -0
- package/src/app/components/marketing/CheckIcon.tsx +16 -0
- package/src/app/components/marketing/MarketingLayout.tsx +23 -0
- package/src/app/components/marketing/SectionHeading.tsx +35 -0
- package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
- package/src/app/components/reports/CreateReportStarter.tsx +74 -0
- package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
- package/src/app/components/reports/DashboardToolbar.tsx +154 -0
- package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
- package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
- package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
- package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
- package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
- package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
- package/src/app/components/reports/custom/chartPalettes.ts +18 -0
- package/src/app/components/reports/custom/types.ts +50 -0
- package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
- package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
- package/src/app/components/ui/AlertBanner.tsx +101 -0
- package/src/app/components/ui/Button.tsx +55 -0
- package/src/app/components/ui/Card.tsx +80 -0
- package/src/app/components/ui/Input.tsx +72 -0
- package/src/app/components/ui/Link.tsx +23 -0
- package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
- package/src/app/components/ui/ThemeToggle.tsx +54 -0
- package/src/app/constants.ts +6 -0
- package/src/app/headers.ts +33 -0
- package/src/app/providers/AuthProvider.tsx +189 -0
- package/src/app/providers/ClientProviders.tsx +18 -0
- package/src/app/providers/QueryProvider.tsx +23 -0
- package/src/app/providers/ThemeProvider.tsx +88 -0
- package/src/app/utils/chartThemes.ts +146 -0
- package/src/app/utils/keybinds.ts +96 -0
- package/src/app/utils/media.tsx +24 -0
- package/src/client.tsx +114 -0
- package/src/config/createLytxAppConfig.ts +252 -0
- package/src/config/resourceNames.ts +88 -0
- package/src/db/index.ts +67 -0
- package/src/index.css +285 -0
- package/src/lib/featureFlags.ts +69 -0
- package/src/pages/GetStarted.tsx +290 -0
- package/src/pages/Home.tsx +268 -0
- package/src/pages/Login.tsx +283 -0
- package/src/pages/PrivacyPolicy.tsx +120 -0
- package/src/pages/Signup.tsx +267 -0
- package/src/pages/TermsOfService.tsx +126 -0
- package/src/pages/VerifyEmail.tsx +56 -0
- package/src/session/durableObject.ts +7 -0
- package/src/session/siteSchema.ts +86 -0
- package/src/session/types.ts +36 -0
- package/src/templates/README.md +80 -0
- package/src/templates/cleanFunctions.js +44 -0
- package/src/templates/embedFunctions.js +52 -0
- package/src/templates/lytx-shared.ts +662 -0
- package/src/templates/lytxpixel-core.ts +144 -0
- package/src/templates/lytxpixel.ts +267 -0
- package/src/templates/lytxpixelBrowser.js +634 -0
- package/src/templates/lytxpixelBrowser.mjs +634 -0
- package/src/templates/parseData.js +12 -0
- package/src/templates/script.ts +31 -0
- package/src/templates/template.tsx +50 -0
- package/src/templates/test.js +3 -0
- package/src/templates/trackWebEvents.ts +177 -0
- package/src/templates/vendors/clickcease.ts +8 -0
- package/src/templates/vendors/google.ts +174 -0
- package/src/templates/vendors/linkedin.ts +23 -0
- package/src/templates/vendors/meta.ts +56 -0
- package/src/templates/vendors/quantcast.ts +22 -0
- package/src/templates/vendors/simplfi.ts +7 -0
- package/src/types/app-context.ts +16 -0
- package/src/utilities/dashboardParams.ts +188 -0
- package/src/utilities/dashboardQueries.ts +537 -0
- package/src/utilities/dashboardTransforms.ts +167 -0
- package/src/utilities/dataValidation.ts +414 -0
- package/src/utilities/detector.ts +73 -0
- package/src/utilities/encrypt.ts +103 -0
- package/src/utilities/index.ts +13 -0
- package/src/utilities/parser.ts +117 -0
- package/src/utilities/performanceMonitoring.ts +570 -0
- package/src/utilities/route_interuptors.ts +24 -0
- package/src/worker.tsx +675 -0
- package/tsconfig.json +78 -0
- package/types/env.d.ts +16 -0
- package/types/rw.d.ts +7 -0
- package/types/shims.d.ts +53 -0
- package/types/vite.d.ts +19 -0
- package/vite/vite-plugin-pixel-bundle.ts +126 -0
- package/vite.config.ts +53 -0
- package/worker-configuration.d.ts +8401 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import { route, prefix } from "rwsdk/router";
|
|
2
|
+
import type { RequestInfo } from "rwsdk/worker";
|
|
3
|
+
import type { AppContext } from "@/types/app-context";
|
|
4
|
+
import type { UserRole } from "@db/types";
|
|
5
|
+
import { IS_DEV } from "rwsdk/constants";
|
|
6
|
+
import { onlyAllowPost } from "@utilities/route_interuptors";
|
|
7
|
+
import { updateTeamName, getTeamMembers, userExists, getTeamSettings, addApiKey } from "@db/d1/teams";
|
|
8
|
+
import { d1_client } from "@db/d1/client";
|
|
9
|
+
import { type AllowedMembers, type Permissions, type AllowedSiteIds, invited_user, team, team_member } from "@db/d1/schema";
|
|
10
|
+
import { and, eq } from "drizzle-orm";
|
|
11
|
+
import { newAccountInviteEmail, sendTeamInviteEmail } from "@lib/sendMail";
|
|
12
|
+
import * as schema from "@db/d1/schema";
|
|
13
|
+
import { getSiteFromContext } from "@/api/authMiddleware";
|
|
14
|
+
// import { auth } from "@lib/auth";
|
|
15
|
+
|
|
16
|
+
type TeamRequestInfo = RequestInfo<any, AppContext>;
|
|
17
|
+
const teamRoute = <TPath extends string>(
|
|
18
|
+
path: TPath,
|
|
19
|
+
handlers: Parameters<typeof route<TPath, TeamRequestInfo>>[1],
|
|
20
|
+
) => route<TPath, TeamRequestInfo>(path, handlers);
|
|
21
|
+
|
|
22
|
+
//PERF: ALL ROUTES HERE WILL BE PREFIXED WITH /api/team
|
|
23
|
+
export const get_team_members = () =>
|
|
24
|
+
teamRoute("/members", [
|
|
25
|
+
async ({ ctx }) => {
|
|
26
|
+
const teamMembers = await getTeamMembers(ctx.team.id)
|
|
27
|
+
if (teamMembers.length === 0) {
|
|
28
|
+
return new Response("No team members found", { status: 404 });
|
|
29
|
+
} else {
|
|
30
|
+
return new Response(JSON.stringify(teamMembers), {
|
|
31
|
+
status: 200,
|
|
32
|
+
headers: { "Content-Type": "application/json" },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]);
|
|
37
|
+
export const get_team_settings = () =>
|
|
38
|
+
teamRoute("/settings", [
|
|
39
|
+
async ({ ctx }) => {
|
|
40
|
+
const { members, keys, sites, pendingInvites } = await getTeamSettings(ctx.team.id);
|
|
41
|
+
if (members.length === 0) {
|
|
42
|
+
return new Response("No team members found", { status: 404 });
|
|
43
|
+
} else {
|
|
44
|
+
return new Response(JSON.stringify({ members, keys, sites, pendingInvites }), {
|
|
45
|
+
status: 200,
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
]);
|
|
51
|
+
type Team_Option = "name";
|
|
52
|
+
const update_team = teamRoute("/update", [
|
|
53
|
+
onlyAllowPost,
|
|
54
|
+
async ({ ctx, request }) => {
|
|
55
|
+
if (IS_DEV)
|
|
56
|
+
console.log(
|
|
57
|
+
"🔥🔥🔥 Team update endpoint hit",
|
|
58
|
+
request.method,
|
|
59
|
+
request.url,
|
|
60
|
+
);
|
|
61
|
+
//create a type that can recieve other team values
|
|
62
|
+
const body = (await request.json()) as {
|
|
63
|
+
option: Team_Option;
|
|
64
|
+
name: string;
|
|
65
|
+
};
|
|
66
|
+
if (!body.option || !body.name)
|
|
67
|
+
return new Response("Invalid request", { status: 400 });
|
|
68
|
+
const checkIfUpdated = await updateTeamName(body.name, ctx.team.id);
|
|
69
|
+
if (body.option === "name" && IS_DEV) {
|
|
70
|
+
console.log("Team name updated", checkIfUpdated, ctx.team);
|
|
71
|
+
}
|
|
72
|
+
if (IS_DEV) console.log(checkIfUpdated, ctx.team, body.option, body.name);
|
|
73
|
+
return new Response("Ok", { status: 201 });
|
|
74
|
+
//return new Response("Not Found.", { status: 404 });
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const add_api_key = teamRoute("/add-api-key", [
|
|
79
|
+
onlyAllowPost,
|
|
80
|
+
async ({ ctx, request }) => {
|
|
81
|
+
if (IS_DEV)
|
|
82
|
+
console.log(
|
|
83
|
+
"🔥🔥🔥 Add team member endpoint hit",
|
|
84
|
+
request.method,
|
|
85
|
+
request.url,
|
|
86
|
+
);
|
|
87
|
+
//check
|
|
88
|
+
const body = await request.json() as {
|
|
89
|
+
site_id?: number;
|
|
90
|
+
permissions: Permissions;
|
|
91
|
+
allowed_team_members?: AllowedMembers;
|
|
92
|
+
};
|
|
93
|
+
if (!body.permissions) {
|
|
94
|
+
return new Response("Invalid request need permissions", { status: 400 });
|
|
95
|
+
}
|
|
96
|
+
if (typeof body.site_id !== "number" || !Number.isInteger(body.site_id)) {
|
|
97
|
+
return new Response("Invalid request need site_id", { status: 400 });
|
|
98
|
+
}
|
|
99
|
+
if (ctx.user_role !== "admin") {
|
|
100
|
+
return new Response("You must be an admin to add an API key", { status: 400 });
|
|
101
|
+
}
|
|
102
|
+
const siteDetails = getSiteFromContext(ctx, body.site_id);
|
|
103
|
+
if (!siteDetails) {
|
|
104
|
+
return new Response("Invalid site_id for current team", { status: 403 });
|
|
105
|
+
}
|
|
106
|
+
const checkIfUpdated = await addApiKey(
|
|
107
|
+
{
|
|
108
|
+
team_id: ctx.team.id,
|
|
109
|
+
site_id: body.site_id,
|
|
110
|
+
permissions: body.permissions,
|
|
111
|
+
allowed_team_members: body.allowed_team_members ?? undefined
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
if (IS_DEV) console.log(checkIfUpdated, ctx.team,);
|
|
115
|
+
return new Response(JSON.stringify(checkIfUpdated), {
|
|
116
|
+
status: 201,
|
|
117
|
+
headers: { "Content-Type": "application/json" },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
]);
|
|
121
|
+
const add_team_member = teamRoute("/add-member", [
|
|
122
|
+
onlyAllowPost,
|
|
123
|
+
async ({ ctx, request }) => {
|
|
124
|
+
if (IS_DEV)
|
|
125
|
+
console.log(
|
|
126
|
+
"🔥🔥🔥 Add team member endpoint hit",
|
|
127
|
+
request.method,
|
|
128
|
+
request.url,
|
|
129
|
+
);
|
|
130
|
+
//check
|
|
131
|
+
const body = (await request.json()) as { email: string, name: string, role: UserRole };
|
|
132
|
+
const domainUrl = new URL(request.url);
|
|
133
|
+
if (!body.email) {
|
|
134
|
+
return new Response("Missing user_id", { status: 400 });
|
|
135
|
+
}
|
|
136
|
+
if (ctx.user_role !== "admin") {
|
|
137
|
+
return new Response("You must be an admin to add team members", { status: 403 });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const normalizedEmail = body.email.trim().toLowerCase();
|
|
141
|
+
|
|
142
|
+
const existingInvite = await d1_client
|
|
143
|
+
.select({ id: invited_user.id })
|
|
144
|
+
.from(invited_user)
|
|
145
|
+
.where(
|
|
146
|
+
and(
|
|
147
|
+
eq(invited_user.team_id, ctx.team.id),
|
|
148
|
+
eq(invited_user.email, normalizedEmail),
|
|
149
|
+
eq(invited_user.accepted, false),
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
.limit(1);
|
|
153
|
+
|
|
154
|
+
if (existingInvite.length > 0) {
|
|
155
|
+
return new Response("Invitation already pending for this email", { status: 409 });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { create, addToTeam, userDetails } = await userExists(normalizedEmail, ctx.team.id);
|
|
159
|
+
if (IS_DEV) console.log(create, addToTeam, userDetails);
|
|
160
|
+
|
|
161
|
+
if (!create && !addToTeam) {
|
|
162
|
+
return new Response("User already exists", { status: 400 });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (addToTeam && userDetails) {
|
|
166
|
+
await d1_client.insert(team_member).values({
|
|
167
|
+
team_id: ctx.team.id,
|
|
168
|
+
user_id: userDetails.id,
|
|
169
|
+
role: body.role,
|
|
170
|
+
});
|
|
171
|
+
await newAccountInviteEmail(normalizedEmail, `${domainUrl.host}/login`);
|
|
172
|
+
return new Response(JSON.stringify({ success: true, inviteSent: true, memberVisible: true }), {
|
|
173
|
+
status: 201,
|
|
174
|
+
headers: { "Content-Type": "application/json" },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (create && !userDetails) {
|
|
178
|
+
await d1_client.insert(invited_user).values({
|
|
179
|
+
team_id: ctx.team.id,
|
|
180
|
+
email: normalizedEmail,
|
|
181
|
+
name: body.name,
|
|
182
|
+
role: body.role,
|
|
183
|
+
accepted: false,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await sendTeamInviteEmail(normalizedEmail, `${domainUrl.host}/signup`);
|
|
187
|
+
return new Response(JSON.stringify({ success: true, inviteSent: true, memberVisible: false }), {
|
|
188
|
+
status: 201,
|
|
189
|
+
headers: { "Content-Type": "application/json" },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return new Response("Failed to add team member", { status: 400 });
|
|
193
|
+
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
const create_team = teamRoute("/create", [
|
|
198
|
+
onlyAllowPost,
|
|
199
|
+
async ({ ctx, request }) => {
|
|
200
|
+
const requestId = crypto.randomUUID();
|
|
201
|
+
let body: unknown;
|
|
202
|
+
try {
|
|
203
|
+
body = await request.json();
|
|
204
|
+
} catch {
|
|
205
|
+
return new Response(JSON.stringify({ error: "Invalid JSON", requestId }), {
|
|
206
|
+
status: 400,
|
|
207
|
+
headers: { "Content-Type": "application/json" },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!body || typeof body !== "object") {
|
|
212
|
+
return new Response(JSON.stringify({ error: "Invalid request body", requestId }), {
|
|
213
|
+
status: 400,
|
|
214
|
+
headers: { "Content-Type": "application/json" },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const { name } = body as { name?: unknown };
|
|
219
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
220
|
+
return new Response(JSON.stringify({ error: "name is required", requestId }), {
|
|
221
|
+
status: 400,
|
|
222
|
+
headers: { "Content-Type": "application/json" },
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const trimmedName = name.trim();
|
|
227
|
+
if (trimmedName.length < 2 || trimmedName.length > 80) {
|
|
228
|
+
return new Response(JSON.stringify({ error: "name must be 2-80 characters", requestId }), {
|
|
229
|
+
status: 400,
|
|
230
|
+
headers: { "Content-Type": "application/json" },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const [newTeam] = await d1_client
|
|
236
|
+
.insert(team)
|
|
237
|
+
.values({
|
|
238
|
+
name: trimmedName,
|
|
239
|
+
created_by: ctx.session.user.id,
|
|
240
|
+
})
|
|
241
|
+
.returning();
|
|
242
|
+
|
|
243
|
+
if (!newTeam) {
|
|
244
|
+
return new Response(JSON.stringify({ error: "Failed to create team", requestId }), {
|
|
245
|
+
status: 500,
|
|
246
|
+
headers: { "Content-Type": "application/json" },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await d1_client.insert(team_member).values({
|
|
251
|
+
team_id: newTeam.id,
|
|
252
|
+
user_id: ctx.session.user.id,
|
|
253
|
+
role: "admin",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return new Response(
|
|
257
|
+
JSON.stringify({ team_id: newTeam.id, name: newTeam.name, requestId }),
|
|
258
|
+
{
|
|
259
|
+
status: 201,
|
|
260
|
+
headers: { "Content-Type": "application/json" },
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error("Create team error:", { requestId, error });
|
|
265
|
+
return new Response(JSON.stringify({ error: "Internal server error", requestId }), {
|
|
266
|
+
status: 500,
|
|
267
|
+
headers: { "Content-Type": "application/json" },
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
const update_member_sites = teamRoute(
|
|
274
|
+
"/update-member-sites",
|
|
275
|
+
[
|
|
276
|
+
onlyAllowPost,
|
|
277
|
+
async ({ ctx, request }) => {
|
|
278
|
+
const body = (await request.json()) as {
|
|
279
|
+
user_id?: string;
|
|
280
|
+
allowed_site_ids?: AllowedSiteIds;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if (!body.user_id || !body.allowed_site_ids) {
|
|
284
|
+
return new Response("Invalid request", { status: 400 });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (ctx.user_role !== "admin") {
|
|
288
|
+
return new Response("You must be an admin to update member sites", {
|
|
289
|
+
status: 403,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const allowed = body.allowed_site_ids;
|
|
294
|
+
if (!Array.isArray(allowed)) {
|
|
295
|
+
return new Response("allowed_site_ids must be an array", { status: 400 });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const allowedSiteIds = allowed
|
|
299
|
+
.filter((id): id is number => typeof id === "number" && Number.isInteger(id));
|
|
300
|
+
|
|
301
|
+
if (!allowed.includes("all") && allowedSiteIds.length !== allowed.length) {
|
|
302
|
+
return new Response(
|
|
303
|
+
"allowed_site_ids must be 'all' or a list of site ids",
|
|
304
|
+
{ status: 400 },
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!allowed.includes("all") && allowedSiteIds.length > 0) {
|
|
309
|
+
const teamSites = await d1_client
|
|
310
|
+
.select({ site_id: schema.sites.site_id })
|
|
311
|
+
.from(schema.sites)
|
|
312
|
+
.where(eq(schema.sites.team_id, ctx.team.id));
|
|
313
|
+
const teamSiteIds = new Set(teamSites.map((s) => s.site_id));
|
|
314
|
+
|
|
315
|
+
const invalidIds = allowedSiteIds.filter((id) => !teamSiteIds.has(id));
|
|
316
|
+
if (invalidIds.length > 0) {
|
|
317
|
+
return new Response(
|
|
318
|
+
`Invalid site ids: ${invalidIds.join(", ")}`,
|
|
319
|
+
{ status: 400 },
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const updated = await d1_client
|
|
325
|
+
.update(team_member)
|
|
326
|
+
.set({ allowed_site_ids: allowed })
|
|
327
|
+
.where(
|
|
328
|
+
and(
|
|
329
|
+
eq(team_member.team_id, ctx.team.id),
|
|
330
|
+
eq(team_member.user_id, body.user_id),
|
|
331
|
+
),
|
|
332
|
+
)
|
|
333
|
+
.returning();
|
|
334
|
+
|
|
335
|
+
if (updated.length === 0) {
|
|
336
|
+
return new Response("Team member not found", { status: 404 });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return new Response(JSON.stringify(updated[0]), {
|
|
340
|
+
status: 200,
|
|
341
|
+
headers: { "Content-Type": "application/json" },
|
|
342
|
+
});
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const update_member_role = teamRoute(
|
|
348
|
+
"/update-member-role",
|
|
349
|
+
[
|
|
350
|
+
onlyAllowPost,
|
|
351
|
+
async ({ ctx, request }) => {
|
|
352
|
+
const body = (await request.json()) as {
|
|
353
|
+
user_id?: string;
|
|
354
|
+
role?: UserRole;
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
if (!body.user_id || !body.role) {
|
|
358
|
+
return new Response("Invalid request", { status: 400 });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!["admin", "editor", "viewer"].includes(body.role)) {
|
|
362
|
+
return new Response("Invalid role", { status: 400 });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (ctx.user_role !== "admin") {
|
|
366
|
+
return new Response("You must be an admin to update member roles", {
|
|
367
|
+
status: 403,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const updated = await d1_client
|
|
372
|
+
.update(team_member)
|
|
373
|
+
.set({ role: body.role })
|
|
374
|
+
.where(
|
|
375
|
+
and(
|
|
376
|
+
eq(team_member.team_id, ctx.team.id),
|
|
377
|
+
eq(team_member.user_id, body.user_id),
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
.returning();
|
|
381
|
+
|
|
382
|
+
if (updated.length === 0) {
|
|
383
|
+
return new Response("Team member not found", { status: 404 });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return new Response(JSON.stringify(updated[0]), {
|
|
387
|
+
status: 200,
|
|
388
|
+
headers: { "Content-Type": "application/json" },
|
|
389
|
+
});
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const remove_pending_invite = teamRoute(
|
|
395
|
+
"/remove-pending-invite",
|
|
396
|
+
[
|
|
397
|
+
onlyAllowPost,
|
|
398
|
+
async ({ ctx, request }) => {
|
|
399
|
+
const body = (await request.json()) as {
|
|
400
|
+
invite_id?: number;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
if (!Number.isInteger(body.invite_id)) {
|
|
404
|
+
return new Response("Invalid invite_id", { status: 400 });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (ctx.user_role !== "admin") {
|
|
408
|
+
return new Response("You must be an admin to remove pending invites", {
|
|
409
|
+
status: 403,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const inviteId = body.invite_id as number;
|
|
414
|
+
const targetInvite = await d1_client
|
|
415
|
+
.select({ id: invited_user.id })
|
|
416
|
+
.from(invited_user)
|
|
417
|
+
.where(
|
|
418
|
+
and(
|
|
419
|
+
eq(invited_user.team_id, ctx.team.id),
|
|
420
|
+
eq(invited_user.id, inviteId),
|
|
421
|
+
),
|
|
422
|
+
)
|
|
423
|
+
.limit(1);
|
|
424
|
+
|
|
425
|
+
if (targetInvite.length === 0) {
|
|
426
|
+
return new Response("Pending invite not found", { status: 404 });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
await d1_client
|
|
430
|
+
.delete(invited_user)
|
|
431
|
+
.where(
|
|
432
|
+
and(
|
|
433
|
+
eq(invited_user.team_id, ctx.team.id),
|
|
434
|
+
eq(invited_user.id, inviteId),
|
|
435
|
+
),
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
return new Response(JSON.stringify({ success: true, invite_id: inviteId }), {
|
|
439
|
+
status: 200,
|
|
440
|
+
headers: { "Content-Type": "application/json" },
|
|
441
|
+
});
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
export const team_dashboard_endpoints = prefix<"/team", TeamRequestInfo>("/team", [
|
|
447
|
+
get_team_members(),
|
|
448
|
+
update_team,
|
|
449
|
+
create_team,
|
|
450
|
+
add_team_member,
|
|
451
|
+
get_team_settings(),
|
|
452
|
+
add_api_key,
|
|
453
|
+
update_member_sites,
|
|
454
|
+
update_member_role,
|
|
455
|
+
remove_pending_invite,
|
|
456
|
+
]);
|