Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

init supporter

+97 -17
+10 -3
apps/main-app/public/editor/editor.tsx
··· 400 400 </p> 401 401 </div> 402 402 <div className="flex items-center gap-3"> 403 - <span className="text-sm text-muted-foreground"> 404 - {userInfo?.handle || 'Loading...'} 405 - </span> 403 + <div className="flex items-center gap-2"> 404 + <span className="text-sm text-muted-foreground"> 405 + {userInfo?.handle || 'Loading...'} 406 + </span> 407 + {userInfo?.isSupporter && ( 408 + <Badge variant="default" className="text-xs"> 409 + Supporter 410 + </Badge> 411 + )} 412 + </div> 406 413 <Button 407 414 variant="ghost" 408 415 size="sm"
+1 -1
apps/main-app/public/editor/hooks/useDomainData.ts
··· 196 196 197 197 // Handle domain limit error more gracefully 198 198 if (errorMessage.includes('Domain limit reached')) { 199 - alert('You have already claimed 3 wisp.place subdomains (maximum limit).') 199 + alert('You have already claimed 3 wisp.place subdomains (maximum limit). Supporters get unlimited subdomains!') 200 200 await fetchDomains() 201 201 } else { 202 202 alert(`Failed to claim domain: ${errorMessage}`)
+1
apps/main-app/public/editor/hooks/useUserInfo.ts
··· 3 3 export interface UserInfo { 4 4 did: string 5 5 handle: string 6 + isSupporter: boolean 6 7 } 7 8 8 9 export function useUserInfo() {
+7 -3
apps/main-app/public/editor/tabs/DomainsTab.tsx
··· 145 145 <CardHeader> 146 146 <CardTitle>wisp.place Subdomains</CardTitle> 147 147 <CardDescription> 148 - Your free subdomains on the wisp.place network (up to 3) 148 + {userInfo?.isSupporter 149 + ? 'Your free subdomains on the wisp.place network (unlimited as a supporter)' 150 + : 'Your free subdomains on the wisp.place network (up to 3)'} 149 151 </CardDescription> 150 152 </CardHeader> 151 153 <CardContent> ··· 211 213 </div> 212 214 )} 213 215 214 - {wispDomains.length < 3 && ( 216 + {(wispDomains.length < 3 || userInfo?.isSupporter) && ( 215 217 <div className="p-4 bg-muted/30 rounded-lg"> 216 218 <p className="text-sm text-muted-foreground mb-4"> 217 219 {wispDomains.length === 0 218 220 ? 'Claim your free wisp.place subdomain' 221 + : userInfo?.isSupporter 222 + ? `Claim another wisp.place subdomain (${wispDomains.length} claimed)` 219 223 : `Claim another wisp.place subdomain (${wispDomains.length}/3)`} 220 224 </p> 221 225 <div className="space-y-3"> ··· 280 284 </div> 281 285 )} 282 286 283 - {wispDomains.length === 3 && ( 287 + {wispDomains.length === 3 && !userInfo?.isSupporter && ( 284 288 <div className="p-3 bg-muted/30 rounded-lg text-center"> 285 289 <p className="text-sm text-muted-foreground"> 286 290 You have claimed the maximum of 3 wisp.place subdomains
+12 -1
apps/main-app/src/index.ts
··· 248 248 return await Bun.file('./apps/main-app/public/editor/onboarding.html').text() 249 249 }) 250 250 .get('/oauth-client-metadata.json', () => { 251 - return createClientMetadata(config) 251 + logger.debug('[OAuth] Client metadata requested', { 252 + LOCAL_DEV: Bun.env.LOCAL_DEV, 253 + DOMAIN: Bun.env.DOMAIN, 254 + BASE_DOMAIN: Bun.env.BASE_DOMAIN, 255 + configDomain: config.domain 256 + }) 257 + const metadata = createClientMetadata(config) 258 + logger.debug('[OAuth] Returning metadata', { 259 + client_id: metadata.client_id, 260 + redirect_uris: metadata.redirect_uris 261 + }) 262 + return metadata 252 263 }) 253 264 .get('/jwks.json', async ({ set }) => { 254 265 // Prevent caching to ensure clients always get fresh keys after rotation
+45 -4
apps/main-app/src/lib/db.ts
··· 153 153 ) 154 154 `; 155 155 156 + // Supporter table - list of supporter DIDs 157 + await db` 158 + CREATE TABLE IF NOT EXISTS supporter ( 159 + did TEXT PRIMARY KEY, 160 + created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) 161 + ) 162 + `; 163 + 164 + // Insert initial supporter 165 + await db` 166 + INSERT INTO supporter (did) 167 + VALUES ('did:plc:ttdrpj45ibqunmfhdsb4zdwq') 168 + ON CONFLICT (did) DO NOTHING 169 + `; 170 + 156 171 // Create indexes for common query patterns 157 172 await Promise.all([ 158 173 // oauth_states cleanup queries ··· 328 343 const h = handle.trim().toLowerCase(); 329 344 if (!isValidHandle(h)) throw new Error('invalid_handle'); 330 345 331 - // Check if user already has 3 domains 332 - const existingCount = await countWispDomains(did); 333 - if (existingCount >= 3) { 334 - throw new Error('domain_limit_reached'); 346 + // Check if user already has 3 domains (unless they're a supporter) 347 + const supporter = await isSupporter(did); 348 + if (!supporter) { 349 + const existingCount = await countWispDomains(did); 350 + if (existingCount >= 3) { 351 + throw new Error('domain_limit_reached'); 352 + } 335 353 } 336 354 337 355 const domain = toDomain(h); ··· 570 588 571 589 console.log('[CookieSecret] Generated new cookie signing secret'); 572 590 return secret; 591 + }; 592 + 593 + // Supporter management functions 594 + export const isSupporter = async (did: string): Promise<boolean> => { 595 + const rows = await db`SELECT 1 FROM supporter WHERE did = ${did} LIMIT 1`; 596 + return rows.length > 0; 597 + }; 598 + 599 + export const addSupporter = async (did: string): Promise<void> => { 600 + await db` 601 + INSERT INTO supporter (did) 602 + VALUES (${did}) 603 + ON CONFLICT (did) DO NOTHING 604 + `; 605 + }; 606 + 607 + export const removeSupporter = async (did: string): Promise<void> => { 608 + await db`DELETE FROM supporter WHERE did = ${did}`; 609 + }; 610 + 611 + export const getAllSupporters = async () => { 612 + const rows = await db`SELECT * FROM supporter ORDER BY created_at ASC`; 613 + return rows; 573 614 }; 574 615 575 616 /**
+11 -4
apps/main-app/src/routes/user.ts
··· 1 1 import { Elysia, t } from 'elysia' 2 2 import { requireAuth } from '../lib/wisp-auth' 3 3 import { NodeOAuthClient } from '@atproto/oauth-client-node' 4 - import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo, getDomainsBySite, getAllWispDomains } from '../lib/db' 4 + import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo, getDomainsBySite, getAllWispDomains, isSupporter } from '../lib/db' 5 5 import { syncSitesFromPDS } from '../lib/sync-sites' 6 6 import { createLogger } from '@wispplace/observability' 7 7 import { getHandleForDid } from '@wispplace/atproto-utils' ··· 46 46 }) 47 47 /** 48 48 * GET /api/user/info 49 - * Success: { did, handle } 49 + * Success: { did, handle, isSupporter } 50 50 */ 51 51 .get('/info', async ({ auth }) => { 52 52 try { ··· 60 60 logger.error('[User] Failed to resolve DID', err) 61 61 } 62 62 63 - return { 63 + // Check if user is a supporter 64 + const supporter = await isSupporter(auth.did) 65 + logger.debug('[User] isSupporter check', { did: auth.did, supporter }) 66 + 67 + const response = { 64 68 did: auth.did, 65 - handle 69 + handle, 70 + isSupporter: supporter 66 71 } 72 + logger.debug('[User] Returning info', response) 73 + return response 67 74 } catch (err) { 68 75 logger.error('[User] Info error', err) 69 76 throw new Error('Failed to get user info')
+2 -1
packages/@wispplace/database/src/index.ts
··· 20 20 CookieSecret, 21 21 AdminUser, 22 22 SiteCache, 23 - SiteSettingsCache 23 + SiteSettingsCache, 24 + Supporter 24 25 } from './types';
+8
packages/@wispplace/database/src/types.ts
··· 84 84 cached_at: number; 85 85 updated_at: number; 86 86 } 87 + 88 + /** 89 + * Supporter - list of supporter DIDs 90 + */ 91 + export interface Supporter { 92 + did: string; 93 + created_at?: number; 94 + }