Skip to content

Commit cc2ef3d

Browse files
thedaviddiasclaude
andcommitted
fix: use Convex for desktop auth relay and add terms/privacy pages
Replace in-memory Map in /api/auth/desktop-relay with Convex desktopAuthCodes table — the Map didn't work on Vercel serverless since each request can hit a different instance. Add missing /terms and /privacy pages linked from the login page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fc1fbad commit cc2ef3d

File tree

7 files changed

+333
-23
lines changed

7 files changed

+333
-23
lines changed
Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,7 @@
1+
import { api } from '@/lib/convex-api'
2+
import { fetchMutation } from 'convex/nextjs'
13
import { type NextRequest, NextResponse } from 'next/server'
24

3-
const CODE_TTL_MS = 5 * 60 * 1000 // 5 minutes
4-
5-
const store = new Map<string, { code: string; createdAt: number }>()
6-
7-
function cleanExpired() {
8-
const now = Date.now()
9-
for (const [key, entry] of store) {
10-
if (now - entry.createdAt > CODE_TTL_MS) {
11-
store.delete(key)
12-
}
13-
}
14-
}
15-
165
/**
176
* POST /api/auth/desktop-relay
187
* Stores an OAuth auth code for a desktop session.
@@ -26,8 +15,7 @@ export async function POST(request: NextRequest) {
2615
return NextResponse.json({ error: 'Missing session or code' }, { status: 400 })
2716
}
2817

29-
cleanExpired()
30-
store.set(session, { code, createdAt: Date.now() })
18+
await fetchMutation(api.desktopAuthCodes.store, { session, code })
3119

3220
return NextResponse.json({ ok: true })
3321
}
@@ -44,13 +32,11 @@ export async function GET(request: NextRequest) {
4432
return NextResponse.json({ error: 'Missing session' }, { status: 400 })
4533
}
4634

47-
cleanExpired()
48-
const entry = store.get(session)
35+
const result = await fetchMutation(api.desktopAuthCodes.consume, { session })
4936

50-
if (!entry) {
37+
if (!result) {
5138
return NextResponse.json({ code: null }, { status: 404 })
5239
}
5340

54-
store.delete(session)
55-
return NextResponse.json({ code: entry.code })
41+
return NextResponse.json({ code: result.code })
5642
}

apps/web/app/privacy/page.tsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { Breadcrumb } from '@/components/layout/breadcrumb'
2+
import { PageContainer } from '@/components/layout/page-container'
3+
import { BreadcrumbSchema } from '@/components/seo/json-ld'
4+
import { ROUTES } from '@/lib/routes'
5+
import { createMetadata } from '@/lib/seo'
6+
import Link from 'next/link'
7+
8+
export const metadata = createMetadata({
9+
title: 'Privacy Policy',
10+
description: 'Privacy Policy for souls.directory — how we handle your data.',
11+
path: '/privacy',
12+
noSuffix: true,
13+
})
14+
15+
export default function PrivacyPage() {
16+
return (
17+
<>
18+
<BreadcrumbSchema items={[{ name: 'Privacy Policy', url: '/privacy' }]} />
19+
<main className="min-h-screen">
20+
<PageContainer paddingY="hero">
21+
<Breadcrumb items={[{ name: 'Privacy Policy' }]} className="mb-8" />
22+
23+
<header className="text-center mb-16">
24+
<h1 className="text-2xl md:text-3xl font-medium text-text mb-4">Privacy Policy</h1>
25+
<p className="text-sm text-text-secondary">Last updated: March 16, 2026</p>
26+
</header>
27+
28+
<article className="max-w-2xl mx-auto space-y-10 text-sm text-text-secondary leading-relaxed">
29+
<section>
30+
<h2 className="text-lg font-medium text-text mb-3">What we collect</h2>
31+
<div className="space-y-4">
32+
<div>
33+
<h3 className="text-sm font-medium text-text mb-1">Without an account</h3>
34+
<p>
35+
Privacy-friendly analytics (page views, referrers, browser/OS) via OpenPanel. No
36+
personal data is collected. No cookies are used for tracking.
37+
</p>
38+
</div>
39+
<div>
40+
<h3 className="text-sm font-medium text-text mb-1">With a GitHub account</h3>
41+
<ul className="list-disc list-inside space-y-1">
42+
<li>GitHub username, email, and avatar (from GitHub OAuth)</li>
43+
<li>Souls you create, star, or comment on</li>
44+
<li>Profile information you choose to add (bio, website, social handles)</li>
45+
</ul>
46+
</div>
47+
</div>
48+
</section>
49+
50+
<section>
51+
<h2 className="text-lg font-medium text-text mb-3">How we use it</h2>
52+
<ul className="list-disc list-inside space-y-2">
53+
<li>To provide the service (display your souls, profile, and activity)</li>
54+
<li>To understand how the site is used and improve it</li>
55+
<li>To prevent abuse and enforce our terms</li>
56+
</ul>
57+
</section>
58+
59+
<section>
60+
<h2 className="text-lg font-medium text-text mb-3">What we don&apos;t do</h2>
61+
<ul className="list-disc list-inside space-y-2">
62+
<li>We don&apos;t sell or share your data with third parties</li>
63+
<li>We don&apos;t use your data for advertising</li>
64+
<li>We don&apos;t track you across other websites</li>
65+
</ul>
66+
</section>
67+
68+
<section>
69+
<h2 className="text-lg font-medium text-text mb-3">Where data is stored</h2>
70+
<p>
71+
Data is stored in Convex (database) and Vercel (hosting). Both are US-based
72+
services. Analytics are processed by OpenPanel.
73+
</p>
74+
</section>
75+
76+
<section>
77+
<h2 className="text-lg font-medium text-text mb-3">Data retention</h2>
78+
<p>
79+
Your data is kept as long as your account is active. You can delete your account and
80+
associated data by contacting us on GitHub.
81+
</p>
82+
</section>
83+
84+
<section>
85+
<h2 className="text-lg font-medium text-text mb-3">Third-party services</h2>
86+
<ul className="list-disc list-inside space-y-2">
87+
<li>
88+
<strong className="text-text">GitHub</strong> &mdash; authentication (OAuth)
89+
</li>
90+
<li>
91+
<strong className="text-text">Convex</strong> &mdash; database and backend
92+
</li>
93+
<li>
94+
<strong className="text-text">Vercel</strong> &mdash; hosting and deployment
95+
</li>
96+
<li>
97+
<strong className="text-text">OpenPanel</strong> &mdash; privacy-friendly
98+
analytics
99+
</li>
100+
<li>
101+
<strong className="text-text">Sentry</strong> &mdash; error monitoring
102+
</li>
103+
</ul>
104+
</section>
105+
106+
<section>
107+
<h2 className="text-lg font-medium text-text mb-3">Changes</h2>
108+
<p>
109+
We may update this policy. Significant changes will be noted with an updated date.
110+
</p>
111+
</section>
112+
113+
<section className="pt-6 border-t border-border">
114+
<p>
115+
Questions? Reach out on{' '}
116+
<a
117+
href="https://github.com/thedaviddias/souls-directory"
118+
target="_blank"
119+
rel="noopener noreferrer"
120+
className="text-text hover:underline"
121+
>
122+
GitHub
123+
</a>
124+
. See also our{' '}
125+
<Link href={ROUTES.terms} className="text-text hover:underline">
126+
Terms of Service
127+
</Link>
128+
.
129+
</p>
130+
</section>
131+
</article>
132+
</PageContainer>
133+
</main>
134+
</>
135+
)
136+
}

apps/web/app/terms/page.tsx

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { Breadcrumb } from '@/components/layout/breadcrumb'
2+
import { PageContainer } from '@/components/layout/page-container'
3+
import { BreadcrumbSchema } from '@/components/seo/json-ld'
4+
import { ROUTES } from '@/lib/routes'
5+
import { createMetadata } from '@/lib/seo'
6+
import Link from 'next/link'
7+
8+
export const metadata = createMetadata({
9+
title: 'Terms of Service',
10+
description: 'Terms of Service for souls.directory — rules for using the platform.',
11+
path: '/terms',
12+
noSuffix: true,
13+
})
14+
15+
export default function TermsPage() {
16+
return (
17+
<>
18+
<BreadcrumbSchema items={[{ name: 'Terms of Service', url: '/terms' }]} />
19+
<main className="min-h-screen">
20+
<PageContainer paddingY="hero">
21+
<Breadcrumb items={[{ name: 'Terms of Service' }]} className="mb-8" />
22+
23+
<header className="text-center mb-16">
24+
<h1 className="text-2xl md:text-3xl font-medium text-text mb-4">Terms of Service</h1>
25+
<p className="text-sm text-text-secondary">Last updated: March 16, 2026</p>
26+
</header>
27+
28+
<article className="max-w-2xl mx-auto space-y-10 text-sm text-text-secondary leading-relaxed">
29+
<section>
30+
<h2 className="text-lg font-medium text-text mb-3">1. Acceptance</h2>
31+
<p>
32+
By using souls.directory you agree to these terms. If you don&apos;t agree, please
33+
don&apos;t use the site.
34+
</p>
35+
</section>
36+
37+
<section>
38+
<h2 className="text-lg font-medium text-text mb-3">2. What the service is</h2>
39+
<p>
40+
souls.directory is a community directory for browsing, uploading, and sharing
41+
SOUL.md personality templates. We are not affiliated with OpenClaw.
42+
</p>
43+
</section>
44+
45+
<section>
46+
<h2 className="text-lg font-medium text-text mb-3">3. Accounts</h2>
47+
<p>
48+
You sign in with GitHub OAuth. You&apos;re responsible for your account activity. We
49+
may suspend or remove accounts that violate these terms.
50+
</p>
51+
</section>
52+
53+
<section>
54+
<h2 className="text-lg font-medium text-text mb-3">4. Content you submit</h2>
55+
<ul className="list-disc list-inside space-y-2">
56+
<li>
57+
Souls you publish are licensed under{' '}
58+
<strong className="text-text">MIT License</strong> and credited to you.
59+
</li>
60+
<li>You must have the right to share any content you upload.</li>
61+
<li>
62+
Don&apos;t submit content that is illegal, harmful, harassing, or infringes on
63+
others&apos; rights.
64+
</li>
65+
</ul>
66+
</section>
67+
68+
<section>
69+
<h2 className="text-lg font-medium text-text mb-3">5. Moderation</h2>
70+
<p>
71+
We reserve the right to remove content or accounts that violate these terms, at our
72+
discretion and without notice.
73+
</p>
74+
</section>
75+
76+
<section>
77+
<h2 className="text-lg font-medium text-text mb-3">6. No warranty</h2>
78+
<p>
79+
The service is provided &quot;as is&quot; without warranties of any kind. We
80+
don&apos;t guarantee uptime, accuracy, or fitness for any purpose.
81+
</p>
82+
</section>
83+
84+
<section>
85+
<h2 className="text-lg font-medium text-text mb-3">7. Limitation of liability</h2>
86+
<p>
87+
To the maximum extent permitted by law, souls.directory and its maintainers are not
88+
liable for any damages arising from your use of the service.
89+
</p>
90+
</section>
91+
92+
<section>
93+
<h2 className="text-lg font-medium text-text mb-3">8. Changes</h2>
94+
<p>
95+
We may update these terms. Continued use after changes means you accept the new
96+
terms.
97+
</p>
98+
</section>
99+
100+
<section className="pt-6 border-t border-border">
101+
<p>
102+
Questions? Reach out on{' '}
103+
<a
104+
href="https://github.com/thedaviddias/souls-directory"
105+
target="_blank"
106+
rel="noopener noreferrer"
107+
className="text-text hover:underline"
108+
>
109+
GitHub
110+
</a>
111+
. See also our{' '}
112+
<Link href={ROUTES.privacy} className="text-text hover:underline">
113+
Privacy Policy
114+
</Link>
115+
.
116+
</p>
117+
</section>
118+
</article>
119+
</PageContainer>
120+
</main>
121+
</>
122+
)
123+
}

apps/web/components/auth/desktop-auth-relay.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,7 @@ export function DesktopAuthRelay() {
9393
<p className="text-lg font-medium text-text">Returning to Soul Studio...</p>
9494

9595
{!showFallback ? (
96-
<p className="text-sm text-text-secondary">
97-
Opening the desktop app...
98-
</p>
96+
<p className="text-sm text-text-secondary">Opening the desktop app...</p>
9997
) : (
10098
<div className="space-y-3">
10199
<p className="text-sm text-text-secondary">

apps/web/convex/_generated/api.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type * as collections from "../collections.js";
1414
import type * as comments from "../comments.js";
1515
import type * as crons from "../crons.js";
1616
import type * as debug from "../debug.js";
17+
import type * as desktopAuthCodes from "../desktopAuthCodes.js";
1718
import type * as http from "../http.js";
1819
import type * as lib_access from "../lib/access.js";
1920
import type * as lib_sanitizeSoulContent from "../lib/sanitizeSoulContent.js";
@@ -42,6 +43,7 @@ declare const fullApi: ApiFromModules<{
4243
comments: typeof comments;
4344
crons: typeof crons;
4445
debug: typeof debug;
46+
desktopAuthCodes: typeof desktopAuthCodes;
4547
http: typeof http;
4648
"lib/access": typeof lib_access;
4749
"lib/sanitizeSoulContent": typeof lib_sanitizeSoulContent;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { v } from 'convex/values'
2+
import { mutation } from './_generated/server'
3+
4+
const CODE_TTL_MS = 5 * 60 * 1000 // 5 minutes
5+
6+
/**
7+
* Store an OAuth auth code for a desktop session.
8+
* Replaces any existing entry for the same session (handles retries).
9+
*/
10+
export const store = mutation({
11+
args: {
12+
session: v.string(),
13+
code: v.string(),
14+
},
15+
handler: async (ctx, args) => {
16+
const existing = await ctx.db
17+
.query('desktopAuthCodes')
18+
.withIndex('by_session', (q) => q.eq('session', args.session))
19+
.first()
20+
if (existing) {
21+
await ctx.db.delete(existing._id)
22+
}
23+
await ctx.db.insert('desktopAuthCodes', {
24+
session: args.session,
25+
code: args.code,
26+
createdAt: Date.now(),
27+
})
28+
},
29+
})
30+
31+
/**
32+
* Retrieve and delete the stored auth code (one-time retrieval).
33+
* Returns null if not found or expired.
34+
*/
35+
export const consume = mutation({
36+
args: {
37+
session: v.string(),
38+
},
39+
handler: async (ctx, args) => {
40+
const entry = await ctx.db
41+
.query('desktopAuthCodes')
42+
.withIndex('by_session', (q) => q.eq('session', args.session))
43+
.first()
44+
45+
if (!entry) return null
46+
47+
// Check TTL
48+
if (Date.now() - entry.createdAt > CODE_TTL_MS) {
49+
await ctx.db.delete(entry._id)
50+
return null
51+
}
52+
53+
// One-time retrieval: delete after reading
54+
await ctx.db.delete(entry._id)
55+
return { code: entry.code }
56+
},
57+
})

0 commit comments

Comments
 (0)