diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12d49b5..dcdb5b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: Build and Push Docker Images on: push: - branches: [ main, leafd/launch ] + branches: [ main, leafd/launch, manitej/app ] jobs: build: @@ -24,24 +24,23 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: | - ghcr.io/${{ github.repository }}/owl-api - ghcr.io/${{ github.repository }}/lark-ui - tags: | - type=ref,event=branch - type=sha,prefix={{branch}}- - type=raw,value=latest,enable={{is_default_branch}} + - name: Set tag prefix + id: tag-prefix + run: | + if [ "${{ github.ref_name }}" = "main" ]; then + echo "prefix=prod-" >> $GITHUB_OUTPUT + else + echo "prefix=staging-" >> $GITHUB_OUTPUT + fi - name: Build and push owl-api uses: docker/build-push-action@v5 with: context: . file: ./owl-api/Dockerfile - tags: ghcr.io/${{ github.repository }}/owl-api:latest,ghcr.io/${{ github.repository }}/owl-api:${{ github.sha }} + tags: | + ghcr.io/${{ github.repository }}/owl-api:latest + ghcr.io/${{ github.repository }}/owl-api:${{ steps.tag-prefix.outputs.prefix }}${{ github.sha }} push: true cache-from: type=gha cache-to: type=gha,mode=max @@ -51,7 +50,9 @@ jobs: with: context: . file: ./lark-ui/Dockerfile - tags: ghcr.io/${{ github.repository }}/lark-ui:latest,ghcr.io/${{ github.repository }}/lark-ui:${{ github.sha }} + tags: | + ghcr.io/${{ github.repository }}/lark-ui:latest + ghcr.io/${{ github.repository }}/lark-ui:${{ steps.tag-prefix.outputs.prefix }}${{ github.sha }} push: true cache-from: type=gha cache-to: type=gha,mode=max diff --git a/env.example b/env.example index f0ca8b3..77e5037 100644 --- a/env.example +++ b/env.example @@ -21,6 +21,10 @@ REDIS_DB=0 # Admin Configuration ADMIN_EMAIL_WHITELIST=admin@example.com,admin2@example.com +# Hackatime Configuration +HACKATIME_ADMIN_API_URL=https://hackatime.hackclub.com/api/admin/v1 +HACKATIME_API_KEY=your_hackatime_api_key_here + # Mail Service Configuration MAIL_SERVICE_PORT=3003 MAIL_SERVICE_CORS_ORIGIN=http://localhost:3000 @@ -42,6 +46,15 @@ LARK_UI_PORT=5173 PUBLIC_API_URL=http://localhost:3000 VITE_API_URL=http://localhost:3002 +# Datadog Configuration (optional) +DD_AGENT_HOST=localhost +DD_TRACE_AGENT_PORT=8126 +DD_APM_ENABLED=true +DD_PROFILING_ENABLED=true +DD_SERVICE=owl-api +DD_ENV=production +DD_VERSION=1.0.0 + # Production Settings (uncomment for production) # USER_SERVICE_NODE_ENV=production # CORS_ORIGIN=https://your-frontend-domain.com,https://your-api-domain.com diff --git a/gateway/src/main.ts b/gateway/src/main.ts index 4872fe0..d2ed3d7 100644 --- a/gateway/src/main.ts +++ b/gateway/src/main.ts @@ -10,11 +10,11 @@ if (process.env.NODE_ENV !== 'production') { const app = express(); const PORT = process.env.PORT || 3000; -const USER_SERVICE_URL = process.env.USER_SERVICE_URL || 'http://localhost:3002'; -const MAIL_SERVICE_URL = process.env.MAIL_SERVICE_URL || 'http://localhost:3002'; +const SERVICE_URL = process.env.SERVICE_URL || 'http://localhost:3002'; const UI_SERVICE_URL = process.env.UI_SERVICE_URL || 'http://localhost:5173'; app.use(cors()); +app.use(express.json()); const createServiceProxy = (serviceName: string, targetUrl: string) => { return createProxyMiddleware({ @@ -22,10 +22,11 @@ const createServiceProxy = (serviceName: string, targetUrl: string) => { changeOrigin: true, logLevel: 'debug', pathRewrite: (path, req) => { - return path.replace(`/api/${serviceName}`, ''); + // Don't rewrite the path - keep the full API path for the unified service + return path; }, onProxyReq: (proxyReq, req, res) => { - console.log(`[${serviceName.toUpperCase()} β†’] ${req.method} ${req.url} -> ${targetUrl}${req.url.replace(`/api/${serviceName}`, '')}`); + console.log(`[${serviceName.toUpperCase()} β†’] ${req.method} ${req.url} -> ${targetUrl}${req.url}`); if (req.body && Object.keys(req.body).length > 0) { const bodyData = JSON.stringify(req.body); @@ -54,7 +55,7 @@ const createServiceProxy = (serviceName: string, targetUrl: string) => { }); }; -app.use('/api/user', express.json(), createServiceProxy('user', USER_SERVICE_URL)); +app.use('/api', express.json(), createServiceProxy('unified', SERVICE_URL)); app.use('/', createProxyMiddleware({ target: UI_SERVICE_URL, @@ -73,7 +74,6 @@ app.use('/', createProxyMiddleware({ app.listen(PORT, () => { console.log(`πŸŒ™ Gateway ready at http://localhost:${PORT}`); console.log(` / β†’ UI (${UI_SERVICE_URL})`); - console.log(` /api/* β†’ Unified Service (${USER_SERVICE_URL})`); - console.log(` Mail Service (${MAIL_SERVICE_URL}) - Internal only`); + console.log(` /api/* β†’ Unified Service (${SERVICE_URL})`); }); diff --git a/lark-ui/src/lib/BottomNavigation.svelte b/lark-ui/src/lib/BottomNavigation.svelte index adcef60..20fe044 100644 --- a/lark-ui/src/lib/BottomNavigation.svelte +++ b/lark-ui/src/lib/BottomNavigation.svelte @@ -1,78 +1,67 @@ - - - -
-
- Create dialogue bubble + +
+ {#if !onboarding} + Notification + {/if}
- - -
diff --git a/lark-ui/src/lib/Button.svelte b/lark-ui/src/lib/Button.svelte new file mode 100644 index 0000000..ff68b7b --- /dev/null +++ b/lark-ui/src/lib/Button.svelte @@ -0,0 +1,121 @@ + + + + + diff --git a/lark-ui/src/lib/Texture.svelte b/lark-ui/src/lib/Texture.svelte new file mode 100644 index 0000000..4dfcf4c --- /dev/null +++ b/lark-ui/src/lib/Texture.svelte @@ -0,0 +1,69 @@ +
+ +
+ +
+
+
+ + \ No newline at end of file diff --git a/lark-ui/src/lib/auth.ts b/lark-ui/src/lib/auth.ts new file mode 100644 index 0000000..a965f1e --- /dev/null +++ b/lark-ui/src/lib/auth.ts @@ -0,0 +1,198 @@ +import { env } from "$env/dynamic/public"; + +const apiUrl = env.PUBLIC_API_URL || ''; + +//auth + +export type User = { + userId: string; + email: string; + firstName: string; + lastName: string; + birthday: string; + role: string; + onboardComplete: boolean; + createdAt: string; + updatedAt: string; + hackatimeAccount: string | null; +}; + +export async function checkAuthStatus() { + const response = await fetch(`${apiUrl}/api/user/auth/me`, { + credentials: 'include' + }); + + if (response.ok) { + const userData = await response.json(); + return userData as User; + } else { + return null; + } +} + +export async function updateUser(data: { + firstName: string; + lastName: string; + birthday: string; +}) { + return await fetch(`${apiUrl}/api/user`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + firstName: data.firstName, + lastName: data.lastName, + birthday: data.birthday, + }) + }); +} + +export async function completeOnboarding() { + return await fetch(`${apiUrl}/api/user/auth/complete-onboarding`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + }); +} + + +// projects + +export type Project = { + projectId: string; + userId: string; + projectTitle: string; + projectType: 'personal_website' | 'platformer_game' | 'wildcard'; + description: string; + createdAt: Date; + updatedAt: Date; + nowHackatimeHours: number | null; + nowHackatimeProjects: string[] | null; +}; + +export async function createProject(data: { + projectTitle: string; + projectType: string; + projectDescription: string; +}) { + const response = await fetch(`${apiUrl}/api/projects/auth`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(data) + }); + + if (response.ok) { + const project = await response.json(); + return project as Project; + } else { + return null; + } +} + +export async function getProjects() { + const response = await fetch(`${apiUrl}/api/projects/auth`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include' + }); + + if (response.ok) { + const projects = await response.json(); + return projects as Project[]; + } else { + return []; + } +} + +export async function getProject(id: string) { + const response = await fetch(`${apiUrl}/api/projects/auth/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include' + }); + + if (response.ok) { + const project = await response.json(); + return project as Project; + } else { + return null; + } +} + +//hackatime + +export type HackatimeProject = { + name: string +} + +export async function checkHackatimeAccount() { + const response = await fetch(`${apiUrl}/api/user/hackatime-account`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include' + }); + + if (response.ok) { + const data = await response.json(); + return data; + } else { + return null; + } +} + +export async function getHackatimeProjects() { + const response = await fetch(`${apiUrl}/api/user/hackatime-projects`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include' + }); + + if (response.ok) { + const data = await response.json(); + return data as { + projects: HackatimeProject[] + }; + } else { + return null; + } +} + +export async function linkHackatimeProjects(projectId: string, projectNames: string[]) { + const response = await fetch(`${apiUrl}/api/projects/auth/${projectId}/hackatime-projects`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ projectNames }) + }); + + if (response.ok) { + return true; + } else { + return false; + } +} + +// hackatime setup + +export async function sendHackatimeOtp(email: string) { + const response = await fetch(`${apiUrl}/api/user/auth/hackatime-link/send-otp`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ email }) + }); + + return response; +} + +export async function verifyHackatimeOtp(otp: string) { + const response = await fetch(`${apiUrl}/api/user/auth/hackatime-link/verify-otp`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ otp }) + }); + + return response; +} \ No newline at end of file diff --git a/lark-ui/src/lib/cards/BaseCard.svelte b/lark-ui/src/lib/cards/BaseCard.svelte new file mode 100644 index 0000000..fc5ebf1 --- /dev/null +++ b/lark-ui/src/lib/cards/BaseCard.svelte @@ -0,0 +1,54 @@ + + + +
+
+
+
+ {@render children?.()} +
+
+
+ + diff --git a/lark-ui/src/lib/cards/FlexCard.svelte b/lark-ui/src/lib/cards/FlexCard.svelte new file mode 100644 index 0000000..79d6318 --- /dev/null +++ b/lark-ui/src/lib/cards/FlexCard.svelte @@ -0,0 +1,54 @@ + + + +
+
+
+
+ {@render children?.()} +
+
+
+ + diff --git a/lark-ui/src/lib/cards/NewProjectCard.svelte b/lark-ui/src/lib/cards/NewProjectCard.svelte new file mode 100644 index 0000000..c9b545f --- /dev/null +++ b/lark-ui/src/lib/cards/NewProjectCard.svelte @@ -0,0 +1,58 @@ + + + + {#snippet children()} +
+

+ NEW + PROJECT +

+ +
+ Add +
+
+ {/snippet} +
+ + diff --git a/lark-ui/src/lib/cards/ProjectCard.svelte b/lark-ui/src/lib/cards/ProjectCard.svelte new file mode 100644 index 0000000..ab0fe04 --- /dev/null +++ b/lark-ui/src/lib/cards/ProjectCard.svelte @@ -0,0 +1,48 @@ + + + + {#snippet children()} +
+

+ {title} +

+
+ {/snippet} +
+ + diff --git a/lark-ui/src/lib/cards/ProjectCardPreview.svelte b/lark-ui/src/lib/cards/ProjectCardPreview.svelte new file mode 100644 index 0000000..55b83de --- /dev/null +++ b/lark-ui/src/lib/cards/ProjectCardPreview.svelte @@ -0,0 +1,48 @@ + + + + {#snippet children()} +
+

+ {title} +

+
+ {/snippet} +
+ + diff --git a/lark-ui/src/lib/cards/ProjectType.svelte b/lark-ui/src/lib/cards/ProjectType.svelte new file mode 100644 index 0000000..de6b9c9 --- /dev/null +++ b/lark-ui/src/lib/cards/ProjectType.svelte @@ -0,0 +1,24 @@ + + + + Personal Website + + + diff --git a/lark-ui/src/lib/hackatime/HackatimeAccountModal.svelte b/lark-ui/src/lib/hackatime/HackatimeAccountModal.svelte new file mode 100644 index 0000000..47c0be1 --- /dev/null +++ b/lark-ui/src/lib/hackatime/HackatimeAccountModal.svelte @@ -0,0 +1,441 @@ + + + + + diff --git a/lark-ui/src/lib/hackatime/HackatimeEntry.svelte b/lark-ui/src/lib/hackatime/HackatimeEntry.svelte new file mode 100644 index 0000000..0e45fba --- /dev/null +++ b/lark-ui/src/lib/hackatime/HackatimeEntry.svelte @@ -0,0 +1,116 @@ + + +
+ {#if variant === "empty"} +
+

{projectName}

+
+ {:else} +
+

{projectName}

+ +

+ 2 HOURS ALL TIME +

+ +

/path/ β€’ githubUrl

+
+ + {#if action === 'add'} + + {/if} + {#if action === 'remove'} + + {/if} + {/if} +
+ + diff --git a/lark-ui/src/lib/hackatime/HackatimeProjectModal.svelte b/lark-ui/src/lib/hackatime/HackatimeProjectModal.svelte new file mode 100644 index 0000000..18a796d --- /dev/null +++ b/lark-ui/src/lib/hackatime/HackatimeProjectModal.svelte @@ -0,0 +1,281 @@ + + + + + diff --git a/lark-ui/src/lib/onboarding/Butler.svelte b/lark-ui/src/lib/onboarding/Butler.svelte new file mode 100644 index 0000000..b6653f6 --- /dev/null +++ b/lark-ui/src/lib/onboarding/Butler.svelte @@ -0,0 +1,59 @@ + + +{#if variant == 4} +
+ Sticker + Hand +
+{:else} + Butler +{/if} + + diff --git a/lark-ui/src/lib/onboarding/CalculatorPass.svelte b/lark-ui/src/lib/onboarding/CalculatorPass.svelte new file mode 100644 index 0000000..1d9afbe --- /dev/null +++ b/lark-ui/src/lib/onboarding/CalculatorPass.svelte @@ -0,0 +1,292 @@ + + +
+
+

GET your entire flight covered

+ +

+ Only need 40hrs to attend! Code extra hours to cover your flight & hotel +

+ +
+

${grant}

+ +
+ +
+
+ +

[{hours}hrs] extra needed!

+ + +
+
+ + diff --git a/lark-ui/src/lib/onboarding/Dialogue.svelte b/lark-ui/src/lib/onboarding/Dialogue.svelte new file mode 100644 index 0000000..2f6140d --- /dev/null +++ b/lark-ui/src/lib/onboarding/Dialogue.svelte @@ -0,0 +1,207 @@ + + +
+
+
+ +

{speaker}

+
+
+
+

{text}

+

Click to continue.

+
+
+ +
+
+ +
+
+ + diff --git a/lark-ui/src/lib/onboarding/ProjectTypeSelect.svelte b/lark-ui/src/lib/onboarding/ProjectTypeSelect.svelte new file mode 100644 index 0000000..5cfcec7 --- /dev/null +++ b/lark-ui/src/lib/onboarding/ProjectTypeSelect.svelte @@ -0,0 +1,87 @@ + + +
+

CREATE NEW PROJECT

+

Build your first project... And get a holographic sticker!

+ +
+ + Personal Website + + + + Platformer Game + + + + More Projects + +
+
+ + diff --git a/lark-ui/src/routes/+layout.svelte b/lark-ui/src/routes/+layout.svelte index 389a3d2..2684db2 100644 --- a/lark-ui/src/routes/+layout.svelte +++ b/lark-ui/src/routes/+layout.svelte @@ -7,6 +7,39 @@ + + {@render children?.()} diff --git a/lark-ui/src/routes/+page.svelte b/lark-ui/src/routes/+page.svelte index 6e0d1be..f090176 100644 --- a/lark-ui/src/routes/+page.svelte +++ b/lark-ui/src/routes/+page.svelte @@ -7,42 +7,21 @@ let email = ''; let errorMessage = ''; let showModal = false; + let isSubmitting = false; function isValidEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } + async function requestOTP(event: Event) { + event.preventDefault(); + errorMessage = ""; -/* Added the clicking sound to buttons */ -let clickSound: HTMLAudioElement | undefined; - -onMount(() => { - clickSound = new Audio('/sounds/click.mp3'); - clickSound.preload = 'auto'; -}); - -function playClick() { - if (!clickSound) return; - try { - clickSound.currentTime = 0; - clickSound.play().catch(() => {}); - } catch (e) { - clickSound.play().catch(() => {}); - } -} - - /* Created a helper to add sound to faq button */ - function navigateToFaq() { - playClick(); - goto('/faq'); - } + if (isSubmitting) { + return; + } - async function handleNavigateToRsvp(event: Event) { - event.preventDefault(); - playClick(); - errorMessage = ''; - if (!email.trim()) { goto('/rsvp'); return; @@ -53,21 +32,62 @@ function playClick() { return; } - const emailToSend = email.trim(); - goto(`/rsvp?email=${encodeURIComponent(emailToSend)}`); + isSubmitting = true; + + try { + // send an otp to the user's email + const apiUrl = env.PUBLIC_API_URL || ""; + + const response = await fetch(`${apiUrl}/api/user/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ email }), + }); - const apiUrl = env.PUBLIC_API_URL || ''; - fetch(`${apiUrl}/api/user/rsvp/initial`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email: emailToSend }), - }).catch(error => { - console.error('Error submitting email:', error); - }); + console.log("sent an OTP"); + + if (!response || !response.ok) { + const data = await response.json(); + errorMessage = data.message || "Failed to send OTP. Please try again."; + return; + } + + goto("/login?email=" + encodeURIComponent(email)); + } finally { + isSubmitting = false; + } } + // async function handleNavigateToRsvp(event: Event) { + // event.preventDefault(); + // errorMessage = ''; + + // if (!email.trim()) { + // goto('/rsvp'); + // return; + // } + + // if (!isValidEmail(email)) { + // errorMessage = 'Please enter a valid email address'; + // return; + // } + + // const emailToSend = email.trim(); + // goto(`/rsvp?email=${encodeURIComponent(emailToSend)}`); + + // const apiUrl = env.PUBLIC_API_URL || ''; + // fetch(`${apiUrl}/api/user/rsvp/initial`, { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ email: emailToSend }), + // }).catch(error => { + // console.error('Error submitting email:', error); + // }); + // } + function openModal() { showModal = true; } @@ -77,36 +97,24 @@ function playClick() { } function handleKeydown(event: KeyboardEvent) { - if (event.key === 'Escape' || event.key === 'Enter' || event.key === ' ') { + if (event.key === "Escape" || event.key === "Enter" || event.key === " ") { closeModal(); } } - - - + + + diff --git a/lark-ui/src/routes/app/projects/+page.svelte b/lark-ui/src/routes/app/projects/+page.svelte new file mode 100644 index 0000000..6aa283b --- /dev/null +++ b/lark-ui/src/routes/app/projects/+page.svelte @@ -0,0 +1,144 @@ + + +
+

YouR PROJECTS

+ + {#if loading} +
Loading projects...
+ {:else if error} +
Error: {error}
+ {:else} +
+ {#each projects as project (project.projectId)} +
+ + {#if !user?.onboardComplete} + this is your project! + {/if} +
+ {/each} + +
+ {/if} + + +
+ + diff --git a/lark-ui/src/routes/app/projects/[id]/+page.svelte b/lark-ui/src/routes/app/projects/[id]/+page.svelte new file mode 100644 index 0000000..f25c35f --- /dev/null +++ b/lark-ui/src/routes/app/projects/[id]/+page.svelte @@ -0,0 +1,292 @@ + + +
+
+
+ + {#if loading} +
Loading project...
+ {:else if error} +
Error: {error}
+ {:else if project} +
+
+ +
+ +
+
+
+

{project.projectTitle}

+ {#if project.nowHackatimeHours} +

2 hours

+ {/if} +
+ +
+ {project.projectType} + {#each project.nowHackatimeProjects as hackatimeProjectName} + linked to {hackatimeProjectName} + {/each} +
+ +

+ {project.description} +

+
+ + {#if user && user.hackatimeAccount} + {#if project.nowHackatimeProjects && project.nowHackatimeProjects.length > 0} +
+
+ {:else} +
+
+ {/if} + {:else} +
+
+ {/if} +
+
+ {/if} + + {#if openHackatimeAccountModal} + { + user = await checkAuthStatus(); + openHackatimeAccountModal = false; + }} /> + {/if} + + {#if openHackatimeProjectModal && project} + { + await loadProject(); + openHackatimeProjectModal = false; + }} + projectId={project.projectId} + /> + {/if} + + +
+ + diff --git a/lark-ui/src/routes/app/projects/create/+page.svelte b/lark-ui/src/routes/app/projects/create/+page.svelte new file mode 100644 index 0000000..e975502 --- /dev/null +++ b/lark-ui/src/routes/app/projects/create/+page.svelte @@ -0,0 +1,223 @@ + + + + Create Your Personal Website - Midnight + + + + +
+
+

{config.title}

+

Don't worry about the perfect name, you can change it later!

+ +
+ + + + + +
+
+
+ + diff --git a/lark-ui/src/routes/app/projects/select/+page.svelte b/lark-ui/src/routes/app/projects/select/+page.svelte new file mode 100644 index 0000000..ab411e6 --- /dev/null +++ b/lark-ui/src/routes/app/projects/select/+page.svelte @@ -0,0 +1,64 @@ + + + + Create Your Project - Midnight + + +
+
+
+
+

Create A Project

+
+
+ + + + + + +
+
+ + diff --git a/lark-ui/src/routes/cheats/+page.svelte b/lark-ui/src/routes/cheats/+page.svelte new file mode 100644 index 0000000..8c33a93 --- /dev/null +++ b/lark-ui/src/routes/cheats/+page.svelte @@ -0,0 +1,1113 @@ + + +
+

API Test Frontend

+ + {#if user} +
+ Logged in as {user.name} ({user.email}) +
+ {#if user.role === 'admin'} + Admin Dashboard + {/if} + +
+
+ {:else} +
Not logged in
+ {/if} + + {#if message} +
{message}
+ {/if} + + {#if error} +
{error}
+ {/if} + + {#if step === 'login'} +
+

Authentication

+
+ + +
+
+ {/if} + + {#if step === 'otp'} +
+

Verify OTP

+
+ + +
+
+ {/if} + + {#if step === 'profile'} +
+

Complete Profile

+
+ + + + +
+
+ {/if} + + {#if step === 'dashboard'} +
+

Create Project

+
+ + + +
+
+ +
+

Onboarding Tests

+
+ + +
+
+ +
+

Create User (No Auth Required)

+
+ + + + +
+
+ +
+

Update User (Auth Required)

+
+ + + + + + + + + + +
+
+ +
+

Update Project (Auth Required)

+

Note: You can only request edits for projects that have at least one submission.

+
+ + + + + + + +
+
+ +
+

Your Projects

+ {#if projects.length === 0} +

No projects yet. Create one above!

+ {:else} + {#each projects as project} +
+

{project.projectTitle}

+

Type: {project.projectType}

+

Created: {new Date(project.createdAt).toLocaleDateString()}

+

Submissions: {project.submissions?.length || 0}

+ {#if project.isLocked} +

Status: πŸ”’ Locked

+ {:else} +

Status: πŸ”“ Unlocked

+ {/if} + {#if (project.submissions?.length || 0) === 0} +

Edit Requests: ⚠️ Submit first to enable edits

+ {:else} +

Edit Requests: βœ… Available

+ {/if} +
+ {/each} + {/if} +
+ +
+

Create Submission

+

Note: Submission data will be automatically copied from your project. Make sure your project is complete first.

+
+ + +
+
+ +
+

My Edit Requests

+ + + {#if editRequests.length > 0} +
+ {#each editRequests as request} +
+

Edit Request #{request.requestId}

+

Type: {request.requestType}

+

Project: {request.project?.projectTitle}

+

Status: + {#if request.status === 'approved'} + βœ… Approved + {:else if request.status === 'rejected'} + ❌ Rejected + {:else} + ⏳ Pending + {/if} +

+ {#if request.reason} +

Reason: {request.reason}

+ {/if} +

Created: {new Date(request.createdAt).toLocaleDateString()}

+
+ {/each} +
+ {:else} +

No edit requests yet.

+ {/if} +
+ + {#if selectedProjectId} +
+

Submissions for Selected Project

+ {#if submissions.length === 0} +

No submissions yet.

+ {:else} + {#each submissions as submission} +
+

Submission #{submission.submissionId}

+

Status: + {#if submission.approvalStatus === 'approved'} + βœ… Approved + {:else if submission.approvalStatus === 'rejected'} + ❌ Rejected + {:else} + ⏳ Pending + {/if} +

+ {#if submission.approvedHours} +

Approved Hours: {submission.approvedHours}

+ {/if} + {#if submission.playableUrl} +

Playable URL: {submission.playableUrl}

+ {/if} + {#if submission.screenshotUrl} +

Screenshot URL: {submission.screenshotUrl}

+ {/if} + {#if submission.description} +

Description: {submission.description}

+ {/if} + {#if submission.repoUrl} +

Repository: {submission.repoUrl}

+ {/if} +

Created: {new Date(submission.createdAt).toLocaleDateString()}

+
+ {/each} + {/if} +
+ + {/if} + {/if} +
+ + \ No newline at end of file diff --git a/lark-ui/src/routes/faq/+page.svelte b/lark-ui/src/routes/faq/+page.svelte index 031b3bb..8d7f23a 100644 --- a/lark-ui/src/routes/faq/+page.svelte +++ b/lark-ui/src/routes/faq/+page.svelte @@ -20,12 +20,6 @@ diff --git a/lark-ui/src/routes/login/+page.server.ts b/lark-ui/src/routes/login/+page.server.ts new file mode 100644 index 0000000..0c3fb3b --- /dev/null +++ b/lark-ui/src/routes/login/+page.server.ts @@ -0,0 +1,53 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { Actions } from './$types'; +import { env } from '$env/dynamic/public'; + +const apiUrl = env.PUBLIC_API_URL || ""; + +async function verifyOtp(email: string, otp: string, fetchFn: typeof fetch) { + const fullUrl = apiUrl ? `${apiUrl}/api/user/auth/verify-otp` : 'http://localhost:3000/api/user/auth/verify-otp'; + return await fetchFn(fullUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ email, otp }) + }); +} + +export const actions = { + verify_otp: async ({ cookies, request, url, fetch: fetchFn }) => { + console.log(env); + + const data = await request.formData(); + + const email = data.get('email'); + const otp = data.get('otp'); + + console.log('email', email); + console.log('otp', otp); + + const response = await verifyOtp(email as string, otp as string, fetchFn) + + if (!response || !response.ok) { + return fail(500, { message: 'Failed to verify OTP', email: email as string }) + } + + const responseData = await response.json(); + + if (responseData.sessionId) { + cookies.set('sessionId', responseData.sessionId, { path: '/' }); + cookies.set('email', email as string, { path: '/', expires: new Date(Date.now() + 600000) }); // 10 min + + if (responseData.isNewUser) { + return redirect(302, '/app/onboarding'); + + // --> step 4 + } else { + return redirect(302, '/app/home'); + } + + } else { + return fail(500, { message: 'Failed to verify OTP', email: email as string }) + } + } +} satisfies Actions; diff --git a/lark-ui/src/routes/login/+page.svelte b/lark-ui/src/routes/login/+page.svelte new file mode 100644 index 0000000..35cfa8f --- /dev/null +++ b/lark-ui/src/routes/login/+page.svelte @@ -0,0 +1,548 @@ + + + + + + + + + +
+ + + + + + + +
diff --git a/lark-ui/src/routes/rsvp/+page.svelte b/lark-ui/src/routes/rsvp/+page.svelte index c318386..2cece3b 100644 --- a/lark-ui/src/routes/rsvp/+page.svelte +++ b/lark-ui/src/routes/rsvp/+page.svelte @@ -68,7 +68,6 @@ async function handleSubmit(event: Event) { event.preventDefault(); - if ( !email.trim() || !firstName.trim() || @@ -153,25 +152,6 @@ rel="stylesheet" />
diff --git a/lark-ui/static/boardingpass.svg b/lark-ui/static/boardingpass.svg new file mode 100644 index 0000000..b5331c8 --- /dev/null +++ b/lark-ui/static/boardingpass.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87162cce1c19dff5f60e87e8bdd8fc45e1d447602e091d32c00d6c2cf3b09f37 +size 461 diff --git a/lark-ui/static/butler/1.png b/lark-ui/static/butler/1.png new file mode 100644 index 0000000..ba0e66d --- /dev/null +++ b/lark-ui/static/butler/1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:132b3ef4d6a5937182dcd21baca8c2cdf03025ef35f972abeeebc2ca3ef0c171 +size 536416 diff --git a/lark-ui/static/butler/2.png b/lark-ui/static/butler/2.png new file mode 100644 index 0000000..e4225d6 --- /dev/null +++ b/lark-ui/static/butler/2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:887d48dc2cbda3c35082655c081aa92dc10813590450ff0bda3b6520c5bb0a73 +size 506122 diff --git a/lark-ui/static/butler/3.png b/lark-ui/static/butler/3.png new file mode 100644 index 0000000..547f4e8 --- /dev/null +++ b/lark-ui/static/butler/3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1709a92a72ab5db750a1244b0e52972dd74cd080fb5d81484aa69a314f89c83f +size 524842 diff --git a/lark-ui/static/butler/orig-2.png b/lark-ui/static/butler/orig-2.png new file mode 100644 index 0000000..3df7aa5 --- /dev/null +++ b/lark-ui/static/butler/orig-2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db0dd3cab91ffebe352272109f1cb1e1218d75f66ba0c31a2222bee47d4d5f19 +size 473550 diff --git a/lark-ui/static/card-blah.svg b/lark-ui/static/card-blah.svg new file mode 100644 index 0000000..bce401e --- /dev/null +++ b/lark-ui/static/card-blah.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4e075a3d3dd6621701fac95bd0084b4ce8abac0b5ea29637f1e338d486bc457 +size 2828 diff --git a/lark-ui/static/card-image.png b/lark-ui/static/card-image.png new file mode 100644 index 0000000..bbc48e0 --- /dev/null +++ b/lark-ui/static/card-image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c26901af4a0838e94855ee1581fa0d1f35f7e55636e2461eb0c47f22b227d58 +size 574659 diff --git a/lark-ui/static/card-quill.svg b/lark-ui/static/card-quill.svg new file mode 100644 index 0000000..d4c19f9 --- /dev/null +++ b/lark-ui/static/card-quill.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e8c40365dfe5cf502a7a33043cd0d8a44dca74a778e372328dbdfe4b3981215 +size 566 diff --git a/lark-ui/static/card-star-left.svg b/lark-ui/static/card-star-left.svg new file mode 100644 index 0000000..c9d01db --- /dev/null +++ b/lark-ui/static/card-star-left.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5200fdbbfec0658fcb96063f69d00197dc540eb424e18038eb3253b7580de15a +size 1586 diff --git a/lark-ui/static/card-star-right.svg b/lark-ui/static/card-star-right.svg new file mode 100644 index 0000000..e4f54c4 --- /dev/null +++ b/lark-ui/static/card-star-right.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e56934c27c82e39947d0ef669cf9e6b2c22a782a5faff8cff97c92db5c57c22 +size 1368 diff --git a/lark-ui/static/cardboard-texture-1.png b/lark-ui/static/cardboard-texture-1.png new file mode 100644 index 0000000..afb6f17 --- /dev/null +++ b/lark-ui/static/cardboard-texture-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5363473f5191c8c8d5bc637aa085a20b92c8635780139269d7ef2f2edb92991 +size 2674 diff --git a/lark-ui/static/cardboard-texture-2.png b/lark-ui/static/cardboard-texture-2.png new file mode 100644 index 0000000..22fd435 --- /dev/null +++ b/lark-ui/static/cardboard-texture-2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e1770daabe50055a9389c866fddc401dc24970575e03af1b9b6e5b0f9791153 +size 11206508 diff --git a/lark-ui/static/cards/cli.svg b/lark-ui/static/cards/cli.svg new file mode 100644 index 0000000..c6e8565 --- /dev/null +++ b/lark-ui/static/cards/cli.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a62019bec8a89e786c96c2f476dd5d58e85baa7e437a0efbbf46b8d8169e1e6a +size 785728 diff --git a/lark-ui/static/cards/desktop_app.svg b/lark-ui/static/cards/desktop_app.svg new file mode 100644 index 0000000..63d1027 --- /dev/null +++ b/lark-ui/static/cards/desktop_app.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6ead67810caa6e48401a2386b2ea9998618443ea1c7b739f652214e0ab7d601 +size 786486 diff --git a/lark-ui/static/cards/game.svg b/lark-ui/static/cards/game.svg new file mode 100644 index 0000000..9d8905f --- /dev/null +++ b/lark-ui/static/cards/game.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95cc6677ad8f7ed0aa7816ee6466b2aefed85bca04cad888c4253ceb7686097d +size 771553 diff --git a/lark-ui/static/cards/mobile_app.svg b/lark-ui/static/cards/mobile_app.svg new file mode 100644 index 0000000..a774d3b --- /dev/null +++ b/lark-ui/static/cards/mobile_app.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f5d6b3c5965b08c713807312743e9cc245a7ec7f08ab590875ee5881594e4b0 +size 785170 diff --git a/lark-ui/static/cards/more_projects.svg b/lark-ui/static/cards/more_projects.svg new file mode 100644 index 0000000..3fed222 --- /dev/null +++ b/lark-ui/static/cards/more_projects.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3779155b75b946f45b9691219600bff2a3bf97903cb364ddbc047efc2e0ad11c +size 9344 diff --git a/lark-ui/static/cards/personal_website.svg b/lark-ui/static/cards/personal_website.svg new file mode 100644 index 0000000..7324fe1 --- /dev/null +++ b/lark-ui/static/cards/personal_website.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35f40d0ab6713e07cbe66dedf203bdfc0d5176f2b1f9b428364fc0db92f2436c +size 798680 diff --git a/lark-ui/static/cards/platformer_game.svg b/lark-ui/static/cards/platformer_game.svg new file mode 100644 index 0000000..8cacde2 --- /dev/null +++ b/lark-ui/static/cards/platformer_game.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a07367b20b13af8c3047a81ae455aad81ce5b0349575a02751c552ff234ddeae +size 798501 diff --git a/lark-ui/static/cards/website.svg b/lark-ui/static/cards/website.svg new file mode 100644 index 0000000..d6a8409 --- /dev/null +++ b/lark-ui/static/cards/website.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60f90beb1135f673ad05e22edf8551ccf2da7cc939b1db42e09ba6b98706ab1d +size 772747 diff --git a/lark-ui/static/cards/wildcard.svg b/lark-ui/static/cards/wildcard.svg new file mode 100644 index 0000000..7c94d38 --- /dev/null +++ b/lark-ui/static/cards/wildcard.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f463ad781aadcf934d0a56a3bb54bf2825976c4b9d48cefe475f5596fcd0c4dd +size 208383 diff --git a/lark-ui/static/crow-small.svg b/lark-ui/static/crow-small.svg new file mode 100644 index 0000000..7fdeb75 --- /dev/null +++ b/lark-ui/static/crow-small.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d71a1c5e3848d0b71f8aaa136caf52acbb423fb82675da8de61c32a63c31c9e +size 30948 diff --git a/lark-ui/static/crow.png b/lark-ui/static/crow.png index 77bf9d9..fe5fd28 100644 --- a/lark-ui/static/crow.png +++ b/lark-ui/static/crow.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd44c42c03efe2c80d8c67011f0b3f04a916cdd77395cdaf8bf188bc7ad7da1b -size 66468 +oid sha256:35b66984dcdb86b47b6bfee29f2f6173adcf63527e85ca312cfb1931b17b2cb6 +size 137232 diff --git a/lark-ui/static/form-texture.png b/lark-ui/static/form-texture.png new file mode 100644 index 0000000..870cbbf --- /dev/null +++ b/lark-ui/static/form-texture.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b77f306aa775c384b5b134e3cf5738762403828b27cd07bb0ce5e892254b08b +size 3438003 diff --git a/lark-ui/static/grunge.jpg b/lark-ui/static/grunge.jpg new file mode 100644 index 0000000..364cc6d Binary files /dev/null and b/lark-ui/static/grunge.jpg differ diff --git a/lark-ui/static/hand.png b/lark-ui/static/hand.png new file mode 100644 index 0000000..bdb623a --- /dev/null +++ b/lark-ui/static/hand.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0651c6c64604a41d6812b52118d8574c6a1d3a2e6bceef77567d6bb95ba59d7a +size 17011 diff --git a/lark-ui/static/handdrawn_text/required.png b/lark-ui/static/handdrawn_text/required.png new file mode 100644 index 0000000..e35cb32 --- /dev/null +++ b/lark-ui/static/handdrawn_text/required.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11e3abf106cebc002f47055ed9c8c94966fc870d8e9e5eb7e1bd11c84141a78b +size 14926 diff --git a/lark-ui/static/handdrawn_text/your_project.png b/lark-ui/static/handdrawn_text/your_project.png new file mode 100644 index 0000000..704694c --- /dev/null +++ b/lark-ui/static/handdrawn_text/your_project.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f008ba5d7a69873c6e9bc9437722f4cb0001cd1774aad903e3214513be6d3a78 +size 24747 diff --git a/lark-ui/static/icons/add.svg b/lark-ui/static/icons/add.svg new file mode 100644 index 0000000..67fee67 --- /dev/null +++ b/lark-ui/static/icons/add.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f13094af094afd21dc1db0674ff847cb0301ae3c29006851607368b350cfff47 +size 293 diff --git a/lark-ui/static/icons/bell.svg b/lark-ui/static/icons/bell.svg new file mode 100644 index 0000000..b851bae --- /dev/null +++ b/lark-ui/static/icons/bell.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2abbdbe051254df311695cd29d3bcdb43fe2f6f7451e6c1fa90f4800d864eb46 +size 970 diff --git a/lark-ui/static/icons/edit.svg b/lark-ui/static/icons/edit.svg new file mode 100644 index 0000000..04e9c89 --- /dev/null +++ b/lark-ui/static/icons/edit.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6990ec6bacca033ebecfaa0facd78f130db9af0abc0108d30144f180132783a1 +size 625 diff --git a/lark-ui/static/icons/link.svg b/lark-ui/static/icons/link.svg new file mode 100644 index 0000000..2b871d9 --- /dev/null +++ b/lark-ui/static/icons/link.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:965875686566166116e01b919e683d32d7e521c89c065a9f2acc5d441462adc3 +size 1029 diff --git a/lark-ui/static/icons/lock.svg b/lark-ui/static/icons/lock.svg new file mode 100644 index 0000000..5269b6a --- /dev/null +++ b/lark-ui/static/icons/lock.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbd315413c33acb31f4d75358eb05a865218438eec18459d95e030ebd0a60cc7 +size 4326 diff --git a/lark-ui/static/icons/quill.svg b/lark-ui/static/icons/quill.svg new file mode 100644 index 0000000..67d5c1e --- /dev/null +++ b/lark-ui/static/icons/quill.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:338c5f12e3511089c23dc2da7672ec7dd768cd4b8af51164afe1fec6adc2455d +size 4189 diff --git a/lark-ui/static/icons/remove.svg b/lark-ui/static/icons/remove.svg new file mode 100644 index 0000000..f2d9919 --- /dev/null +++ b/lark-ui/static/icons/remove.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcb2ee62ec236507117d32abbad07b3be4c8ef937437af5ae91f52de43b4f1d6 +size 290 diff --git a/lark-ui/static/lock-circle.svg b/lark-ui/static/lock-circle.svg new file mode 100644 index 0000000..37daba7 --- /dev/null +++ b/lark-ui/static/lock-circle.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0bf1d3752c9a6effbf8c806d2b2b7bc04708e330fe743dffda41fae1a933c8c8 +size 255 diff --git a/lark-ui/static/notification-icon.svg b/lark-ui/static/notification-icon.svg new file mode 100644 index 0000000..8de9f1f --- /dev/null +++ b/lark-ui/static/notification-icon.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a6cc1ac7ea47f1ed4b8468f72c12410f124b6ea835a948891ad7aac79ed7cf6 +size 1425 diff --git a/lark-ui/static/platformer.svg b/lark-ui/static/platformer.svg deleted file mode 100644 index 3fdfa6d..0000000 --- a/lark-ui/static/platformer.svg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:018566d6df822eb40f439b0da21844236af1b0370a3f10abe15302a09a453efb -size 784133 diff --git a/lark-ui/static/plus-icon.svg b/lark-ui/static/plus-icon.svg new file mode 100644 index 0000000..b753c1b --- /dev/null +++ b/lark-ui/static/plus-icon.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b575b4a68be4509ecb30d9387748a937c4697629cf97c2377d49a694654e420b +size 333 diff --git a/lark-ui/static/shapes/shape-bg-1.svg b/lark-ui/static/shapes/shape-bg-1.svg new file mode 100644 index 0000000..e083278 --- /dev/null +++ b/lark-ui/static/shapes/shape-bg-1.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8651745f1234f743210da2f4d218bac714c201fb64d99b1def1362e8d664c01f +size 161 diff --git a/lark-ui/static/shapes/shape-button-1.svg b/lark-ui/static/shapes/shape-button-1.svg new file mode 100644 index 0000000..c33f4cd --- /dev/null +++ b/lark-ui/static/shapes/shape-button-1.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54a158ffc13f36240d5236c2d37167501121c2c13b7402208d95f193cbd2cc61 +size 166 diff --git a/lark-ui/static/shapes/shape-label-1.svg b/lark-ui/static/shapes/shape-label-1.svg new file mode 100644 index 0000000..6019bfe --- /dev/null +++ b/lark-ui/static/shapes/shape-label-1.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b51f43493551e6dce20b64c848d91f805fc3ff25758e71392297ad56d6fc6900 +size 235 diff --git a/lark-ui/static/slider-track.svg b/lark-ui/static/slider-track.svg new file mode 100644 index 0000000..cec0285 --- /dev/null +++ b/lark-ui/static/slider-track.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b01f75d2d4b7ee00928451b351bbb28878244dc989ac76eb484d032e60862b0 +size 463 diff --git a/lark-ui/static/sticker.png b/lark-ui/static/sticker.png new file mode 100644 index 0000000..5ce4f93 --- /dev/null +++ b/lark-ui/static/sticker.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:042aa4b35fc910ae1bd85174ea8e202a755560df5a122e68e17737a6d4885c6a +size 182609 diff --git a/lark-ui/static/tooltip-arrow.svg b/lark-ui/static/tooltip-arrow.svg new file mode 100644 index 0000000..0fc2a7b --- /dev/null +++ b/lark-ui/static/tooltip-arrow.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09ed7c1993f6fc05d6e3e6c28f148e48c8da686bbd577b1efeb4a4508b9ceae5 +size 444 diff --git a/lark-ui/static/tooltip-bg.svg b/lark-ui/static/tooltip-bg.svg new file mode 100644 index 0000000..382c967 --- /dev/null +++ b/lark-ui/static/tooltip-bg.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37363b95ef4811c51100026115c84c3d6b6cf03c01c2678e51cbe966c4d2da32 +size 484 diff --git a/lark-ui/static/wildcard.svg b/lark-ui/static/wildcard.svg deleted file mode 100644 index b702c3c..0000000 --- a/lark-ui/static/wildcard.svg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0b0c2e9028245628a60da8b9a3c28772f6959695f6605c1071b34cc9261b1f94 -size 207984 diff --git a/lark-ui/svelte.config.js b/lark-ui/svelte.config.js index e0a641e..39036ea 100644 --- a/lark-ui/svelte.config.js +++ b/lark-ui/svelte.config.js @@ -11,7 +11,9 @@ const config = { // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. // If your environment is not supported, or you settled on a specific environment, switch out the adapter. // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter() + adapter: adapter({ + precompress: true + }) } }; diff --git a/owl-api/HACKATIME_API_DOCUMENTATION.md b/owl-api/HACKATIME_API_DOCUMENTATION.md new file mode 100644 index 0000000..f88cd70 --- /dev/null +++ b/owl-api/HACKATIME_API_DOCUMENTATION.md @@ -0,0 +1,397 @@ +# Hackatime API Endpoints + +## Overview +These endpoints allow authenticated users to check their Hackatime account status and retrieve their coding statistics and project information. + +## Endpoints + +### 1. Check Hackatime Account Status + +``` +GET /api/user/hackatime-account +``` + +Check if the authenticated user has a Hackatime account linked. + +**Response (200 OK):** +```json +{ + "email": "user@example.com", + "hasHackatimeAccount": true, + "hackatimeAccountId": "100" +} +``` + +When no account is linked: +```json +{ + "email": "user@example.com", + "hasHackatimeAccount": false, + "hackatimeAccountId": null +} +``` + +--- + +### 2. Get Hackatime Projects + +``` +GET /api/user/hackatime-projects +``` + +## Authentication +This endpoint requires authentication via the `AuthGuard`. Users must be logged in and have a valid session cookie. + +**Required Headers:** +``` +Cookie: sessionId= +``` + +## Response Format + +### Success Response (200 OK) +Returns a JSON object containing the user's Hackatime information: + +```json +{ + "user_id": 100, + "username": "techpixel", + "projects": [ + { + "name": "project-name", + "total_heartbeats": 31773, + "total_duration": 119828, + "first_heartbeat": 1738440566.3219998, + "last_heartbeat": 1755700946.734, + "languages": ["TypeScript", "JavaScript", "CSS", "HTML"], + "repo": "https://github.com/username/project-name", + "repo_mapping_id": 1847 + } + ], + "total_projects": 63 +} +``` + +### Error Responses + +#### 401 Unauthorized +User is not authenticated or session is invalid. + +```json +{ + "statusCode": 401, + "message": "Session required" +} +``` + +#### 404 Not Found +User has no Hackatime account linked. + +```json +{ + "statusCode": 404, + "message": "No Hackatime account linked to this user" +} +``` + +Or user not found: + +```json +{ + "statusCode": 404, + "message": "User not found" +} +``` + +#### 500 Internal Server Error +Error fetching data from Hackatime API. + +```json +{ + "statusCode": 500, + "message": "Failed to fetch hackatime projects" +} +``` + +## Field Descriptions + +- `user_id`: The Hackatime user ID +- `username`: The user's Hackatime username +- `projects`: Array of project objects + - `name`: Project name + - `total_heartbeats`: Total number of coding heartbeats recorded + - `total_duration`: Total coding duration in seconds + - `first_heartbeat`: Unix timestamp of first heartbeat + - `last_heartbeat`: Unix timestamp of last heartbeat + - `languages`: Array of programming languages used + - `repo`: GitHub repository URL (may be null) + - `repo_mapping_id`: Hackatime repository mapping ID (may be null) +- `total_projects`: Total number of projects for the user + +## Example Usage + +### Check Hackatime Account Status + +```bash +curl -X GET "https://api.example.com/api/user/hackatime-account" \ + -H "Cookie: sessionId=your-session-id-here" +``` + +### Get Hackatime Projects + +```bash +curl -X GET "https://api.example.com/api/user/hackatime-projects" \ + -H "Cookie: sessionId=your-session-id-here" +``` + +### Using JavaScript (Fetch API) + +Check account status: +```javascript +fetch('https://api.example.com/api/user/hackatime-account', { + method: 'GET', + credentials: 'include', +}) + .then(response => response.json()) + .then(data => console.log('Hackatime status:', data)) + .catch(error => console.error('Error:', error)); +``` + +Get projects: +```javascript +fetch('https://api.example.com/api/user/hackatime-projects', { + method: 'GET', + credentials: 'include', +}) + .then(response => response.json()) + .then(data => console.log('Projects:', data)) + .catch(error => console.error('Error:', error)); +``` + +### Using JavaScript (Axios) + +Check account status: +```javascript +axios.get('https://api.example.com/api/user/hackatime-account', { + withCredentials: true +}) + .then(response => console.log('Hackatime status:', response.data)) + .catch(error => console.error('Error:', error)); +``` + +Get projects: +```javascript +axios.get('https://api.example.com/api/user/hackatime-projects', { + withCredentials: true +}) + .then(response => console.log('Projects:', response.data)) + .catch(error => console.error('Error:', error)); +``` + +## Account Linking Methods + +### Automatic Linking (During Signup) + +Users are automatically linked to their Hackatime account during signup based on their email address. The system: + +1. Checks if a Hackatime account exists with the same email +2. If found, stores the Hackatime user ID in the `hackatime_account` field +3. If not found, leaves the field empty (null) + +**Note:** This only works if `HACKATIME_API_KEY` is configured in your `.env` file. + +### Manual Linking (OTP Verification) + +Users can manually link their Hackatime account using the OTP verification endpoints: + +1. User provides their Hackatime email address +2. System sends a 6-digit OTP to that email +3. User enters the OTP code +4. System verifies email ownership and links the account + +This method ensures that users can only link accounts they actually control. + +## Troubleshooting + +### "No Hackatime account linked" Error + +If you receive this error, it means: +- No Hackatime account was found during initial signup, OR +- The account was never manually linked via OTP + +**Solution:** Use the manual linking flow (OTP verification) to link your account. + +### "HACKATIME_API_KEY not configured" Warning + +This appears in the logs when the API key is not set in your `.env` file. + +**Solution:** +1. Add `HACKATIME_API_KEY` to your root `.env` file +2. Ensure the value is your actual bearer token (starts with `hka_...`) +3. Restart the API service + +### "No Hackatime account found with this email" Error + +This occurs when: +- The email doesn't exist in the Hackatime database, OR +- The `HACKATIME_API_KEY` is not configured correctly + +**Solution:** Verify the email exists in Hackatime and that the API key has proper permissions. + +## Configuration + +The endpoints require the following environment variables to be set in your root `.env` file: + +```bash +# Hackatime Configuration +HACKATIME_ADMIN_API_URL=https://hackatime.hackclub.com/api/admin/v1 +HACKATIME_API_KEY=your_hackatime_api_key_here +``` + +**Important Notes:** +- Add these variables to the root `.env` file (not `owl-api/.env`) +- The `HACKATIME_ADMIN_API_URL` should be the base URL, not the API key +- The `HACKATIME_API_KEY` should be your actual bearer token +- After updating `.env`, restart the API service to load the new variables + +--- + +### 3. Send Hackatime Link OTP + +``` +POST /api/user/auth/hackatime-link/send-otp +``` + +Send a verification code to an email address to link that Hackatime account to your user account. + +**Request Body:** +```json +{ + "email": "your-hackatime-email@example.com" +} +``` + +**Response (200 OK):** +```json +{ + "message": "Verification code sent to your email" +} +``` + +**Error Responses:** + +400 Bad Request - Invalid email format: +```json +{ + "statusCode": 400, + "message": "Invalid email format" +} +``` + +--- + +### 4. Verify Hackatime Link OTP + +``` +POST /api/user/auth/hackatime-link/verify-otp +``` + +Verify the OTP code and link your Hackatime account. + +**Request Body:** +```json +{ + "otp": "123456" +} +``` + +**Response (200 OK):** +```json +{ + "message": "Hackatime account linked successfully", + "hackatimeAccountId": 100 +} +``` + +**Error Responses:** + +400 Bad Request - No account found: +```json +{ + "statusCode": 400, + "message": "No Hackatime account found with this email" +} +``` + +401 Unauthorized - Invalid or expired OTP: +```json +{ + "statusCode": 401, + "message": "Invalid or expired verification code" +} +``` + +## Linking Your Hackatime Account + +To link your Hackatime account: + +1. Call the send OTP endpoint with your Hackatime email address +2. Check your email for the verification code +3. Call the verify OTP endpoint with the code +4. Your Hackatime account will be linked automatically + +Example workflow: +```javascript +// Step 1: Send OTP +await axios.post('/api/user/auth/hackatime-link/send-otp', { + email: 'your-hackatime-email@example.com' +}); + +// Step 2: Verify OTP (after receiving email) +await axios.post('/api/user/auth/hackatime-link/verify-otp', { + otp: '123456' +}); +``` + +## Data Format Notes + +### Timestamps +- All timestamps are in Unix format (seconds since epoch) +- Example: `1738440566.3219998` = December 31, 2024 + +### Duration +- Duration is measured in seconds +- Example: `119828` = ~33.3 hours + +### Languages +- Languages array may include empty strings for projects without language detection +- Empty strings represent unknown/mixed language projects + +### Repository Information +- Repository information is optional and may be null for local-only projects +- `repo`: GitHub repository URL (if linked) +- `repo_mapping_id`: Hackatime's internal repository mapping ID (if available) + +## Database Schema + +The Hackatime integration adds the following fields to the `users` table: + +- `hackatime_account` (TEXT, nullable): Stores the Hackatime user ID if account is linked + +## Email Templates + +### Hackatime Link OTP Email +When users request to link their Hackatime account, they receive an email with: +- Subject: "Link Your Hackatime Account" +- Content: 6-digit verification code +- Expiration: 10 minutes + +**Metadata Type:** `hackatime-link-otp` + +## Security Considerations + +- OTP codes expire after 10 minutes +- OTP codes can only be used once +- Users must be authenticated to send/verify OTP codes +- Email verification ensures users can only link accounts they own +- The `hackatimeAccount` field is only populated after successful OTP verification diff --git a/owl-api/package.json b/owl-api/package.json index 04f09f8..3d3d791 100644 --- a/owl-api/package.json +++ b/owl-api/package.json @@ -34,6 +34,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.7", + "dd-trace": "^5.73.0", "dotenv": "^17.2.3", "express": "^4.18.2", "ioredis": "^5.8.1", diff --git a/owl-api/prisma/migrations/20250123120000_add_user_project_submission_models/migration.sql b/owl-api/prisma/migrations/20250123120000_add_user_project_submission_models/migration.sql index a268e6d..4da2d0c 100644 --- a/owl-api/prisma/migrations/20250123120000_add_user_project_submission_models/migration.sql +++ b/owl-api/prisma/migrations/20250123120000_add_user_project_submission_models/migration.sql @@ -1,3 +1,15 @@ +-- CreateEnum +CREATE TYPE "ProjectType" AS ENUM ('personal_website', 'platformer_game', 'wildcard'); + +-- CreateEnum +CREATE TYPE "ApprovalStatus" AS ENUM ('pending', 'approved', 'rejected'); + +-- CreateEnum +CREATE TYPE "EditRequestType" AS ENUM ('project_update', 'user_update'); + +-- CreateEnum +CREATE TYPE "RequestStatus" AS ENUM ('pending', 'approved', 'rejected'); + -- CreateTable CREATE TABLE "users" ( "user_id" SERIAL NOT NULL, @@ -15,24 +27,13 @@ CREATE TABLE "users" ( "country" VARCHAR(255), "zip_code" VARCHAR(255), "airtable_rec_id" VARCHAR(255), + "hackatime_account" TEXT, "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP(3) NOT NULL, CONSTRAINT "users_pkey" PRIMARY KEY ("user_id") ); --- CreateEnum -CREATE TYPE "ProjectType" AS ENUM ('personal_website', 'platformer_game', 'wildcard'); - --- CreateEnum -CREATE TYPE "ApprovalStatus" AS ENUM ('pending', 'approved', 'rejected'); - --- CreateEnum -CREATE TYPE "EditRequestType" AS ENUM ('project_update', 'user_update'); - --- CreateEnum -CREATE TYPE "RequestStatus" AS ENUM ('pending', 'approved', 'rejected'); - -- CreateTable CREATE TABLE "projects" ( "project_id" SERIAL NOT NULL, @@ -87,32 +88,6 @@ CREATE TABLE "user_sessions" ( CONSTRAINT "user_sessions_pkey" PRIMARY KEY ("id") ); --- CreateIndex -CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); - - --- CreateIndex -CREATE INDEX "idx_projects_user_id" ON "projects"("user_id"); - --- CreateIndex -CREATE INDEX "idx_submissions_project_id" ON "submissions"("project_id"); - --- CreateIndex -CREATE INDEX "idx_users_email" ON "users"("email"); - - --- CreateIndex -CREATE INDEX "user_sessions_user_id_idx" ON "user_sessions"("user_id"); - --- CreateIndex -CREATE INDEX "user_sessions_otp_code_idx" ON "user_sessions"("otp_code"); - --- AddForeignKey -ALTER TABLE "projects" ADD CONSTRAINT "projects_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "submissions" ADD CONSTRAINT "submissions_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("project_id") ON DELETE CASCADE ON UPDATE CASCADE; - -- CreateTable CREATE TABLE "edit_requests" ( "request_id" SERIAL NOT NULL, @@ -131,23 +106,109 @@ CREATE TABLE "edit_requests" ( CONSTRAINT "edit_requests_pkey" PRIMARY KEY ("request_id") ); --- CreateIndex -CREATE INDEX "edit_requests_user_id_idx" ON "edit_requests"("user_id"); +-- CreateTable +CREATE TABLE "admin_users" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, --- CreateIndex -CREATE INDEX "edit_requests_project_id_idx" ON "edit_requests"("project_id"); + CONSTRAINT "admin_users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "admin_sessions" ( + "id" TEXT NOT NULL, + "adminUserId" TEXT NOT NULL, + "otpCode" TEXT NOT NULL, + "otpExpiresAt" TIMESTAMP(3) NOT NULL, + "isVerified" BOOLEAN NOT NULL DEFAULT false, + "verifiedAt" TIMESTAMP(3), + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "admin_sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "email_jobs" ( + "id" TEXT NOT NULL, + "recipientEmail" TEXT NOT NULL, + "subject" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "scheduledFor" TIMESTAMP(3), + "sentAt" TIMESTAMP(3), + "failedAt" TIMESTAMP(3), + "errorMessage" TEXT, + "metadata" JSONB, + "lockedBy" TEXT, + "lockedAt" TIMESTAMP(3), + "attempts" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "email_jobs_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "sticker_tokens" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "token" TEXT NOT NULL, + "rsvpNumber" INTEGER NOT NULL, + "isUsed" BOOLEAN NOT NULL DEFAULT false, + "usedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "sticker_tokens_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "hackatime_link_otps" ( + "id" TEXT NOT NULL, + "user_id" INTEGER NOT NULL, + "email" TEXT NOT NULL, + "otp_code" TEXT NOT NULL, + "expires_at" TIMESTAMP(3) NOT NULL, + "is_used" BOOLEAN NOT NULL DEFAULT false, + "used_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "hackatime_link_otps_pkey" PRIMARY KEY ("id") +); -- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); +CREATE INDEX "idx_projects_user_id" ON "projects"("user_id"); +CREATE INDEX "idx_submissions_project_id" ON "submissions"("project_id"); +CREATE INDEX "idx_users_email" ON "users"("email"); +CREATE INDEX "user_sessions_user_id_idx" ON "user_sessions"("user_id"); +CREATE INDEX "user_sessions_otp_code_idx" ON "user_sessions"("otp_code"); +CREATE INDEX "edit_requests_user_id_idx" ON "edit_requests"("user_id"); +CREATE INDEX "edit_requests_project_id_idx" ON "edit_requests"("project_id"); CREATE INDEX "edit_requests_status_idx" ON "edit_requests"("status"); +CREATE UNIQUE INDEX "admin_users_email_key" ON "admin_users"("email"); +CREATE INDEX "admin_sessions_adminUserId_idx" ON "admin_sessions"("adminUserId"); +CREATE INDEX "admin_sessions_otpCode_idx" ON "admin_sessions"("otpCode"); +CREATE INDEX "email_jobs_status_idx" ON "email_jobs"("status"); +CREATE INDEX "email_jobs_recipientEmail_idx" ON "email_jobs"("recipientEmail"); +CREATE INDEX "email_jobs_createdAt_idx" ON "email_jobs"("createdAt"); +CREATE INDEX "email_jobs_scheduledFor_idx" ON "email_jobs"("scheduledFor"); +CREATE INDEX "email_jobs_lockedBy_idx" ON "email_jobs"("lockedBy"); +CREATE UNIQUE INDEX "sticker_tokens_token_key" ON "sticker_tokens"("token"); +CREATE INDEX "sticker_tokens_token_idx" ON "sticker_tokens"("token"); +CREATE INDEX "sticker_tokens_email_idx" ON "sticker_tokens"("email"); +CREATE INDEX "hackatime_link_otps_user_id_idx" ON "hackatime_link_otps"("user_id"); +CREATE INDEX "hackatime_link_otps_otp_code_idx" ON "hackatime_link_otps"("otp_code"); -- AddForeignKey +ALTER TABLE "projects" ADD CONSTRAINT "projects_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "submissions" ADD CONSTRAINT "submissions_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("project_id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "user_sessions" ADD CONSTRAINT "user_sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey ALTER TABLE "edit_requests" ADD CONSTRAINT "edit_requests_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey ALTER TABLE "edit_requests" ADD CONSTRAINT "edit_requests_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("project_id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey ALTER TABLE "edit_requests" ADD CONSTRAINT "edit_requests_reviewed_by_fkey" FOREIGN KEY ("reviewed_by") REFERENCES "users"("user_id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "admin_sessions" ADD CONSTRAINT "admin_sessions_adminUserId_fkey" FOREIGN KEY ("adminUserId") REFERENCES "admin_users"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "hackatime_link_otps" ADD CONSTRAINT "hackatime_link_otps_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; \ No newline at end of file diff --git a/owl-api/prisma/migrations/20250123120000_init_consolidated/migration.sql b/owl-api/prisma/migrations/20250123120000_init_consolidated/migration.sql new file mode 100644 index 0000000..22eb7f2 --- /dev/null +++ b/owl-api/prisma/migrations/20250123120000_init_consolidated/migration.sql @@ -0,0 +1,267 @@ +-- CreateEnum (only if they don't exist) +DO $$ BEGIN + CREATE TYPE "ProjectType" AS ENUM ('personal_website', 'platformer_game', 'wildcard'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE "ApprovalStatus" AS ENUM ('pending', 'approved', 'rejected'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE "EditRequestType" AS ENUM ('project_update', 'user_update'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE "RequestStatus" AS ENUM ('pending', 'approved', 'rejected'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- CreateTable +CREATE TABLE IF NOT EXISTS "users" ( + "user_id" SERIAL NOT NULL, + "email" VARCHAR(255) NOT NULL, + "first_name" VARCHAR(255) NOT NULL, + "last_name" VARCHAR(255) NOT NULL, + "birthday" DATE NOT NULL, + "role" VARCHAR(50) DEFAULT 'user', + "onboard_complete" BOOLEAN DEFAULT false, + "onboarded_at" TIMESTAMP(3), + "address_line_1" VARCHAR(255), + "address_line_2" VARCHAR(255), + "city" VARCHAR(255), + "state" VARCHAR(255), + "country" VARCHAR(255), + "zip_code" VARCHAR(255), + "airtable_rec_id" VARCHAR(255), + "hackatime_account" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("user_id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "projects" ( + "project_id" SERIAL NOT NULL, + "user_id" INTEGER NOT NULL, + "project_title" VARCHAR(30) NOT NULL, + "project_type" "ProjectType" NOT NULL, + "now_hackatime_hours" DOUBLE PRECISION, + "now_hackatime_projects" TEXT[] DEFAULT '{}', + "approved_hours" DOUBLE PRECISION, + "description" VARCHAR(500), + "screenshot_url" TEXT, + "playable_url" TEXT, + "repo_url" TEXT, + "hours_justification" VARCHAR(500), + "airtable_rec_id" VARCHAR(255), + "is_locked" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "projects_pkey" PRIMARY KEY ("project_id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "submissions" ( + "submission_id" SERIAL NOT NULL, + "project_id" INTEGER NOT NULL, + "playable_url" TEXT, + "screenshot_url" TEXT, + "description" VARCHAR(500), + "repo_url" TEXT, + "approved_hours" DOUBLE PRECISION, + "hours_justification" VARCHAR(500), + "approval_status" "ApprovalStatus" NOT NULL DEFAULT 'pending', + "reviewed_by" VARCHAR(255), + "reviewed_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "submissions_pkey" PRIMARY KEY ("submission_id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "user_sessions" ( + "id" TEXT NOT NULL, + "user_id" INTEGER NOT NULL, + "otp_code" TEXT NOT NULL, + "otp_expires_at" TIMESTAMP(3) NOT NULL, + "is_verified" BOOLEAN NOT NULL DEFAULT false, + "verified_at" TIMESTAMP(3), + "expires_at" TIMESTAMP(3) NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "user_sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "edit_requests" ( + "request_id" SERIAL NOT NULL, + "user_id" INTEGER NOT NULL, + "project_id" INTEGER NOT NULL, + "request_type" "EditRequestType" NOT NULL, + "current_data" JSONB NOT NULL, + "requested_data" JSONB NOT NULL, + "status" "RequestStatus" NOT NULL DEFAULT 'pending', + "reason" VARCHAR(500), + "reviewed_by" INTEGER, + "reviewed_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "edit_requests_pkey" PRIMARY KEY ("request_id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "admin_users" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "admin_users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "admin_sessions" ( + "id" TEXT NOT NULL, + "adminUserId" TEXT NOT NULL, + "otpCode" TEXT NOT NULL, + "otpExpiresAt" TIMESTAMP(3) NOT NULL, + "isVerified" BOOLEAN NOT NULL DEFAULT false, + "verifiedAt" TIMESTAMP(3), + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "admin_sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "email_jobs" ( + "id" TEXT NOT NULL, + "recipientEmail" TEXT NOT NULL, + "subject" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "scheduledFor" TIMESTAMP(3), + "sentAt" TIMESTAMP(3), + "failedAt" TIMESTAMP(3), + "errorMessage" TEXT, + "metadata" JSONB, + "lockedBy" TEXT, + "lockedAt" TIMESTAMP(3), + "attempts" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "email_jobs_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "sticker_tokens" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "token" TEXT NOT NULL, + "rsvpNumber" INTEGER NOT NULL, + "isUsed" BOOLEAN NOT NULL DEFAULT false, + "usedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "sticker_tokens_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "hackatime_link_otps" ( + "id" TEXT NOT NULL, + "user_id" INTEGER NOT NULL, + "email" TEXT NOT NULL, + "otp_code" TEXT NOT NULL, + "expires_at" TIMESTAMP(3) NOT NULL, + "is_used" BOOLEAN NOT NULL DEFAULT false, + "used_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "hackatime_link_otps_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX IF NOT EXISTS "users_email_key" ON "users"("email"); +CREATE INDEX IF NOT EXISTS "idx_projects_user_id" ON "projects"("user_id"); +CREATE INDEX IF NOT EXISTS "idx_submissions_project_id" ON "submissions"("project_id"); +CREATE INDEX IF NOT EXISTS "idx_users_email" ON "users"("email"); +CREATE INDEX IF NOT EXISTS "user_sessions_user_id_idx" ON "user_sessions"("user_id"); +CREATE INDEX IF NOT EXISTS "user_sessions_otp_code_idx" ON "user_sessions"("otp_code"); +CREATE INDEX IF NOT EXISTS "edit_requests_user_id_idx" ON "edit_requests"("user_id"); +CREATE INDEX IF NOT EXISTS "edit_requests_project_id_idx" ON "edit_requests"("project_id"); +CREATE INDEX IF NOT EXISTS "edit_requests_status_idx" ON "edit_requests"("status"); +CREATE UNIQUE INDEX IF NOT EXISTS "admin_users_email_key" ON "admin_users"("email"); +CREATE INDEX IF NOT EXISTS "admin_sessions_adminUserId_idx" ON "admin_sessions"("adminUserId"); +CREATE INDEX IF NOT EXISTS "admin_sessions_otpCode_idx" ON "admin_sessions"("otpCode"); +CREATE INDEX IF NOT EXISTS "email_jobs_status_idx" ON "email_jobs"("status"); +CREATE INDEX IF NOT EXISTS "email_jobs_recipientEmail_idx" ON "email_jobs"("recipientEmail"); +CREATE INDEX IF NOT EXISTS "email_jobs_createdAt_idx" ON "email_jobs"("createdAt"); +CREATE INDEX IF NOT EXISTS "email_jobs_scheduledFor_idx" ON "email_jobs"("scheduledFor"); +CREATE INDEX IF NOT EXISTS "email_jobs_lockedBy_idx" ON "email_jobs"("lockedBy"); +CREATE UNIQUE INDEX IF NOT EXISTS "sticker_tokens_token_key" ON "sticker_tokens"("token"); +CREATE INDEX IF NOT EXISTS "sticker_tokens_token_idx" ON "sticker_tokens"("token"); +CREATE INDEX IF NOT EXISTS "sticker_tokens_email_idx" ON "sticker_tokens"("email"); +CREATE INDEX IF NOT EXISTS "hackatime_link_otps_user_id_idx" ON "hackatime_link_otps"("user_id"); +CREATE INDEX IF NOT EXISTS "hackatime_link_otps_otp_code_idx" ON "hackatime_link_otps"("otp_code"); + +-- AddForeignKey (only if they don't exist) +DO $$ BEGIN + ALTER TABLE "projects" ADD CONSTRAINT "projects_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + ALTER TABLE "submissions" ADD CONSTRAINT "submissions_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("project_id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + ALTER TABLE "user_sessions" ADD CONSTRAINT "user_sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + ALTER TABLE "edit_requests" ADD CONSTRAINT "edit_requests_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + ALTER TABLE "edit_requests" ADD CONSTRAINT "edit_requests_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("project_id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + ALTER TABLE "edit_requests" ADD CONSTRAINT "edit_requests_reviewed_by_fkey" FOREIGN KEY ("reviewed_by") REFERENCES "users"("user_id") ON DELETE SET NULL ON UPDATE CASCADE; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + ALTER TABLE "admin_sessions" ADD CONSTRAINT "admin_sessions_adminUserId_fkey" FOREIGN KEY ("adminUserId") REFERENCES "admin_users"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + ALTER TABLE "hackatime_link_otps" ADD CONSTRAINT "hackatime_link_otps_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/owl-api/prisma/migrations/20251020000000_init/migration.sql b/owl-api/prisma/migrations/20251020000000_init/migration.sql deleted file mode 100644 index fc5fff6..0000000 --- a/owl-api/prisma/migrations/20251020000000_init/migration.sql +++ /dev/null @@ -1,53 +0,0 @@ -CREATE TABLE "admin_users" ( - "id" TEXT NOT NULL, - "email" TEXT NOT NULL, - "isActive" BOOLEAN NOT NULL DEFAULT true, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "admin_users_pkey" PRIMARY KEY ("id") -); - -CREATE TABLE "admin_sessions" ( - "id" TEXT NOT NULL, - "adminUserId" TEXT NOT NULL, - "otpCode" TEXT NOT NULL, - "otpExpiresAt" TIMESTAMP(3) NOT NULL, - "isVerified" BOOLEAN NOT NULL DEFAULT false, - "verifiedAt" TIMESTAMP(3), - "expiresAt" TIMESTAMP(3) NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "admin_sessions_pkey" PRIMARY KEY ("id") -); - -CREATE TABLE "email_jobs" ( - "id" TEXT NOT NULL, - "recipientEmail" TEXT NOT NULL, - "subject" TEXT NOT NULL, - "status" TEXT NOT NULL DEFAULT 'pending', - "scheduledFor" TIMESTAMP(3), - "sentAt" TIMESTAMP(3), - "failedAt" TIMESTAMP(3), - "errorMessage" TEXT, - "metadata" JSONB, - "lockedBy" TEXT, - "lockedAt" TIMESTAMP(3), - "attempts" INTEGER NOT NULL DEFAULT 0, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "email_jobs_pkey" PRIMARY KEY ("id") -); - -CREATE UNIQUE INDEX "admin_users_email_key" ON "admin_users"("email"); -CREATE INDEX "admin_sessions_adminUserId_idx" ON "admin_sessions"("adminUserId"); -CREATE INDEX "admin_sessions_otpCode_idx" ON "admin_sessions"("otpCode"); -CREATE INDEX "email_jobs_status_idx" ON "email_jobs"("status"); -CREATE INDEX "email_jobs_recipientEmail_idx" ON "email_jobs"("recipientEmail"); -CREATE INDEX "email_jobs_createdAt_idx" ON "email_jobs"("createdAt"); -CREATE INDEX "email_jobs_scheduledFor_idx" ON "email_jobs"("scheduledFor"); -CREATE INDEX "email_jobs_lockedBy_idx" ON "email_jobs"("lockedBy"); - -ALTER TABLE "admin_sessions" ADD CONSTRAINT "admin_sessions_adminUserId_fkey" FOREIGN KEY ("adminUserId") REFERENCES "admin_users"("id") ON DELETE CASCADE ON UPDATE CASCADE; - diff --git a/owl-api/prisma/migrations/20251021000000_add_sticker_tokens/migration.sql b/owl-api/prisma/migrations/20251021000000_add_sticker_tokens/migration.sql deleted file mode 100644 index 9f59b66..0000000 --- a/owl-api/prisma/migrations/20251021000000_add_sticker_tokens/migration.sql +++ /dev/null @@ -1,18 +0,0 @@ -CREATE TABLE "sticker_tokens" ( - "id" TEXT NOT NULL, - "email" TEXT NOT NULL, - "token" TEXT NOT NULL, - "rsvpNumber" INTEGER NOT NULL, - "isUsed" BOOLEAN NOT NULL DEFAULT false, - "usedAt" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "sticker_tokens_pkey" PRIMARY KEY ("id") -); - -CREATE UNIQUE INDEX "sticker_tokens_token_key" ON "sticker_tokens"("token"); -CREATE INDEX "sticker_tokens_token_idx" ON "sticker_tokens"("token"); -CREATE INDEX "sticker_tokens_email_idx" ON "sticker_tokens"("email"); - - diff --git a/owl-api/prisma/migrations/migration_lock.toml b/owl-api/prisma/migrations/migration_lock.toml index e353774..833d667 100644 --- a/owl-api/prisma/migrations/migration_lock.toml +++ b/owl-api/prisma/migrations/migration_lock.toml @@ -1,2 +1,3 @@ +# Please do not edit this file manually +# It should be added to your version-control system (i.e. Git) provider = "postgresql" - diff --git a/owl-api/prisma/schema.prisma b/owl-api/prisma/schema.prisma index f199f40..73773c2 100644 --- a/owl-api/prisma/schema.prisma +++ b/owl-api/prisma/schema.prisma @@ -24,12 +24,14 @@ model User { country String? zipCode String? @map("zip_code") airtableRecId String? @map("airtable_rec_id") + hackatimeAccount String? @map("hackatime_account") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") projects Project[] sessions UserSession[] editRequests EditRequest[] reviewedRequests EditRequest[] @relation("EditRequestReviewer") + hackatimeLinkOtps HackatimeLinkOtp[] @@map("users") } @@ -45,6 +47,7 @@ model Project { description String? @db.VarChar(500) hoursJustification String? @map("hours_justification") @db.VarChar(500) nowHackatimeHours Float? @map("now_hackatime_hours") + nowHackatimeProjects String[] @map("now_hackatime_projects") playableUrl String? @map("playable_url") projectTitle String @map("project_title") @db.VarChar(30) repoUrl String? @map("repo_url") @@ -160,6 +163,22 @@ model StickerToken { @@map("sticker_tokens") } +model HackatimeLinkOtp { + id String @id @default(uuid()) + userId Int @map("user_id") + email String + otpCode String @map("otp_code") + expiresAt DateTime @map("expires_at") + isUsed Boolean @default(false) @map("is_used") + usedAt DateTime? @map("used_at") + createdAt DateTime @default(now()) @map("created_at") + user User @relation(fields: [userId], references: [userId], onDelete: Cascade) + + @@index([userId]) + @@index([otpCode]) + @@map("hackatime_link_otps") +} + model EditRequest { requestId Int @id @default(autoincrement()) @map("request_id") userId Int @map("user_id") diff --git a/owl-api/src/admin/admin.controller.ts b/owl-api/src/admin/admin.controller.ts index 1f2ce5d..26f3923 100644 --- a/owl-api/src/admin/admin.controller.ts +++ b/owl-api/src/admin/admin.controller.ts @@ -46,4 +46,25 @@ export class AdminController { ) { return this.adminService.unlockProject(id, req.user.userId); } + + @Put('edit-requests/:id/approve') + @UseGuards(RolesGuard) + @Roles(Role.Admin) + async approveEditRequest( + @Param('id', ParseIntPipe) id: number, + @Req() req: Request, + ) { + return this.adminService.approveEditRequest(id, req.user.userId); + } + + @Put('edit-requests/:id/reject') + @UseGuards(RolesGuard) + @Roles(Role.Admin) + async rejectEditRequest( + @Param('id', ParseIntPipe) id: number, + @Body() body: { reason: string }, + @Req() req: Request, + ) { + return this.adminService.rejectEditRequest(id, body.reason, req.user.userId); + } } diff --git a/owl-api/src/admin/admin.service.ts b/owl-api/src/admin/admin.service.ts index 9468b8f..0bbf822 100644 --- a/owl-api/src/admin/admin.service.ts +++ b/owl-api/src/admin/admin.service.ts @@ -114,6 +114,7 @@ export class AdminService { repoUrl: submission.project.repoUrl, screenshotUrl: submission.project.screenshotUrl, nowHackatimeHours: submission.project.nowHackatimeHours, + nowHackatimeProjects: submission.project.nowHackatimeProjects, }, submission: { description: submission.description, @@ -186,6 +187,7 @@ export class AdminService { repoUrl: true, screenshotUrl: true, nowHackatimeHours: true, + nowHackatimeProjects: true, airtableRecId: true, isLocked: true, createdAt: true, @@ -236,4 +238,156 @@ export class AdminService { return updatedProject; } + + async approveEditRequest(requestId: number, adminUserId: number) { + const editRequest = await this.prisma.editRequest.findUnique({ + where: { requestId }, + include: { + project: true, + user: true, + }, + }); + + if (!editRequest) { + throw new NotFoundException('Edit request not found'); + } + + if (editRequest.status !== 'pending') { + throw new ForbiddenException('Edit request has already been processed'); + } + + // Calculate hackatime hours if hackatime projects are being updated + let calculatedHours = editRequest.project.nowHackatimeHours; + if ((editRequest.requestedData as any).nowHackatimeProjects) { + // For now, we'll set a placeholder value. In a real implementation, + // you would fetch hours from the hackatime API based on project names + calculatedHours = ((editRequest.requestedData as any).nowHackatimeProjects as string[]).length * 10; // Placeholder calculation + } + + // Update the project with the requested data + const updateData: any = {}; + if ((editRequest.requestedData as any).projectTitle !== undefined) { + updateData.projectTitle = (editRequest.requestedData as any).projectTitle; + } + if ((editRequest.requestedData as any).description !== undefined) { + updateData.description = (editRequest.requestedData as any).description; + } + if ((editRequest.requestedData as any).playableUrl !== undefined) { + updateData.playableUrl = (editRequest.requestedData as any).playableUrl; + } + if ((editRequest.requestedData as any).repoUrl !== undefined) { + updateData.repoUrl = (editRequest.requestedData as any).repoUrl; + } + if ((editRequest.requestedData as any).screenshotUrl !== undefined) { + updateData.screenshotUrl = (editRequest.requestedData as any).screenshotUrl; + } + if ((editRequest.requestedData as any).airtableRecId !== undefined) { + updateData.airtableRecId = (editRequest.requestedData as any).airtableRecId; + } + if ((editRequest.requestedData as any).nowHackatimeProjects !== undefined) { + updateData.nowHackatimeProjects = (editRequest.requestedData as any).nowHackatimeProjects; + updateData.nowHackatimeHours = calculatedHours; + } + + // Update the project + const updatedProject = await this.prisma.project.update({ + where: { projectId: editRequest.projectId }, + data: updateData, + }); + + // Update the edit request status + const updatedEditRequest = await this.prisma.editRequest.update({ + where: { requestId }, + data: { + status: 'approved', + reviewedBy: adminUserId, + reviewedAt: new Date(), + }, + include: { + user: { + select: { + userId: true, + firstName: true, + lastName: true, + email: true, + }, + }, + project: { + select: { + projectId: true, + projectTitle: true, + projectType: true, + }, + }, + reviewer: { + select: { + userId: true, + firstName: true, + lastName: true, + email: true, + }, + }, + }, + }); + + return { + message: 'Edit request approved successfully.', + editRequest: updatedEditRequest, + project: updatedProject, + }; + } + + async rejectEditRequest(requestId: number, reason: string, adminUserId: number) { + const editRequest = await this.prisma.editRequest.findUnique({ + where: { requestId }, + }); + + if (!editRequest) { + throw new NotFoundException('Edit request not found'); + } + + if (editRequest.status !== 'pending') { + throw new ForbiddenException('Edit request has already been processed'); + } + + const updatedEditRequest = await this.prisma.editRequest.update({ + where: { requestId }, + data: { + status: 'rejected', + reason, + reviewedBy: adminUserId, + reviewedAt: new Date(), + }, + include: { + user: { + select: { + userId: true, + firstName: true, + lastName: true, + email: true, + }, + }, + project: { + select: { + projectId: true, + projectTitle: true, + projectType: true, + }, + }, + reviewer: { + select: { + userId: true, + firstName: true, + lastName: true, + email: true, + }, + }, + }, + }); + + return { + message: 'Edit request rejected successfully.', + editRequest: updatedEditRequest, + }; + } } diff --git a/owl-api/src/airtable/airtable.service.ts b/owl-api/src/airtable/airtable.service.ts index 229ae54..07228e1 100644 --- a/owl-api/src/airtable/airtable.service.ts +++ b/owl-api/src/airtable/airtable.service.ts @@ -26,6 +26,7 @@ export class AirtableService { repoUrl: string; screenshotUrl: string; nowHackatimeHours: number; + nowHackatimeProjects: string[]; }; submission: { description: string; @@ -63,6 +64,7 @@ export class AirtableService { } ], 'Optional - Override Hours Spent': data.project.nowHackatimeHours, + 'Hackatime Projects': data.project.nowHackatimeProjects.join(', '), 'Automation - First Submitted At': new Date().toISOString(), 'Automation - Submit to Unified YSWS': true, }; diff --git a/owl-api/src/app.module.ts b/owl-api/src/app.module.ts index 18487cd..5534c14 100644 --- a/owl-api/src/app.module.ts +++ b/owl-api/src/app.module.ts @@ -1,4 +1,5 @@ import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; import { MailModule } from "./mail/mail.module"; import { UserModule } from "./user/user.module"; import { AuthModule } from "./auth/auth.module"; @@ -6,7 +7,6 @@ import { ProjectsModule } from "./projects/projects.module"; import { AdminModule } from "./admin/admin.module"; import { EditRequestsModule } from "./edit-requests/edit-requests.module"; import { HealthModule } from "./health/health.module"; -import { ConfigModule } from "@nestjs/config" //todo: dynamically enable & disable modules based on env. this will allow separate modules to run as separate services @Module({ diff --git a/owl-api/src/auth/auth.controller.ts b/owl-api/src/auth/auth.controller.ts index ddb2faf..c43700f 100644 --- a/owl-api/src/auth/auth.controller.ts +++ b/owl-api/src/auth/auth.controller.ts @@ -69,6 +69,12 @@ export class AuthController { return this.authService.getCurrentUser(req.cookies.sessionId); } + @Post('verify-session') + async verifySession(@Body() body: { sessionId: string }) { + const sessionId = body.sessionId; + return this.authService.getCurrentUser(sessionId); + } + @Post('logout') @UseGuards(AuthGuard) async logout(@Res({ passthrough: true }) res: Response) { @@ -90,4 +96,18 @@ export class AuthController { const userId = req.user.userId; return this.authService.getOnboardingStatus(userId); } + + @Post('hackatime-link/send-otp') + @UseGuards(AuthGuard) + async sendHackatimeLinkOtp(@Req() req: Request, @Body() body: { email: string }) { + const userId = req.user.userId; + return this.authService.sendHackatimeLinkOtp(userId, body.email); + } + + @Post('hackatime-link/verify-otp') + @UseGuards(AuthGuard) + async verifyHackatimeLinkOtp(@Req() req: Request, @Body() body: { otp: string }) { + const userId = req.user.userId; + return this.authService.verifyHackatimeLinkOtp(userId, body.otp); + } } diff --git a/owl-api/src/auth/auth.guard.ts b/owl-api/src/auth/auth.guard.ts index 47da0b8..6d2800b 100644 --- a/owl-api/src/auth/auth.guard.ts +++ b/owl-api/src/auth/auth.guard.ts @@ -8,7 +8,7 @@ export class AuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); const sessionId = request.cookies?.sessionId; - + if (!sessionId) { throw new UnauthorizedException('Session required'); } diff --git a/owl-api/src/auth/auth.service.ts b/owl-api/src/auth/auth.service.ts index 59eff66..db56692 100644 --- a/owl-api/src/auth/auth.service.ts +++ b/owl-api/src/auth/auth.service.ts @@ -39,14 +39,16 @@ export class AuthService { }, }); } else { - // For new users, create a temporary user first + const hackatimeAccount = await this.checkHackatimeAccount(email); + const tempUser = await this.prisma.user.create({ data: { email, - firstName: 'Temporary', // Temporary first name - lastName: 'User', // Temporary last name - birthday: new Date('2000-01-01'), // Temporary birthday - role: 'user', // Default role + firstName: 'Temporary', + lastName: 'User', + birthday: new Date('2000-01-01'), + role: 'user', + hackatimeAccount: hackatimeAccount?.toString() || null, }, }); @@ -115,6 +117,7 @@ export class AuthService { return { message: 'OTP verified successfully', + isNewUser: !existingUser.onboardComplete, user: { userId: existingUser.userId, email: existingUser.email, @@ -235,4 +238,191 @@ export class AuthService { private generateOtp(): string { return Math.floor(100000 + Math.random() * 900000).toString(); } + + async checkHackatimeAccount(email: string): Promise { + const HACKATIME_ADMIN_API_URL = process.env.HACKATIME_ADMIN_API_URL || 'https://hackatime.hackclub.com/api/admin/v1'; + const HACKATIME_API_KEY = process.env.HACKATIME_API_KEY; + + console.log('=== CHECKING HACKATIME ACCOUNT ==='); + console.log('Email:', email); + console.log('API Key configured:', !!HACKATIME_API_KEY); + console.log('API URL:', HACKATIME_ADMIN_API_URL); + + if (!HACKATIME_API_KEY) { + console.warn('HACKATIME_API_KEY not configured, skipping Hackatime lookup'); + return null; + } + + try { + const searchQuery = { + query: ` + SELECT + users.id, + users.username, + users.github_username, + users.slack_username, + email_addresses.email + FROM + users + INNER JOIN email_addresses ON users.id = email_addresses.user_id + WHERE + email_addresses.email = '${email}' + LIMIT 1; + `, + }; + + console.log('Sending query to Hackatime API...'); + + const res = await fetch(`${HACKATIME_ADMIN_API_URL}/execute`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${HACKATIME_API_KEY}`, + }, + body: JSON.stringify(searchQuery), + }); + + console.log('Response status:', res.status); + + if (!res.ok) { + console.error('Failed to check Hackatime account:', res.status); + return null; + } + + const data = await res.json(); + console.log('Response data:', JSON.stringify(data, null, 2)); + + if (data.rows && data.rows.length > 0) { + const hackatimeId = data.rows[0].id[1]; + console.log(`βœ“ Found Hackatime account for ${email}: ${hackatimeId}`); + return hackatimeId; + } + + console.log(`βœ— No Hackatime account found for ${email}`); + return null; + } catch (error) { + console.error('Error checking Hackatime account:', error); + return null; + } + } + + async sendHackatimeLinkOtp(userId: number, email: string) { + const user = await this.prisma.user.findUnique({ + where: { userId }, + }); + + if (!user) { + throw new UnauthorizedException('User not found'); + } + + const hackatimeAccountId = await this.checkHackatimeAccount(email); + + if (!hackatimeAccountId) { + throw new BadRequestException('No Hackatime account found with this email'); + } + + const linkedUser = await this.prisma.user.findFirst({ + where: { + hackatimeAccount: hackatimeAccountId.toString(), + NOT: { userId }, + }, + select: { userId: true }, + }); + + if (linkedUser) { + throw new BadRequestException('This Hackatime account is already linked to another user'); + } + + const otp = this.generateOtp(); + const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes + + const existingOtp = await this.prisma.hackatimeLinkOtp.findFirst({ + where: { userId }, + }); + + if (existingOtp) { + await this.prisma.hackatimeLinkOtp.delete({ + where: { id: existingOtp.id }, + }); + } + + await this.prisma.hackatimeLinkOtp.create({ + data: { + userId, + email, + otpCode: otp, + expiresAt, + }, + }); + + console.log('=== SENDING HACKATIME LINK OTP ==='); + console.log('Email:', email); + console.log('OTP:', otp); + + const htmlContent = ` +
+

Link Your Hackatime Account

+

Your verification code is:

+
+ ${otp} +
+

This code expires in 10 minutes.

+

If you didn't request this, please ignore this email.

+
+ `; + + console.log('Calling sendImmediateEmail with type: hackatime-link-otp'); + await this.mailService.sendImmediateEmail( + email, + htmlContent, + 'Link Your Hackatime Account', + { + type: 'hackatime-link-otp', + } + ); + console.log('Email sent successfully'); + + return { message: 'Verification code sent to your email' }; + } + + async verifyHackatimeLinkOtp(userId: number, otp: string) { + const otpRecord = await this.prisma.hackatimeLinkOtp.findFirst({ + where: { + userId, + otpCode: otp, + expiresAt: { gt: new Date() }, + isUsed: false, + }, + }); + + if (!otpRecord) { + throw new UnauthorizedException('Invalid or expired verification code'); + } + + const hackatimeAccount = await this.checkHackatimeAccount(otpRecord.email); + + if (!hackatimeAccount) { + throw new BadRequestException('No Hackatime account found with this email'); + } + + await this.prisma.user.update({ + where: { userId }, + data: { + hackatimeAccount: hackatimeAccount.toString(), + }, + }); + + await this.prisma.hackatimeLinkOtp.update({ + where: { id: otpRecord.id }, + data: { + isUsed: true, + usedAt: new Date(), + }, + }); + + return { + message: 'Hackatime account linked successfully', + hackatimeAccountId: hackatimeAccount, + }; + } } diff --git a/owl-api/src/datadog.config.ts b/owl-api/src/datadog.config.ts new file mode 100644 index 0000000..9d563e8 --- /dev/null +++ b/owl-api/src/datadog.config.ts @@ -0,0 +1,23 @@ +import tracer from 'dd-trace'; + +const serviceName = process.env.DD_SERVICE || 'owl-api'; +const environment = process.env.DD_ENV || process.env.NODE_ENV || 'production'; +const version = process.env.DD_VERSION || 'unknown'; + +console.log(`πŸ” Initializing Datadog tracing for service: ${serviceName}`); +console.log(` Environment: ${environment}`); +console.log(` Version: ${version}`); +console.log(` Profiling: ${process.env.DD_PROFILING_ENABLED !== 'false' ? 'enabled' : 'disabled'}`); +console.log(` APM: ${process.env.DD_APM_ENABLED !== 'false' ? 'enabled' : 'disabled'}`); + +tracer.init({ + profiling: process.env.DD_PROFILING_ENABLED !== 'false', + service: serviceName, + env: environment, + version: version, + logInjection: true, +}); + +console.log('βœ… Datadog tracing initialized successfully'); + +export default tracer; diff --git a/owl-api/src/mail/mail.service.ts b/owl-api/src/mail/mail.service.ts index e5301b1..f6ea7c8 100644 --- a/owl-api/src/mail/mail.service.ts +++ b/owl-api/src/mail/mail.service.ts @@ -180,6 +180,12 @@ export class MailService { } async sendImmediateEmail(email: string, htmlContent: string, subject: string, metadata: any = {}): Promise<{ success: boolean }> { + console.log('=== sendImmediateEmail CALLED ==='); + console.log('Email:', email); + console.log('Subject:', subject); + console.log('Metadata type:', metadata.type); + console.log('HTML content length:', htmlContent.length); + const fromEmail = process.env.EMAIL_FROM || 'noreply@midnight.hackclub.com'; const from = `Midnight (Hack Club) <${fromEmail}>`; diff --git a/owl-api/src/main.ts b/owl-api/src/main.ts index 2a4ddfa..53ba3aa 100644 --- a/owl-api/src/main.ts +++ b/owl-api/src/main.ts @@ -1,3 +1,4 @@ +import './datadog.config'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; diff --git a/owl-api/src/projects/dto/create-project.dto.ts b/owl-api/src/projects/dto/create-project.dto.ts index 3b26f3c..b9e7497 100644 --- a/owl-api/src/projects/dto/create-project.dto.ts +++ b/owl-api/src/projects/dto/create-project.dto.ts @@ -10,4 +10,8 @@ export class CreateProjectDto { @IsEnum(ProjectType) @IsNotEmpty() projectType: ProjectType; + + @IsString() + @IsNotEmpty() + projectDescription: string; } diff --git a/owl-api/src/projects/dto/update-hackatime-projects.dto.ts b/owl-api/src/projects/dto/update-hackatime-projects.dto.ts new file mode 100644 index 0000000..635887b --- /dev/null +++ b/owl-api/src/projects/dto/update-hackatime-projects.dto.ts @@ -0,0 +1,8 @@ +import { IsArray, IsString, ArrayMinSize } from 'class-validator'; + +export class UpdateHackatimeProjectsDto { + @IsArray() + @ArrayMinSize(1) + @IsString({ each: true }) + projectNames: string[]; +} diff --git a/owl-api/src/projects/dto/update-project.dto.ts b/owl-api/src/projects/dto/update-project.dto.ts index 72a691e..a48008e 100644 --- a/owl-api/src/projects/dto/update-project.dto.ts +++ b/owl-api/src/projects/dto/update-project.dto.ts @@ -1,4 +1,4 @@ -import { IsOptional, IsString, IsUrl, MaxLength } from 'class-validator'; +import { IsOptional, IsString, IsUrl, MaxLength, IsArray, ArrayMinSize } from 'class-validator'; export class UpdateProjectDto { @IsString() @@ -28,4 +28,10 @@ export class UpdateProjectDto { @IsString() @IsOptional() airtableRecId?: string; + + @IsArray() + @IsOptional() + @ArrayMinSize(1) + @IsString({ each: true }) + nowHackatimeProjects?: string[]; } diff --git a/owl-api/src/projects/projects.controller.ts b/owl-api/src/projects/projects.controller.ts index 19c8dc8..6c504f5 100644 --- a/owl-api/src/projects/projects.controller.ts +++ b/owl-api/src/projects/projects.controller.ts @@ -4,6 +4,7 @@ import { ProjectsService } from './projects.service'; import { CreateProjectDto } from './dto/create-project.dto'; import { UpdateProjectDto } from './dto/update-project.dto'; import { CreateSubmissionDto } from './dto/create-submission.dto'; +import { UpdateHackatimeProjectsDto } from './dto/update-hackatime-projects.dto'; import { AuthGuard } from '../auth/auth.guard'; @Controller('api/projects/auth') @@ -56,4 +57,21 @@ export class ProjectsController { ) { return this.projectsService.createEditRequest(id, updateProjectDto, req.user.userId); } + + @Put(':id/hackatime-projects') + async updateHackatimeProjects( + @Param('id', ParseIntPipe) id: number, + @Body() updateHackatimeProjectsDto: UpdateHackatimeProjectsDto, + @Req() req: Request, + ) { + return this.projectsService.updateHackatimeProjects(id, updateHackatimeProjectsDto, req.user.userId); + } + + @Get(':id/hackatime-projects') + async getHackatimeProjects( + @Param('id', ParseIntPipe) id: number, + @Req() req: Request, + ) { + return this.projectsService.getHackatimeProjects(id, req.user.userId); + } } diff --git a/owl-api/src/projects/projects.service.ts b/owl-api/src/projects/projects.service.ts index ee37ec1..b3c8098 100644 --- a/owl-api/src/projects/projects.service.ts +++ b/owl-api/src/projects/projects.service.ts @@ -1,8 +1,9 @@ -import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common'; +import { Injectable, NotFoundException, ForbiddenException, BadRequestException } from '@nestjs/common'; import { PrismaService } from '../prisma.service'; import { CreateProjectDto } from './dto/create-project.dto'; import { UpdateProjectDto } from './dto/update-project.dto'; import { CreateSubmissionDto } from './dto/create-submission.dto'; +import { UpdateHackatimeProjectsDto } from './dto/update-hackatime-projects.dto'; @Injectable() export class ProjectsService { @@ -14,6 +15,7 @@ export class ProjectsService { userId, projectTitle: createProjectDto.projectTitle, projectType: createProjectDto.projectType, + description: createProjectDto.projectDescription, }, include: { user: { @@ -99,7 +101,8 @@ export class ProjectsService { // Validate required project fields if (!project.projectTitle || !project.description || project.nowHackatimeHours === null || project.nowHackatimeHours === undefined || - !project.playableUrl || !project.repoUrl || !project.screenshotUrl) { + !project.playableUrl || !project.repoUrl || !project.screenshotUrl || + !project.nowHackatimeProjects || project.nowHackatimeProjects.length === 0) { throw new ForbiddenException('Project incomplete. Please complete all required project fields first.'); } @@ -177,6 +180,8 @@ export class ProjectsService { repoUrl: project.repoUrl, screenshotUrl: project.screenshotUrl, airtableRecId: project.airtableRecId, + nowHackatimeProjects: project.nowHackatimeProjects, + nowHackatimeHours: project.nowHackatimeHours, }; // Prepare requested data (only include fields that are being updated) @@ -199,6 +204,10 @@ export class ProjectsService { if (updateProjectDto.airtableRecId !== undefined) { requestedData.airtableRecId = updateProjectDto.airtableRecId; } + if (updateProjectDto.nowHackatimeProjects !== undefined) { + requestedData.nowHackatimeProjects = updateProjectDto.nowHackatimeProjects; + requestedData.nowHackatimeHours = null; // Will be calculated by admin when approving + } // Create edit request const editRequest = await this.prisma.editRequest.create({ @@ -233,4 +242,190 @@ export class ProjectsService { editRequest, }; } + + async updateHackatimeProjects(projectId: number, updateHackatimeProjectsDto: UpdateHackatimeProjectsDto, userId: number) { + const project = await this.prisma.project.findUnique({ + where: { projectId }, + include: { + submissions: true, + user: true, + }, + }); + + if (!project) { + throw new NotFoundException('Project not found'); + } + + if (project.userId !== userId) { + throw new ForbiddenException('Access denied'); + } + + if (!project.user.hackatimeAccount) { + throw new BadRequestException('No Hackatime account linked to this user'); + } + + const HACKATIME_ADMIN_API_URL = process.env.HACKATIME_ADMIN_API_URL || 'https://hackatime.hackclub.com/api/admin/v1'; + const HACKATIME_API_KEY = process.env.HACKATIME_API_KEY; + + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (HACKATIME_API_KEY) { + headers['Authorization'] = `Bearer ${HACKATIME_API_KEY}`; + } + + const res = await fetch( + `${HACKATIME_ADMIN_API_URL}/user/projects?id=${project.user.hackatimeAccount}`, + { + method: 'GET', + headers, + }, + ); + + if (!res.ok) { + throw new BadRequestException('Failed to fetch hackatime projects for validation'); + } + + const hackatimeProjects = await res.json(); + const availableProjectNames = new Set(); + + if (Array.isArray(hackatimeProjects)) { + hackatimeProjects.forEach((p: any) => { + availableProjectNames.add(p.name || p.projectName || p); + }); + } else if (hackatimeProjects.projects && Array.isArray(hackatimeProjects.projects)) { + hackatimeProjects.projects.forEach((p: any) => { + availableProjectNames.add(p.name || p.projectName || p); + }); + } else if (hackatimeProjects.name || hackatimeProjects.projectName) { + availableProjectNames.add(hackatimeProjects.name || hackatimeProjects.projectName); + } + + for (const projectName of updateHackatimeProjectsDto.projectNames) { + if (!availableProjectNames.has(projectName)) { + throw new BadRequestException(`Project "${projectName}" is not a valid hackatime project`); + } + } + + const allLinkedProjects = await this.prisma.project.findMany({ + where: { + userId: { not: userId }, + }, + select: { + nowHackatimeProjects: true, + }, + }); + + const linkedByOthers = new Set(); + allLinkedProjects.forEach(p => { + if (p.nowHackatimeProjects) { + p.nowHackatimeProjects.forEach(name => linkedByOthers.add(name)); + } + }); + + const currentlyLinked = project.nowHackatimeProjects || []; + const updatingToAlreadyLinked = updateHackatimeProjectsDto.projectNames.filter( + name => linkedByOthers.has(name) && !currentlyLinked.includes(name) + ); + + if (updatingToAlreadyLinked.length > 0) { + throw new BadRequestException( + `Project(s) ${updatingToAlreadyLinked.join(', ')} are already linked to another project` + ); + } + + // If project is locked (has submissions), create an edit request instead of direct update + if (project.isLocked) { + // Check if user has made at least one submission for this project + if (project.submissions.length === 0) { + throw new ForbiddenException('You must submit at least one submission before requesting project edits.'); + } + + // Get current project data + const currentData = { + nowHackatimeProjects: project.nowHackatimeProjects, + nowHackatimeHours: project.nowHackatimeHours, + }; + + // Prepare requested data + const requestedData = { + nowHackatimeProjects: updateHackatimeProjectsDto.projectNames, + nowHackatimeHours: null, // Will be calculated by admin when approving + }; + + // Create edit request + const editRequest = await this.prisma.editRequest.create({ + data: { + userId, + projectId, + requestType: 'project_update', + currentData, + requestedData, + }, + include: { + user: { + select: { + userId: true, + firstName: true, + lastName: true, + email: true, + }, + }, + project: { + select: { + projectId: true, + projectTitle: true, + projectType: true, + }, + }, + }, + }); + + return { + message: 'Edit request created successfully. Waiting for admin approval.', + editRequest, + }; + } + + // Direct update if project is not locked + const updatedProject = await this.prisma.project.update({ + where: { projectId }, + data: { + nowHackatimeProjects: updateHackatimeProjectsDto.projectNames, + nowHackatimeHours: null, // Will be calculated separately + }, + }); + + return { + message: 'Hackatime projects updated successfully.', + project: updatedProject, + }; + } + + async getHackatimeProjects(projectId: number, userId: number) { + const project = await this.prisma.project.findUnique({ + where: { projectId }, + select: { + projectId: true, + nowHackatimeProjects: true, + nowHackatimeHours: true, + userId: true, + }, + }); + + if (!project) { + throw new NotFoundException('Project not found'); + } + + if (project.userId !== userId) { + throw new ForbiddenException('Access denied'); + } + + return { + projectId: project.projectId, + hackatimeProjects: project.nowHackatimeProjects, + hackatimeHours: project.nowHackatimeHours, + }; + } } diff --git a/owl-api/src/redis.service.ts b/owl-api/src/redis.service.ts index ad041bd..48e6189 100644 --- a/owl-api/src/redis.service.ts +++ b/owl-api/src/redis.service.ts @@ -17,12 +17,39 @@ export class RedisService implements OnModuleInit, OnModuleDestroy { console.log('Connected to Redis'); }); - // Test Redis connection and commands - try { - await this.testRedisCommands(); - await this.getRedisInfo(); - } catch (error) { - console.error('Redis connection test failed:', error); + this.client.on('ready', () => { + console.log('Redis client ready'); + }); + + this.client.on('close', () => { + console.log('Redis connection closed'); + }); + + this.client.on('reconnecting', () => { + console.log('Redis reconnecting...'); + }); + + // Test Redis connection and commands with retry + let retryCount = 0; + const maxRetries = 3; + + while (retryCount < maxRetries) { + try { + await this.testRedisCommands(); + await this.getRedisInfo(); + console.log('Redis initialization successful'); + break; + } catch (error) { + retryCount++; + console.error(`Redis connection test failed (attempt ${retryCount}/${maxRetries}):`, error); + + if (retryCount < maxRetries) { + console.log('Retrying Redis connection in 2 seconds...'); + await new Promise(resolve => setTimeout(resolve, 2000)); + } else { + console.error('Redis connection failed after all retries. Application will continue but Redis features may not work properly.'); + } + } } } @@ -94,14 +121,33 @@ export class RedisService implements OnModuleInit, OnModuleDestroy { } async onModuleDestroy() { - await this.client.quit(); + if (this.client) { + await this.client.quit(); + } + } + + private async isRedisAvailable(): Promise { + try { + await this.client.ping(); + return true; + } catch (error) { + return false; + } } async get(key: string): Promise { + if (!(await this.isRedisAvailable())) { + console.warn('Redis not available, returning null for get operation'); + return null; + } return await this.client.get(key); } async set(key: string, value: string, ttlSeconds?: number): Promise { + if (!(await this.isRedisAvailable())) { + console.warn('Redis not available, skipping set operation'); + return; + } if (ttlSeconds) { await this.client.setex(key, ttlSeconds, value); } else { @@ -110,31 +156,58 @@ export class RedisService implements OnModuleInit, OnModuleDestroy { } async del(key: string): Promise { + if (!(await this.isRedisAvailable())) { + console.warn('Redis not available, skipping delete operation'); + return; + } await this.client.del(key); } async exists(key: string): Promise { + if (!(await this.isRedisAvailable())) { + console.warn('Redis not available, returning false for exists check'); + return false; + } const result = await this.client.exists(key); return result === 1; } async acquireLock(key: string, value: string, ttlSeconds: number): Promise { + if (!(await this.isRedisAvailable())) { + console.warn('Redis not available, lock acquisition failed'); + return false; + } + try { + // Try the modern SET with NX and EX options first const result = await this.client.set(key, value, 'EX', ttlSeconds, 'NX'); return result === 'OK'; } catch (error) { - console.error('Redis acquireLock error:', error); - // Fallback to using SETNX + EXPIRE - const setResult = await this.client.setnx(key, value); - if (setResult === 1) { - await this.client.expire(key, ttlSeconds); - return true; + console.warn('Redis SET with NX/EX failed, trying fallback method:', error.message); + + try { + // Fallback to using SETNX + EXPIRE + const setResult = await this.client.setnx(key, value); + if (setResult === 1) { + await this.client.expire(key, ttlSeconds); + return true; + } + return false; + } catch (fallbackError) { + console.error('Redis acquireLock fallback also failed:', fallbackError); + // If Redis is completely unavailable, we could return false or implement + // an in-memory lock mechanism as a last resort + return false; } - return false; } } async releaseLock(key: string, value: string): Promise { + if (!(await this.isRedisAvailable())) { + console.warn('Redis not available, lock release failed'); + return false; + } + const script = ` if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) @@ -147,6 +220,11 @@ export class RedisService implements OnModuleInit, OnModuleDestroy { } async extendLock(key: string, value: string, ttlSeconds: number): Promise { + if (!(await this.isRedisAvailable())) { + console.warn('Redis not available, lock extension failed'); + return false; + } + const script = ` if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("expire", KEYS[1], ARGV[2]) @@ -173,6 +251,7 @@ export class RedisService implements OnModuleInit, OnModuleDestroy { try { // Test basic commands await this.client.ping(); + console.log('Redis PING successful'); // Test SET command await this.client.set('test:basic', 'value'); @@ -182,6 +261,7 @@ export class RedisService implements OnModuleInit, OnModuleDestroy { if (value !== 'value') { throw new Error('Basic SET/GET failed'); } + console.log('Redis SET/GET/DEL commands working'); // Test SET with EX const setExResult = await this.client.setex('test:ex', 10, 'value'); @@ -189,15 +269,29 @@ export class RedisService implements OnModuleInit, OnModuleDestroy { throw new Error('SETEX command failed'); } await this.client.del('test:ex'); + console.log('Redis SETEX command working'); - // Test SETNX - const setNxResult = await this.client.setnx('test:nx', 'value'); - if (setNxResult !== 1) { - throw new Error('SETNX command failed'); + // Test SETNX with better error handling + try { + const setNxResult = await this.client.setnx('test:nx', 'value'); + if (setNxResult !== 1) { + console.warn('SETNX command returned unexpected result:', setNxResult); + // Don't throw error, just log warning + } else { + console.log('Redis SETNX command working'); + } + await this.client.del('test:nx'); + } catch (setNxError) { + console.warn('SETNX command failed, but continuing:', setNxError.message); + // Try to clean up if key exists + try { + await this.client.del('test:nx'); + } catch (cleanupError) { + // Ignore cleanup errors + } } - await this.client.del('test:nx'); - console.log('All Redis commands working correctly'); + console.log('Redis connection and basic commands working correctly'); return true; } catch (error) { console.error('Redis command test failed:', error); diff --git a/owl-api/src/user/user.controller.ts b/owl-api/src/user/user.controller.ts index b4ee62c..8f20f07 100644 --- a/owl-api/src/user/user.controller.ts +++ b/owl-api/src/user/user.controller.ts @@ -1,4 +1,5 @@ import { Controller, Post, Get, Put, Body, Req, Res, HttpCode, UseGuards } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; import { UserService } from './user.service'; import { InitialRsvpDto } from './dto/initial-rsvp.dto'; import { CompleteRsvpDto } from './dto/complete-rsvp.dto'; @@ -125,4 +126,21 @@ export class UserController { const userId = req.user.userId; return this.userService.updateUser(userId, updateUserDto); } + + @Get('api/user/hackatime-projects') + @UseGuards(AuthGuard) + @HttpCode(200) + async getHackatimeProjects(@Req() req: express.Request) { + const userEmail = req.user.email; + return this.userService.getHackatimeProjects(userEmail); + } + + @Get('api/user/hackatime-account') + @UseGuards(AuthGuard) + @Throttle({ default: { ttl: 2000, limit: 1 } }) + @HttpCode(200) + async checkHackatimeAccount(@Req() req: express.Request) { + const userEmail = req.user.email; + return this.userService.checkHackatimeAccountStatus(userEmail); + } } diff --git a/owl-api/src/user/user.service.ts b/owl-api/src/user/user.service.ts index ceb2e00..407e591 100644 --- a/owl-api/src/user/user.service.ts +++ b/owl-api/src/user/user.service.ts @@ -602,4 +602,201 @@ export class UserService { getHealth() { return { status: 'ok', service: 'user-service' }; } + + async checkHackatimeAccountStatus(userEmail: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { email: userEmail }, + select: { + userId: true, + email: true, + hackatimeAccount: true, + }, + }); + + if (!user) { + throw new HttpException( + 'User not found', + HttpStatus.NOT_FOUND, + ); + } + + // Query hackatime database directly to check if account exists + const hackatimeId = await this.checkHackatimeAccount(userEmail); + + // Update user's hackatime account if found and different from stored value + if (hackatimeId && hackatimeId.toString() !== user.hackatimeAccount) { + await this.prisma.user.update({ + where: { userId: user.userId }, + data: { hackatimeAccount: hackatimeId.toString() }, + }); + } + + return { + email: user.email, + hasHackatimeAccount: !!hackatimeId, + hackatimeAccountId: hackatimeId?.toString() || null, + }; + } + + private async checkHackatimeAccount(email: string): Promise { + const HACKATIME_ADMIN_API_URL = process.env.HACKATIME_ADMIN_API_URL || 'https://hackatime.hackclub.com/api/admin/v1'; + const HACKATIME_API_KEY = process.env.HACKATIME_API_KEY; + + console.log('=== CHECKING HACKATIME ACCOUNT ==='); + console.log('Email:', email); + console.log('API Key configured:', !!HACKATIME_API_KEY); + console.log('API URL:', HACKATIME_ADMIN_API_URL); + + if (!HACKATIME_API_KEY) { + console.warn('HACKATIME_API_KEY not configured, skipping Hackatime lookup'); + return null; + } + + try { + const searchQuery = { + query: ` + SELECT + users.id, + users.username, + users.github_username, + users.slack_username, + email_addresses.email + FROM + users + INNER JOIN email_addresses ON users.id = email_addresses.user_id + WHERE + email_addresses.email = '${email}' + LIMIT 1; + `, + }; + + console.log('Sending query to Hackatime API...'); + + const res = await fetch(`${HACKATIME_ADMIN_API_URL}/execute`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${HACKATIME_API_KEY}`, + }, + body: JSON.stringify(searchQuery), + }); + + console.log('Response status:', res.status); + + if (!res.ok) { + console.error('Failed to check Hackatime account:', res.status); + return null; + } + + const data = await res.json(); + console.log('Response data:', JSON.stringify(data, null, 2)); + + if (data.rows && data.rows.length > 0) { + const hackatimeId = data.rows[0].id[1]; + console.log(`βœ“ Found Hackatime account for ${email}: ${hackatimeId}`); + return hackatimeId; + } + + console.log(`βœ— No Hackatime account found for ${email}`); + return null; + } catch (error) { + console.error('Error checking Hackatime account:', error); + return null; + } + } + + async getHackatimeProjects(userEmail: string): Promise { + const HACKATIME_ADMIN_API_URL = process.env.HACKATIME_ADMIN_API_URL || 'https://hackatime.hackclub.com/api/admin/v1'; + const HACKATIME_API_KEY = process.env.HACKATIME_API_KEY; + + const user = await this.prisma.user.findUnique({ + where: { email: userEmail }, + include: { + projects: { + select: { + nowHackatimeProjects: true, + }, + }, + }, + }); + + if (!user) { + throw new HttpException( + 'User not found', + HttpStatus.NOT_FOUND, + ); + } + + if (!user.hackatimeAccount) { + throw new HttpException( + 'No Hackatime account linked to this user', + HttpStatus.NOT_FOUND, + ); + } + + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (HACKATIME_API_KEY) { + headers['Authorization'] = `Bearer ${HACKATIME_API_KEY}`; + } + + const res = await fetch( + `${HACKATIME_ADMIN_API_URL}/user/projects?id=${user.hackatimeAccount}`, + { + method: 'GET', + headers, + }, + ); + + if (!res.ok) { + if (res.status === 404) { + throw new HttpException( + 'Hackatime projects not found for this user', + HttpStatus.NOT_FOUND, + ); + } + throw new HttpException( + 'Failed to fetch hackatime projects', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const hackatimeProjects = await res.json(); + const linkedProjectNames = new Set(); + + user.projects.forEach(project => { + if (project.nowHackatimeProjects) { + project.nowHackatimeProjects.forEach(name => linkedProjectNames.add(name)); + } + }); + + if (Array.isArray(hackatimeProjects)) { + return hackatimeProjects.filter((project: any) => + !linkedProjectNames.has(project.name || project.projectName || project) + ); + } + + if (hackatimeProjects.projects && Array.isArray(hackatimeProjects.projects)) { + return { + ...hackatimeProjects, + projects: hackatimeProjects.projects.filter((project: any) => + !linkedProjectNames.has(project.name || project.projectName || project) + ), + }; + } + + if (hackatimeProjects.name || hackatimeProjects.projectName) { + const projectName = hackatimeProjects.name || hackatimeProjects.projectName; + if (linkedProjectNames.has(projectName)) { + throw new HttpException( + 'All hackatime projects are already linked', + HttpStatus.NOT_FOUND, + ); + } + } + + return hackatimeProjects; + } } diff --git a/owl-api/start.sh b/owl-api/start.sh index ac80ede..b5510eb 100644 --- a/owl-api/start.sh +++ b/owl-api/start.sh @@ -42,23 +42,71 @@ else echo "Attempting to resolve failed migrations..." # Resolve potentially failed migrations in order - echo "Resolving migration: 20251021000000_add_sticker_tokens" - npx prisma migrate resolve --applied 20251021000000_add_sticker_tokens || echo "Could not resolve add_sticker_tokens migration" + echo "Resolving migration: 20250123120000_add_user_project_submission_models" + npx prisma migrate resolve --applied 20250123120000_add_user_project_submission_models || echo "Could not resolve add_user_project_submission_models migration" - echo "Resolving migration: 20251021000001_add_scheduled_for" - npx prisma migrate resolve --applied 20251021000001_add_scheduled_for || echo "Could not resolve add_scheduled_for migration" + # If the old migration structure exists, try to resolve those as well + echo "Resolving migration: 20251020000000_init" + npx prisma migrate resolve --applied 20251020000000_init || echo "Could not resolve init migration" - echo "Resolving migration: 20251021000002_add_job_locking" - npx prisma migrate resolve --applied 20251021000002_add_job_locking || echo "Could not resolve add_job_locking migration" + echo "Resolving migration: 20251021000000_add_sticker_tokens" + npx prisma migrate resolve --applied 20251021000000_add_sticker_tokens || echo "Could not resolve add_sticker_tokens migration" echo "Retrying migration deploy after resolution attempts..." if npx prisma migrate deploy; then echo "Migrations completed successfully after resolution" else - echo "Migration still failed after resolution, but continuing with startup" + echo "Migration still failed after resolution, trying consolidated migration..." + + # Try to resolve the consolidated migration if it exists + echo "Resolving consolidated migration: 20250123120000_init_consolidated" + npx prisma migrate resolve --applied 20250123120000_init_consolidated || echo "Could not resolve consolidated migration" + + # Check if the error is about existing types/tables (common after partial migrations) + echo "Checking if schema elements already exist..." + if npx prisma db push --accept-data-loss --skip-generate > /dev/null 2>&1; then + echo "Schema is already up to date, marking migration as applied" + npx prisma migrate resolve --applied 20250123120000_init_consolidated || echo "Could not mark consolidated migration as applied" + fi + + # Final attempt to deploy + if npx prisma migrate deploy; then + echo "Migrations completed successfully with consolidated migration" + else + echo "Migration still failed, but continuing with startup" + fi fi fi fi echo "Starting user service..." + +# Initialize Datadog monitoring +echo "Initializing Datadog monitoring..." +if [ -n "$DD_AGENT_HOST" ]; then + echo "Datadog agent host: $DD_AGENT_HOST" + echo "Datadog trace agent port: ${DD_TRACE_AGENT_PORT:-8126}" + echo "Datadog APM enabled: ${DD_APM_ENABLED:-false}" + echo "Datadog profiling enabled: ${DD_PROFILING_ENABLED:-false}" + echo "Datadog service: ${DD_SERVICE:-owl-api}" + echo "Datadog environment: ${DD_ENV:-production}" + echo "Datadog version: ${DD_VERSION:-1.0.0}" + + # Check if Datadog agent is reachable + if command -v nc >/dev/null 2>&1; then + if nc -z "$DD_AGENT_HOST" "${DD_TRACE_AGENT_PORT:-8126}" 2>/dev/null; then + echo "βœ… Datadog agent is reachable at $DD_AGENT_HOST:${DD_TRACE_AGENT_PORT:-8126}" + else + echo "⚠️ Warning: Datadog agent at $DD_AGENT_HOST:${DD_TRACE_AGENT_PORT:-8126} is not reachable" + echo " Application will continue without APM tracing" + fi + else + echo "ℹ️ Note: netcat not available, skipping Datadog agent connectivity check" + echo " Datadog tracing will be attempted regardless" + fi +else + echo "⚠️ Warning: DD_AGENT_HOST not set, Datadog monitoring disabled" +fi + +echo "Starting NestJS application..." exec node dist/main diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 250bc4e..21b3bc5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,13 +54,13 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^6.1.0 - version: 6.1.1(@sveltejs/kit@2.46.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6))) + version: 6.1.1(@sveltejs/kit@2.46.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6))) '@sveltejs/adapter-node': specifier: ^5.3.3 - version: 5.3.3(@sveltejs/kit@2.46.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6))) + version: 5.3.3(@sveltejs/kit@2.46.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6))) '@sveltejs/kit': specifier: ^2.43.2 - version: 2.46.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)) + version: 2.46.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)) '@sveltejs/vite-plugin-svelte': specifier: ^6.2.0 version: 6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)) @@ -115,6 +115,9 @@ importers: cookie-parser: specifier: ^1.4.7 version: 1.4.7 + dd-trace: + specifier: ^5.73.0 + version: 5.73.0(@openfeature/core@1.9.1) dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -429,6 +432,42 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@datadog/flagging-core@0.1.0-preview.12': + resolution: {integrity: sha512-3oI/8dNpGkZaYf77KJpZMNoYpnkwCq3Dgy0UChEz6JX2jtxbwv1sQFAaG1z078T/FwPnB2hMuO5zLXKkx53StA==} + peerDependencies: + '@openfeature/core': ^1.8.1 + + '@datadog/libdatadog@0.7.0': + resolution: {integrity: sha512-VVZLspzQcfEU47gmGCVoRkngn7RgFRR4CHjw4YaX8eWT+xz4Q4l6PvA45b7CMk9nlt3MNN5MtGdYttYMIpo6Sg==} + + '@datadog/native-appsec@10.3.0': + resolution: {integrity: sha512-FjNhxKetbBVGiq+dl8NYoi/95IAYU+W7PjBceNP7Qkvbt8uhy+KTlQVW1A2X67mK0CKHahDTesBMaPwY9zhflw==} + engines: {node: '>=16'} + + '@datadog/native-iast-taint-tracking@4.0.0': + resolution: {integrity: sha512-2uF8RnQkJO5bmLi26Zkhxg+RFJn/uEsesYTflScI/Cz/BWv+792bxI+OaCKvhgmpLkm8EElenlpidcJyZm7GYw==} + + '@datadog/native-metrics@3.1.1': + resolution: {integrity: sha512-MU1gHrolwryrU4X9g+fylA1KPH3S46oqJPEtVyrO+3Kh29z80fegmtyrU22bNt8LigPUK/EdPCnSbMe88QbnxQ==} + engines: {node: '>=16'} + + '@datadog/openfeature-node-server@0.1.0-preview.12': + resolution: {integrity: sha512-bzh2zk84bobSyOzcF6wxzFIKcEQV2M2QP5nWLfDNOPl4tS/qxoTpi3hvT8a6TnPvXmSZrcfB2JUq54qaT3BnKw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@openfeature/server-sdk': ~1.18.0 + + '@datadog/pprof@5.11.1': + resolution: {integrity: sha512-DX3F0v0BVOuP7RUBiu7bDhuGfFfICJRcElB+ZrEykMvkvBXe7FdVXoybYFviLH177hulwYTtW9Rcly7NXGarTw==} + engines: {node: '>=16'} + + '@datadog/sketches-js@2.1.1': + resolution: {integrity: sha512-d5RjycE+MObE/hU+8OM5Zp4VjTwiPLRa8299fj7muOmR16fb942z8byoMbCErnGh0lBevvgkGrLclQDvINbIyg==} + + '@datadog/wasm-js-rewriter@4.0.1': + resolution: {integrity: sha512-JRa05Je6gw+9+3yZnm/BroQZrEfNwRYCxms56WCCHzOBnoPihQLB0fWy5coVJS29kneCUueUvBvxGp6NVXgdqw==} + engines: {node: '>= 10'} + '@emnapi/core@1.6.0': resolution: {integrity: sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==} @@ -810,6 +849,10 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@isaacs/ttlcache@1.4.1': + resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} + engines: {node: '>=12'} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -922,6 +965,18 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -1034,6 +1089,43 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@openfeature/core@1.9.1': + resolution: {integrity: sha512-YySPtH4s/rKKnHRU0xyFGrqMU8XA+OIPNWDrlEFxE6DCVWCIrxE5YpiB94YD2jMFn6SSdA0cwQ8vLkCkl8lm8A==} + + '@opentelemetry/api-logs@0.207.0': + resolution: {integrity: sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.9.1': + resolution: {integrity: sha512-6/qon6tw2I8ZaJnHAQUUn4BqhTbTNRS0WP8/bA0ynaX+Uzp/DDbd0NS0Cq6TMlh8+mrlsyqDE7mO50nmv2Yvlg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.5.0' + + '@opentelemetry/resources@1.9.1': + resolution: {integrity: sha512-VqBGbnAfubI+l+yrtYxeLyOoL358JK57btPMJDd3TCOV3mV5TNBmzvOfmesM4NeTyXuGJByd3XvOHvFezLn3rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.5.0' + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.9.1': + resolution: {integrity: sha512-oPQdbFDmZvjXk5ZDoBGXG8B4tSB/qW5vQunJWQMFUBp7Xe8O1ByPANueJ+Jzg58esEBegyyxZ7LRmfJr7kFcFg==} + engines: {node: '>=14'} + '@paralleldrive/cuid2@2.2.2': resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} @@ -1078,6 +1170,36 @@ packages: '@prisma/get-platform@6.18.0': resolution: {integrity: sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@rollup/plugin-commonjs@28.0.7': resolution: {integrity: sha512-6cE2Wr/MkpdtTS8gXlCn9Zdmf7e9Xm96yFqOwFEXuvYLAHtjRf57/n6GEVF4K8NSesT1eKdBtcDA/SQdpW/8nA==} engines: {node: '>=16.0.0 || 14 >= 14.17'} @@ -1730,6 +1852,11 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-import-phases@1.0.4: resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} engines: {node: '>=10.13.0'} @@ -2021,6 +2148,9 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.1.0: resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} @@ -2195,6 +2325,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-randomuuid@1.0.0: + resolution: {integrity: sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA==} + css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} @@ -2206,6 +2339,22 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} + dc-polyfill@0.1.10: + resolution: {integrity: sha512-9iSbB8XZ7aIrhUtWI5ulEOJ+IyUN+axquodHK+bZO4r7HfY/xwmo6I4fYYf+aiDom+WMcN/wnzCz+pKvHDDCug==} + engines: {node: '>=12.17'} + + dd-trace@5.73.0: + resolution: {integrity: sha512-n5CKDhkgm2VdcZ6fKIdWhUQkDKyuZmJXXafUe/X62idJ020x0B9ERaKARs3zdCtNzK08yzSNElljKOcD83VD2Q==} + engines: {node: '>=18'} + peerDependencies: + '@openfeature/core': ^1.9.0 + '@openfeature/server-sdk': ~1.19.0 + peerDependenciesMeta: + '@openfeature/core': + optional: true + '@openfeature/server-sdk': + optional: true + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -2248,6 +2397,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delay@5.0.0: + resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} + engines: {node: '>=10'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2563,6 +2716,9 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -2856,6 +3012,9 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-in-the-middle@1.15.0: + resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} engines: {node: '>=8'} @@ -3013,6 +3172,10 @@ packages: resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-docblock@30.2.0: resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -3132,6 +3295,10 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -3163,6 +3330,11 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonpath-plus@10.3.0: + resolution: {integrity: sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==} + engines: {node: '>=18.0.0'} + hasBin: true + juice@10.0.1: resolution: {integrity: sha512-ZhJT1soxJCkOiO55/mz8yeBKTAJhRzX9WBO+16ZTqNTONnnVlUPyVBIzQ7lDRjaBdTbid+bAnyIon/GM3yp4cA==} engines: {node: '>=10.0.0'} @@ -3250,6 +3422,9 @@ packages: resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} + limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -3284,6 +3459,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -3291,6 +3469,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lower-case@1.1.4: resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} @@ -3304,6 +3485,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -3520,6 +3705,9 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -3542,6 +3730,9 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + mutexify@1.4.0: + resolution: {integrity: sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3572,6 +3763,9 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} @@ -3591,6 +3785,14 @@ packages: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} + node-gyp-build@3.9.0: + resolution: {integrity: sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A==} + hasBin: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -3648,6 +3850,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + opentracing@0.14.7: + resolution: {integrity: sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q==} + engines: {node: '>=0.10'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3776,6 +3982,9 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + pprof-format@2.2.1: + resolution: {integrity: sha512-p4tVN7iK19ccDqQv8heyobzUmbHyds4N2FI6aBMcXz6y99MglTWDxIyhFkNaLeEXs6IFUEzT0zya0icbSLLY0g==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3806,6 +4015,10 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3831,6 +4044,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -3914,10 +4130,17 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup@4.52.4: resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3954,6 +4177,9 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + semifies@1.0.0: + resolution: {integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4056,6 +4282,9 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + spark-md5@3.0.2: + resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} + spawn-command@0.0.2: resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} @@ -4203,6 +4432,9 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tlhunter-sorted-set@0.1.0: + resolution: {integrity: sha512-eGYW4bjf1DtrHzUYxYfAcSytpOkA44zsr7G2n3PV7yOUR23vmkGe3LL4R+1jL9OsXtbsFOwe8XtbCrabeaEFnw==} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -4299,6 +4531,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + ttl-set@1.0.0: + resolution: {integrity: sha512-2fuHn/UR+8Z9HK49r97+p2Ru1b5Eewg2QqPrU14BVCQ9QoyU3+vLLZk2WEiyZ9sgJh6W8G1cZr9I2NBLywAHrA==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4819,6 +5054,49 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@datadog/flagging-core@0.1.0-preview.12(@openfeature/core@1.9.1)': + dependencies: + '@openfeature/core': 1.9.1 + spark-md5: 3.0.2 + + '@datadog/libdatadog@0.7.0': {} + + '@datadog/native-appsec@10.3.0': + dependencies: + node-gyp-build: 3.9.0 + + '@datadog/native-iast-taint-tracking@4.0.0': + dependencies: + node-gyp-build: 3.9.0 + + '@datadog/native-metrics@3.1.1': + dependencies: + node-addon-api: 6.1.0 + node-gyp-build: 3.9.0 + + '@datadog/openfeature-node-server@0.1.0-preview.12(@openfeature/core@1.9.1)': + dependencies: + '@datadog/flagging-core': 0.1.0-preview.12(@openfeature/core@1.9.1) + transitivePeerDependencies: + - '@openfeature/core' + + '@datadog/pprof@5.11.1': + dependencies: + delay: 5.0.0 + node-gyp-build: 3.9.0 + p-limit: 3.1.0 + pprof-format: 2.2.1 + source-map: 0.7.6 + + '@datadog/sketches-js@2.1.1': {} + + '@datadog/wasm-js-rewriter@4.0.1': + dependencies: + js-yaml: 4.1.0 + lru-cache: 7.18.3 + module-details-from-path: 1.0.4 + node-gyp-build: 4.8.4 + '@emnapi/core@1.6.0': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -5131,6 +5409,8 @@ snapshots: dependencies: minipass: 7.1.2 + '@isaacs/ttlcache@1.4.1': {} + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -5349,6 +5629,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + '@lukeed/csprng@1.1.0': {} '@napi-rs/wasm-runtime@0.2.12': @@ -5490,6 +5778,34 @@ snapshots: '@one-ini/wasm@0.1.1': {} + '@openfeature/core@1.9.1': {} + + '@opentelemetry/api-logs@0.207.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/core@1.9.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.9.1 + + '@opentelemetry/resources@1.9.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.9.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.9.1 + + '@opentelemetry/semantic-conventions@1.28.0': {} + + '@opentelemetry/semantic-conventions@1.9.1': {} + '@paralleldrive/cuid2@2.2.2': dependencies: '@noble/hashes': 1.8.0 @@ -5536,6 +5852,29 @@ snapshots: dependencies: '@prisma/debug': 6.18.0 + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@rollup/plugin-commonjs@28.0.7(rollup@4.52.4)': dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.52.4) @@ -5654,19 +5993,19 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-auto@6.1.1(@sveltejs/kit@2.46.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))': + '@sveltejs/adapter-auto@6.1.1(@sveltejs/kit@2.46.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))': dependencies: - '@sveltejs/kit': 2.46.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)) + '@sveltejs/kit': 2.46.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)) - '@sveltejs/adapter-node@5.3.3(@sveltejs/kit@2.46.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))': + '@sveltejs/adapter-node@5.3.3(@sveltejs/kit@2.46.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))': dependencies: '@rollup/plugin-commonjs': 28.0.7(rollup@4.52.4) '@rollup/plugin-json': 6.1.0(rollup@4.52.4) '@rollup/plugin-node-resolve': 16.0.3(rollup@4.52.4) - '@sveltejs/kit': 2.46.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)) + '@sveltejs/kit': 2.46.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)) rollup: 4.52.4 - '@sveltejs/kit@2.46.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6))': + '@sveltejs/kit@2.46.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) @@ -5684,6 +6023,8 @@ snapshots: sirv: 3.0.2 svelte: 5.40.0 vite: 7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6) + optionalDependencies: + '@opentelemetry/api': 1.9.0 '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)))(svelte@5.40.0)(vite@7.1.10(@types/node@22.18.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6))': dependencies: @@ -6205,6 +6546,10 @@ snapshots: mime-types: 3.0.1 negotiator: 1.0.0 + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-import-phases@1.0.4(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -6540,6 +6885,8 @@ snapshots: dependencies: consola: 3.4.2 + cjs-module-lexer@1.4.3: {} + cjs-module-lexer@2.1.0: {} class-transformer@0.5.1: {} @@ -6695,6 +7042,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-randomuuid@1.0.0: {} + css-select@5.2.2: dependencies: boolbase: 1.0.0 @@ -6709,6 +7058,49 @@ snapshots: dependencies: '@babel/runtime': 7.28.4 + dc-polyfill@0.1.10: {} + + dd-trace@5.73.0(@openfeature/core@1.9.1): + dependencies: + '@datadog/libdatadog': 0.7.0 + '@datadog/native-appsec': 10.3.0 + '@datadog/native-iast-taint-tracking': 4.0.0 + '@datadog/native-metrics': 3.1.1 + '@datadog/openfeature-node-server': 0.1.0-preview.12(@openfeature/core@1.9.1) + '@datadog/pprof': 5.11.1 + '@datadog/sketches-js': 2.1.1 + '@datadog/wasm-js-rewriter': 4.0.1 + '@isaacs/ttlcache': 1.4.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.207.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.9.1(@opentelemetry/api@1.9.0) + crypto-randomuuid: 1.0.0 + dc-polyfill: 0.1.10 + ignore: 7.0.5 + import-in-the-middle: 1.15.0 + istanbul-lib-coverage: 3.2.2 + jest-docblock: 29.7.0 + jsonpath-plus: 10.3.0 + limiter: 1.1.5 + lodash.sortby: 4.7.0 + lru-cache: 10.4.3 + module-details-from-path: 1.0.4 + mutexify: 1.4.0 + opentracing: 0.14.7 + path-to-regexp: 0.1.12 + pprof-format: 2.2.1 + protobufjs: 7.5.4 + retry: 0.13.1 + rfdc: 1.4.1 + semifies: 1.0.0 + shell-quote: 1.8.3 + source-map: 0.7.6 + tlhunter-sorted-set: 0.1.0 + ttl-set: 1.0.0 + optionalDependencies: + '@openfeature/core': 1.9.1 + debug@2.6.9: dependencies: ms: 2.0.0 @@ -6731,6 +7123,8 @@ snapshots: defu@6.1.4: {} + delay@5.0.0: {} + delayed-stream@1.0.0: {} denque@2.1.0: {} @@ -7117,6 +7511,8 @@ snapshots: fast-diff@1.3.0: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7460,6 +7856,13 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@1.15.0: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + import-local@3.2.0: dependencies: pkg-dir: 4.2.0 @@ -7670,6 +8073,10 @@ snapshots: chalk: 4.1.2 pretty-format: 30.2.0 + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + jest-docblock@30.2.0: dependencies: detect-newline: 3.1.0 @@ -7920,6 +8327,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsep@1.4.0: {} + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -7942,6 +8351,12 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonpath-plus@10.3.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + juice@10.0.1: dependencies: cheerio: 1.0.0-rc.12 @@ -8012,6 +8427,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 + limiter@1.1.5: {} + lines-and-columns@1.2.4: {} load-esm@1.0.3: {} @@ -8036,6 +8453,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash.sortby@4.7.0: {} + lodash@4.17.21: {} log-symbols@4.1.0: @@ -8043,6 +8462,8 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + long@5.3.2: {} + lower-case@1.1.4: {} lru-cache@10.4.3: {} @@ -8053,6 +8474,8 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@7.18.3: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -8441,6 +8864,8 @@ snapshots: dependencies: minimist: 1.2.8 + module-details-from-path@1.0.4: {} + mri@1.2.0: {} mrmime@2.0.1: {} @@ -8461,6 +8886,10 @@ snapshots: mute-stream@2.0.0: {} + mutexify@1.4.0: + dependencies: + queue-tick: 1.0.1 + nanoid@3.3.11: {} napi-postinstall@0.3.4: {} @@ -8479,6 +8908,8 @@ snapshots: node-abort-controller@3.1.1: {} + node-addon-api@6.1.0: {} + node-emoji@1.11.0: dependencies: lodash: 4.17.21 @@ -8491,6 +8922,10 @@ snapshots: node-forge@1.3.1: {} + node-gyp-build@3.9.0: {} + + node-gyp-build@4.8.4: {} + node-int64@0.4.0: {} node-releases@2.0.23: {} @@ -8539,6 +8974,8 @@ snapshots: dependencies: mimic-fn: 2.1.0 + opentracing@0.14.7: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -8664,6 +9101,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + pprof-format@2.2.1: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -8689,6 +9128,21 @@ snapshots: proto-list@1.2.4: {} + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 22.18.10 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -8710,6 +9164,8 @@ snapshots: queue-microtask@1.2.3: {} + queue-tick@1.0.1: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -8786,8 +9242,12 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + retry@0.13.1: {} + reusify@1.1.0: {} + rfdc@1.4.1: {} + rollup@4.52.4: dependencies: '@types/estree': 1.0.8 @@ -8859,6 +9319,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) + semifies@1.0.0: {} + semver@6.3.1: {} semver@7.7.3: {} @@ -8991,6 +9453,8 @@ snapshots: source-map@0.7.6: {} + spark-md5@3.0.2: {} + spawn-command@0.0.2: {} sprintf-js@1.0.3: {} @@ -9153,6 +9617,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tlhunter-sorted-set@0.1.0: {} + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -9247,6 +9713,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + ttl-set@1.0.0: + dependencies: + fast-fifo: 1.3.2 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1