mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 01:46:43 +02:00
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:
parent
b97149ac12
commit
61d181fbc2
3148 changed files with 437 additions and 46640 deletions
|
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
|
@ -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(),
|
||||
});
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -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}`);
|
||||
};
|
||||
88
apps-archived/uload/apps/web/src/routes/api/vote/+server.ts
Normal file
88
apps-archived/uload/apps/web/src/routes/api/vote/+server.ts
Normal 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 });
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue