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 @@
-
-
-
-
-
-
+
navigateTo('create')}
+ onclick={() => navigateTo('create')}
role="tab"
aria-selected={activeTab === 'create'}
>
Create
+
navigateTo('explore')}
+ role="tab"
+ aria-selected={activeTab === 'explore'}
+ >
+ Explore
+ {#if onboarding}
+
+ {/if}
+
+
navigateTo('shop')}
+ role="tab"
+ aria-selected={activeTab === 'shop'}
+ >
+ Shop
+ {#if onboarding}
+
+ {/if}
+
+
+
+ {#if !onboarding}
+
+ {/if}
-
navigateTo('explore')}
- role="tab"
- aria-selected={activeTab === 'explore'}
- >
- Explore
-
-
navigateTo('shop')}
- role="tab"
- aria-selected={activeTab === 'shop'}
- >
- Shop
-
-
navigateTo('login')}
- role="tab"
- aria-selected={activeTab === 'login'}
- >
- LOGIN
-
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 @@
+
+
+
+
+ {label}
+ {#if icon}
+
+ {/if}
+
+
+
+
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
+
+
+
+
+
+
+ {/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 @@
+
+
+
+
+
+
+
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 @@
+
+
+
+
e.stopPropagation()} role="none">
+ {#if page == 0}
+
+
LINK your account to hackatime
+
+ checking if you already have a hackatime account...
+
+
+
+ {/if}
+ {#if page == 1}
+
+ {#if hasHackatime}
+
LINK your account to hackatime
+
+ hackatime account found! β
+
+ if your aunt knew what hackatime was, sheβd be proud!
+
+
+
+ {:else}
+
Sign up for hackatime
+
+
We couldn't find a hackatime account associated with this email. Let's get you set up with hackatime.
+
+
What is Hackatime?
+
blah blah blah blah insert useful description of hackatime for people to understand it blah blah
+
+
+
+ skipPage(10)}>
+ skipPage(20)}>
+
+ {/if}
+
+ {/if}
+ {#if page == 2}
+
+
Hackatime Integrity Agreement
+
+
+ Donβt cheat the time tracking system. No bots, no fake key presses, no UI manipulation. If you do, youβll be banned from Hackatime and other participating YSWS / events / programs
+
+
+ By clicking "Next", you agree to the integrity agreement.
+
+
+
+
+ {/if}
+
+
+ {#if page == 3}
+
+
FIRST PROJECT SET UP!
+
+
Nice Work! You successfully set up your first project! Build and submit your first project to get this exclusive sticker!
+
+
+
+ {/if}
+
+
+ {#if page == 10}
+
Link hackatime account
+
+
+ Enter your email to link your hackatime account
+
+
+
+
+ skipPage(1)}>
+ {
+ submitting = true;
+ const result = await sendHackatimeOtp(email);
+ submitting = false;
+ if (result.ok) {
+ nextPage();
+ } else {
+ try {
+ const jsonData = await result.json();
+ error = jsonData.error || "Failed to send OTP";
+ } catch (e) {
+ error = "Failed to send OTP";
+ }
+ }
+ }}>
+
+ {#if error}
+
{error}
+ {/if}
+ {/if}
+ {#if page == 11}
+
Sign up for hackatime
+
+
+ We sent you an OTP at {email}. Enter it below:
+
+
+
+
+
+ {/if}
+ {#if page == 12}
+
LINK your account to hackatime
+
+ hackatime account set up! β
+
+ if your aunt knew what hackatime was, sheβd be proud!
+
+
+
skipPage(2)}>
+ {/if}
+
+ {#if page == 20}
+
Set Up Hackatime
+
{
+ open('https://hackatime.hackclub.com')
+ }}>
+
+ skipPage(1)}>
+
+
+ {/if}
+ {#if page == 21}
+
Install Hackatime
+
Follow the instructions on the hackatime website to install the hackatime app on your device.
+
+
+ {
+ submitting = true;
+ await checkHackatimeStatus();
+ submitting = false;
+ nextPage();
+ }}>
+
+ {/if}
+ {#if page == 22}
+
Linking Hackatime
+ {#if hasHackatime}
+
+ hackatime account found! β
+
+ if your aunt knew what hackatime was, sheβd be proud!
+
+
skipPage(3)}>
+ {:else}
+
hackatime account not found
+
+
+ {
+ submitting = true;
+ await checkHackatimeStatus();
+ submitting = false;
+ }}>
+ skipPage(10)} color="blue">
+
+ {/if}
+ {/if}
+
+
+
+
+
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 @@
+
+
+
+
e.stopPropagation()} role="none">
+
+
Link a hackatime project
+ {#if page == 0}
+
+ grepping your projects...
+
+
+ {/if}
+ {#if page == 1}
+ {#if selectedHackatimeProjects.length}
+
+ {#each selectedHackatimeProjects as project}
+ removeProject(project)}
+ />
+ {/each}
+
+ {:else}
+
+ {/if}
+
+
+
+
+ {#if error}
+
{error}
+ {/if}
+ {/if}
+ {#if page == 2}
+ {#await hackatimeProjects}
+
getting hackatime projects...
+
+ {:then projects}
+ {#if projects && projects.length}
+
+ {#each projects as project}
+ trackProject(project.name)}
+ />
+ {/each}
+
+ {:else}
+
+
skipPage(1)}
+ />
+ {/if}
+ {/await}
+ {/if}
+
+
+
+
+
+
+
+
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}
+
+
+
+
+{:else}
+
+{/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
+
+
+
+
+
[{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!
+
+
+
+
+
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}
+
+ {/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}
+
+ openHackatimeProjectModal = true}/>
+
+ {:else}
+
+ openHackatimeProjectModal = true}/>
+
+ {/if}
+ {:else}
+
+
openHackatimeAccountModal = true}/>
+
+
+ {/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
+
+
+
+
+
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})
+
+
+ {:else}
+
Not logged in
+ {/if}
+
+ {#if message}
+
{message}
+ {/if}
+
+ {#if error}
+
{error}
+ {/if}
+
+ {#if step === 'login'}
+
+
Authentication
+
+
+
+ {loading ? 'Sending...' : 'Send OTP'}
+
+
+
+ {/if}
+
+ {#if step === 'otp'}
+
+
Verify OTP
+
+
+
+ {loading ? 'Verifying...' : 'Verify OTP'}
+
+
+
+ {/if}
+
+ {#if step === 'profile'}
+
+
Complete Profile
+
+
+
+
+
+ {loading ? 'Creating...' : 'Complete Profile'}
+
+
+
+ {/if}
+
+ {#if step === 'dashboard'}
+
+
Create Project
+
+
+
+ Select Project Type
+ Personal Website
+ Platformer Game
+ Wildcard
+
+
+ {loading ? 'Creating...' : 'Create Project'}
+
+
+
+
+
+
Onboarding Tests
+
+
+ {loading ? 'Testing...' : 'Complete Onboarding'}
+
+
+ {loading ? 'Testing...' : 'Check Status'}
+
+
+
+
+
+
Create User (No Auth Required)
+
+
+
+
+
+ {loading ? 'Creating...' : 'Create User'}
+
+
+
+
+
+
Update User (Auth Required)
+
+
+
+
+
+
+
+
+
+
+
+ {loading ? 'Updating...' : 'Update User'}
+
+
+
+
+
+
+
+
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.
+
+
+ Select Project
+ {#each projects as project}
+ {project.projectTitle}
+ {/each}
+
+
+ {loading ? 'Creating...' : 'Submit Project'}
+
+
+
+
+
+
My Edit Requests
+
+ {loading ? 'Loading...' : 'Load 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SIGN INTO MIDNIGHT
+
+
+
+
+
+ Enter One-Time Passcode
+
+
+
+
+ {#if message}
+
+ {message}
+
+ {/if}
+
+
+
+ {#if isSubmitting}
+
+
+
+
+
+ SUBMITTING...
+
+ {:else}
+ SUBMIT
+ {/if}
+
+
+
+
+
+
+
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