chore: archive inactive projects to apps-archived/

Move inactive projects out of active workspace:
- bauntown (community website)
- maerchenzauber (AI story generation)
- memoro (voice memo app)
- news (news aggregation)
- nutriphi (nutrition tracking)
- reader (reading app)
- uload (URL shortener)
- wisekeep (AI wisdom extraction)

Update CLAUDE.md documentation:
- Add presi to active projects
- Document archived projects section
- Update workspace configuration

Archived apps can be re-activated by moving back to apps/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-29 07:03:59 +01:00
parent b97149ac12
commit 61d181fbc2
3148 changed files with 437 additions and 46640 deletions

View file

@ -0,0 +1,45 @@
import { json } from '@sveltejs/kit';
import { validateUsername } from '$lib/username';
import type { RequestHandler } from './$types';
import { users } from '$lib/db/schema';
import { eq } from 'drizzle-orm';
export const GET: RequestHandler = async ({ url, locals }) => {
const username = url.searchParams.get('username');
if (!username) {
return json({ available: false, error: 'Username required' });
}
// Validate format first
const validation = validateUsername(username);
if (!validation.valid) {
return json({ available: false, error: validation.error });
}
try {
// Try to find a user with this username using Drizzle ORM
const [existingUser] = await locals.db
.select()
.from(users)
.where(eq(users.username, username))
.limit(1);
// If no user found, username is available
if (!existingUser) {
return json({ available: true });
}
// Check if it's the current user (they're checking their temp username)
if (locals.user && existingUser.id === locals.user.id) {
// It's their own temporary username, so it's "available" for them
return json({ available: true });
}
// Username taken by someone else
return json({ available: false });
} catch (err) {
console.error('Error checking username:', err);
return json({ available: false, error: 'Database error' }, { status: 500 });
}
};

View file

@ -0,0 +1,10 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async () => {
return json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
});
};

View file

@ -0,0 +1,42 @@
import { json } from '@sveltejs/kit';
import { redis, ensureRedisConnection, redisAvailable } from '$lib/server/redis';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async () => {
const status = {
connected: false,
host: process.env.REDIS_HOST || 'not configured',
enabled: !!redis,
available: redisAvailable,
cachedLinks: 0,
error: null as string | null,
};
try {
// Try to connect
const connected = await ensureRedisConnection();
status.connected = connected;
if (connected && redis) {
// Count cached redirects
const keys = await redis.keys('redirect:*');
status.cachedLinks = keys.length;
// Test basic operation
await redis.setex('test:ping', 10, 'pong');
const test = await redis.get('test:ping');
if (test !== 'pong') {
status.error = 'Read/write test failed';
}
} else if (!redis) {
status.error = 'Redis is not configured (check environment variables)';
} else {
status.error = 'Could not establish connection';
}
} catch (error: any) {
status.error = error.message;
status.connected = false;
}
return json(status);
};

View file

@ -0,0 +1,123 @@
import { json } from '@sveltejs/kit';
import { getStripe } from '$lib/server/stripe';
import { env } from '$env/dynamic/private';
import type { RequestHandler } from './$types';
// Map our locale codes to Stripe's expected format
function mapLocaleToStripe(locale: string): any {
const stripeLocales: Record<string, any> = {
en: 'en',
de: 'de',
it: 'it',
fr: 'fr',
es: 'es',
};
return stripeLocales[locale] || 'auto';
}
export const POST: RequestHandler = async ({ request, locals, url }) => {
try {
// Check if user is authenticated
if (!locals.pb.authStore.isValid || !locals.user) {
return json({ error: 'Bitte erst einloggen' }, { status: 401 });
}
const user = locals.user;
const { priceType = 'monthly', locale = 'en' } = await request.json();
// Check if user already has active subscription
if (user.subscription_status === 'pro') {
return json({ error: 'Du hast bereits ein aktives Abo' }, { status: 400 });
}
// Select the correct price ID
let priceId: string;
let mode: 'subscription' | 'payment' = 'subscription';
switch (priceType) {
case 'yearly':
priceId = env.STRIPE_PRICE_YEARLY;
break;
case 'lifetime':
priceId = env.STRIPE_PRICE_LIFETIME;
mode = 'payment'; // One-time payment for lifetime
break;
case 'monthly':
default:
priceId = env.STRIPE_PRICE_MONTHLY;
break;
}
if (!priceId) {
throw new Error(`Price ID not found for type: ${priceType}`);
}
// Initialize Stripe
const stripe = getStripe();
// Create or get Stripe customer
let stripeCustomerId = user.stripe_customer_id;
if (!stripeCustomerId) {
const customer = await stripe.customers.create({
email: user.email,
name: user.name || undefined,
metadata: {
pocketbase_id: user.id,
username: user.username || '',
},
});
stripeCustomerId = customer.id;
// Save customer ID to PocketBase
await locals.pb.collection('users').update(user.id, {
stripe_customer_id: stripeCustomerId,
});
}
// Create Stripe Checkout session
const session = await stripe.checkout.sessions.create({
customer: stripeCustomerId,
payment_method_types: ['card', 'sepa_debit'],
billing_address_collection: 'required',
line_items: [
{
price: priceId,
quantity: 1,
},
],
mode,
allow_promotion_codes: true,
success_url: `${url.origin}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${url.origin}/pricing?cancelled=true`,
locale: mapLocaleToStripe(locale),
metadata: {
user_id: user.id,
user_email: user.email,
price_type: priceType,
},
...(mode === 'subscription' && {
subscription_data: {
metadata: {
pocketbase_user_id: user.id,
},
},
}),
});
return json({
sessionId: session.id,
url: session.url,
});
} catch (error) {
console.error('Stripe checkout error:', error);
return json(
{
error:
error instanceof Error ? error.message : 'Fehler beim Erstellen der Checkout-Session',
},
{ status: 500 }
);
}
};

View file

@ -0,0 +1,244 @@
import { getStripe } from '$lib/server/stripe';
import { env } from '$env/dynamic/private';
import { env as publicEnv } from '$env/dynamic/public';
import type { RequestHandler } from './$types';
import PocketBase from 'pocketbase';
export const POST: RequestHandler = async ({ request, locals }) => {
const body = await request.text();
const signature = request.headers.get('stripe-signature');
// Initialize Stripe
const stripe = getStripe();
const STRIPE_WEBHOOK_SECRET = env.STRIPE_WEBHOOK_SECRET;
// For development without webhook secret
if (!STRIPE_WEBHOOK_SECRET || STRIPE_WEBHOOK_SECRET === 'undefined') {
console.warn('⚠️ No webhook secret configured - running in test mode');
const event = JSON.parse(body);
return handleWebhookEvent(event, locals);
}
if (!signature) {
return new Response('No signature', { status: 400 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, STRIPE_WEBHOOK_SECRET);
} catch (err: any) {
// Webhook signature verification failed
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
return handleWebhookEvent(event, locals);
};
async function handleWebhookEvent(event: any, locals?: any) {
// Create admin PocketBase client for webhooks
const pb = new PocketBase(publicEnv.PUBLIC_POCKETBASE_URL);
// Admin auth for updating users
try {
if (!env.POCKETBASE_ADMIN_EMAIL || !env.POCKETBASE_ADMIN_PASSWORD) {
throw new Error(
'Admin credentials not configured. Please set POCKETBASE_ADMIN_EMAIL and POCKETBASE_ADMIN_PASSWORD environment variables.'
);
}
await pb.admins.authWithPassword(env.POCKETBASE_ADMIN_EMAIL, env.POCKETBASE_ADMIN_PASSWORD);
// Admin authenticated successfully
} catch (error) {
// Admin authentication failed
// Return error response for missing credentials
return new Response(
JSON.stringify({
error: 'Webhook processing failed due to configuration error',
details: 'Admin authentication failed. Please check server configuration.',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
// Checkout completed
const userId = session.metadata?.user_id;
// Processing user_id from metadata
if (!userId) {
// No user_id in session metadata
break;
}
const priceType = session.metadata?.price_type || 'monthly';
// Handle lifetime purchase differently
if (priceType === 'lifetime') {
await pb.collection('users').update(userId, {
subscription_status: 'pro',
stripe_customer_id: session.customer,
stripe_subscription_id: 'lifetime_' + session.id,
current_period_end: new Date('2099-12-31').toISOString(), // Far future date
links_created_this_month: 0,
});
// User purchased lifetime access
} else {
// Handle subscription
if (session.subscription) {
const stripe = getStripe();
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
);
await pb.collection('users').update(userId, {
subscription_status: 'pro',
stripe_customer_id: session.customer,
stripe_subscription_id: subscription.id,
current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
links_created_this_month: 0,
});
// User upgraded to Pro
}
}
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object;
// Subscription updated
// Get user by stripe_subscription_id
try {
const users = await pb.collection('users').getList(1, 1, {
filter: `stripe_subscription_id = "${subscription.id}"`,
});
if (users.items.length > 0) {
const user = users.items[0];
// Map Stripe status to our status
let status = 'free';
switch (subscription.status) {
case 'active':
status = 'pro';
break;
case 'past_due':
status = 'past_due';
break;
case 'canceled':
case 'unpaid':
status = 'cancelled';
break;
}
await pb.collection('users').update(user.id, {
subscription_status: status,
current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
});
// User subscription status updated
}
} catch (error) {
// Error finding user for subscription
}
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object;
// Subscription cancelled
try {
const users = await pb.collection('users').getList(1, 1, {
filter: `stripe_subscription_id = "${subscription.id}"`,
});
if (users.items.length > 0) {
const user = users.items[0];
await pb.collection('users').update(user.id, {
subscription_status: 'free',
stripe_subscription_id: null,
current_period_end: null,
});
// User downgraded to Free
}
} catch (error) {
// Error finding user for cancelled subscription
}
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object;
// Payment failed
if (invoice.subscription) {
try {
const users = await pb.collection('users').getList(1, 1, {
filter: `stripe_subscription_id = "${invoice.subscription}"`,
});
if (users.items.length > 0) {
const user = users.items[0];
await pb.collection('users').update(user.id, {
subscription_status: 'past_due',
});
// User payment failed - marked as past_due
}
} catch (error) {
// Error handling payment failure
}
}
break;
}
case 'invoice.payment_succeeded': {
const invoice = event.data.object;
// Payment succeeded
if (invoice.subscription) {
try {
const users = await pb.collection('users').getList(1, 1, {
filter: `stripe_subscription_id = "${invoice.subscription}"`,
});
if (users.items.length > 0) {
const user = users.items[0];
// Reactivate if was past_due
if (user.subscription_status === 'past_due') {
await pb.collection('users').update(user.id, {
subscription_status: 'pro',
});
// User reactivated after payment
}
}
} catch (error) {
// Error handling payment success
}
}
break;
}
default:
// Unhandled event type
}
return new Response('Webhook processed', { status: 200 });
} catch (error) {
// Webhook processing error
return new Response('Webhook processing failed', { status: 500 });
} finally {
pb.authStore.clear();
}
}

View file

@ -0,0 +1,42 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ locals }) => {
try {
console.log('[TEST-PB] Testing PocketBase connection');
console.log('[TEST-PB] PocketBase URL:', locals.pb?.baseUrl);
console.log('[TEST-PB] PocketBase instance exists:', !!locals.pb);
// Try to fetch health status
const healthCheck = await fetch(`${locals.pb.baseUrl}/api/health`);
const healthData = await healthCheck.json();
console.log('[TEST-PB] Health check response:', healthData);
// Try to list collections (public endpoint)
try {
const collections = await locals.pb.collections.getList(1, 1);
console.log('[TEST-PB] Can access collections:', !!collections);
} catch (e) {
console.log('[TEST-PB] Cannot access collections (might be normal):', e.message);
}
return json({
success: true,
pocketbaseUrl: locals.pb?.baseUrl,
health: healthData,
timestamp: new Date().toISOString(),
});
} catch (error: any) {
console.error('[TEST-PB] Error:', error);
return json(
{
success: false,
error: error.message,
pocketbaseUrl: locals.pb?.baseUrl,
timestamp: new Date().toISOString(),
},
{ status: 500 }
);
}
};

View file

@ -0,0 +1,21 @@
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { env } from '$env/dynamic/public';
import { dev } from '$app/environment';
// Alternative verification endpoint that redirects to PocketBase's built-in verification
export const GET: RequestHandler = async ({ url }) => {
const token = url.searchParams.get('token');
if (!token) {
// No token - redirect to login with error
redirect(303, '/login?error=missing-token');
}
// Get the correct PocketBase URL based on environment
const pbUrl = env.PUBLIC_POCKETBASE_URL || (dev ? 'http://localhost:8090' : 'https://pb.ulo.ad');
// Redirect to PocketBase's built-in verification endpoint
// PocketBase will handle the verification and show its own success/error page
redirect(303, `${pbUrl}/_/#/auth/confirm-verification/${token}`);
};

View file

@ -0,0 +1,88 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { pb } from '$lib/pocketbase';
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const { featureRequestId, action } = await request.json();
if (!featureRequestId || !action) {
return json({ error: 'Invalid request' }, { status: 400 });
}
try {
// Use admin client for updating vote counts
const adminPb = new (await import('pocketbase')).default(pb.baseUrl);
// Try to authenticate as admin using environment variables
try {
await adminPb.admins.authWithPassword(
process.env.POCKETBASE_ADMIN_EMAIL || 'admin@example.com',
process.env.POCKETBASE_ADMIN_PASSWORD || 'admin123456'
);
} catch (authError) {
console.error('Admin auth failed, trying alternative approach');
// If admin auth fails, we'll use the regular pb instance
}
if (action === 'add') {
// Check if vote already exists
const existingVotes = await pb.collection('featurevotes').getList(1, 1, {
filter: `user_id = "${locals.user.id}" && feature_request_id = "${featureRequestId}"`,
});
if (existingVotes.items.length === 0) {
// Create vote
await pb.collection('featurevotes').create({
user_id: locals.user.id,
feature_request_id: featureRequestId,
});
// Get current vote count and increment
const featureRequest = await pb.collection('featurerequests').getOne(featureRequestId);
const newCount = (featureRequest.vote_count || 0) + 1;
// Update using admin client if available, otherwise try regular
const client = adminPb.authStore.isValid ? adminPb : pb;
await client.collection('featurerequests').update(featureRequestId, {
vote_count: newCount,
});
return json({ success: true, voteCount: newCount });
}
return json({ success: false, message: 'Already voted' });
} else if (action === 'remove') {
// Find and delete vote
const existingVotes = await pb.collection('featurevotes').getList(1, 1, {
filter: `user_id = "${locals.user.id}" && feature_request_id = "${featureRequestId}"`,
});
if (existingVotes.items.length > 0) {
await pb.collection('featurevotes').delete(existingVotes.items[0].id);
// Get current vote count and decrement
const featureRequest = await pb.collection('featurerequests').getOne(featureRequestId);
const newCount = Math.max(0, (featureRequest.vote_count || 0) - 1);
// Update using admin client if available, otherwise try regular
const client = adminPb.authStore.isValid ? adminPb : pb;
await client.collection('featurerequests').update(featureRequestId, {
vote_count: newCount,
});
return json({ success: true, voteCount: newCount });
}
return json({ success: false, message: 'No vote found' });
}
return json({ error: 'Invalid action' }, { status: 400 });
} catch (error) {
console.error('Vote error:', error);
return json({ error: 'Internal server error' }, { status: 500 });
}
};