mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 06:21:09 +02:00
refactor: restructure
monorepo with apps/ and services/ directories
This commit is contained in:
parent
25824ed0ac
commit
ff80aeec1f
4062 changed files with 2592 additions and 1278 deletions
44
apps/uload/scripts/apply-db-optimizations.sh
Executable file
44
apps/uload/scripts/apply-db-optimizations.sh
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
#!/bin/bash
|
||||
|
||||
# ULoad Database Optimization Script
|
||||
# Applies performance optimizations to the SQLite database
|
||||
|
||||
echo "🚀 Applying database optimizations..."
|
||||
|
||||
# Backup current database
|
||||
echo "📦 Creating backup..."
|
||||
cp backend/pb_data/data.db backend/pb_data/data.db.backup.$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
# Apply optimizations to local database
|
||||
echo "⚡ Applying optimizations to local database..."
|
||||
sqlite3 backend/pb_data/data.db < scripts/optimize-database.sql
|
||||
|
||||
# Check if production database optimization is needed
|
||||
if [ "$1" = "--production" ]; then
|
||||
echo "🌐 Production mode detected"
|
||||
echo "⚠️ Manual production database optimization required"
|
||||
echo " Run this SQL script on your production PocketBase:"
|
||||
echo " cat scripts/optimize-database.sql"
|
||||
fi
|
||||
|
||||
echo "✅ Database optimizations applied successfully!"
|
||||
echo "📊 Database size after optimization:"
|
||||
ls -lh backend/pb_data/data.db
|
||||
|
||||
echo ""
|
||||
echo "🔍 Performance improvements applied:"
|
||||
echo " • WAL mode enabled for better concurrency"
|
||||
echo " • Cache size optimized to 8MB"
|
||||
echo " • Memory-mapped I/O enabled"
|
||||
echo " • Missing indexes created for:"
|
||||
echo " - Links by user and active status"
|
||||
echo " - Analytics by link and date"
|
||||
echo " - Composite indexes for dashboard queries"
|
||||
echo " • Statistics updated with ANALYZE"
|
||||
|
||||
echo ""
|
||||
echo "🎯 Expected performance improvements:"
|
||||
echo " • 50-80% faster link lookups"
|
||||
echo " • 60-90% faster analytics queries"
|
||||
echo " • Better concurrent access performance"
|
||||
echo " • Faster dashboard loading"
|
||||
46
apps/uload/scripts/check-prod-redis.sh
Executable file
46
apps/uload/scripts/check-prod-redis.sh
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "🔍 Checking Redis Cache in Production"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Your production domain
|
||||
DOMAIN="https://ulo.ad"
|
||||
|
||||
# Test link (create one if needed)
|
||||
TEST_CODE="test-redis-$(date +%s)"
|
||||
TEST_URL="https://example.com/redis-test"
|
||||
|
||||
echo "1. Creating a test link via API..."
|
||||
# You would need to create a test link first via your admin panel or API
|
||||
|
||||
echo ""
|
||||
echo "2. Testing redirect performance..."
|
||||
echo ""
|
||||
|
||||
# First request (should be cache MISS)
|
||||
echo "First request (expected: CACHE MISS):"
|
||||
time curl -I -s -o /dev/null -w "HTTP Status: %{http_code}\nTime: %{time_total}s\nRedirect: %{redirect_url}\n" "$DOMAIN/$TEST_CODE"
|
||||
|
||||
echo ""
|
||||
echo "Waiting 1 second..."
|
||||
sleep 1
|
||||
|
||||
# Second request (should be cache HIT)
|
||||
echo ""
|
||||
echo "Second request (expected: CACHE HIT):"
|
||||
time curl -I -s -o /dev/null -w "HTTP Status: %{http_code}\nTime: %{time_total}s\nRedirect: %{redirect_url}\n" "$DOMAIN/$TEST_CODE"
|
||||
|
||||
echo ""
|
||||
echo "Third request (should be cache HIT):"
|
||||
time curl -I -s -o /dev/null -w "HTTP Status: %{http_code}\nTime: %{time_total}s\nRedirect: %{redirect_url}\n" "$DOMAIN/$TEST_CODE"
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "✅ If the 2nd and 3rd requests are faster, cache is working!"
|
||||
echo ""
|
||||
echo "Tips for verification:"
|
||||
echo "- Check your server logs for 'Cache HIT!' messages"
|
||||
echo "- First visit should show 'Cache MISS'"
|
||||
echo "- Subsequent visits should show 'Cache HIT!'"
|
||||
echo "- Cache TTL: 5 min (normal) or 24h (popular links)"
|
||||
12
apps/uload/scripts/create-admin.sh
Normal file
12
apps/uload/scripts/create-admin.sh
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "Creating PocketBase admin account..."
|
||||
|
||||
cd backend
|
||||
|
||||
# Use expect or echo to provide input
|
||||
echo -e "till.schneider@memoro.ai\np0ck3t-RA1N\np0ck3t-RA1N" | ./pocketbase superuser create
|
||||
|
||||
echo "Admin account created!"
|
||||
echo "Email: till.schneider@memoro.ai"
|
||||
echo "Password: p0ck3t-RA1N"
|
||||
425
apps/uload/scripts/create-collections.mjs
Normal file
425
apps/uload/scripts/create-collections.mjs
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Creates PocketBase collections via API
|
||||
* Run: node scripts/create-collections.mjs
|
||||
*/
|
||||
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('http://localhost:8090');
|
||||
|
||||
// Admin credentials
|
||||
const ADMIN_EMAIL = 'till.schneider@memoro.ai';
|
||||
const ADMIN_PASSWORD = 'p0ck3t-RAJ';
|
||||
|
||||
async function createCollections() {
|
||||
console.log('🔧 Creating PocketBase Collections...\n');
|
||||
|
||||
try {
|
||||
// 1. Authenticate as admin
|
||||
console.log('🔐 Authenticating as admin...');
|
||||
await pb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
|
||||
console.log('✅ Authenticated\n');
|
||||
|
||||
// 2. Create Links Collection
|
||||
console.log('📦 Creating Links collection...');
|
||||
try {
|
||||
await pb.collections.create({
|
||||
name: 'links',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{
|
||||
name: 'short_code',
|
||||
type: 'text',
|
||||
required: true,
|
||||
options: {
|
||||
min: 3,
|
||||
max: 50,
|
||||
pattern: '^[a-zA-Z0-9_/-]+$'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'custom_code',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'original_url',
|
||||
type: 'url',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: false,
|
||||
options: {
|
||||
max: 200
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
required: false,
|
||||
options: {
|
||||
max: 500
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user_id',
|
||||
type: 'relation',
|
||||
required: false,
|
||||
options: {
|
||||
collectionId: '_pb_users_auth_',
|
||||
cascadeDelete: true,
|
||||
maxSelect: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'bool',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'max_clicks',
|
||||
type: 'number',
|
||||
required: false,
|
||||
options: {
|
||||
min: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'expires_at',
|
||||
type: 'date',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'click_count',
|
||||
type: 'number',
|
||||
required: false,
|
||||
options: {
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'qr_code',
|
||||
type: 'file',
|
||||
required: false,
|
||||
options: {
|
||||
maxSelect: 1,
|
||||
maxSize: 5242880
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'json',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'utm_source',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'utm_medium',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'utm_campaign',
|
||||
type: 'text',
|
||||
required: false
|
||||
}
|
||||
],
|
||||
indexes: [
|
||||
'CREATE UNIQUE INDEX idx_short_code ON links (short_code)'
|
||||
],
|
||||
listRule: '',
|
||||
viewRule: '',
|
||||
createRule: '@request.auth.id != ""',
|
||||
updateRule: '@request.auth.id = user_id',
|
||||
deleteRule: '@request.auth.id = user_id'
|
||||
});
|
||||
console.log('✅ Links collection created');
|
||||
} catch (e) {
|
||||
if (e.response?.message?.includes('already exists')) {
|
||||
console.log('⚠️ Links collection already exists');
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Create Clicks Collection
|
||||
console.log('📦 Creating Clicks collection...');
|
||||
try {
|
||||
await pb.collections.create({
|
||||
name: 'clicks',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{
|
||||
name: 'link_id',
|
||||
type: 'relation',
|
||||
required: true,
|
||||
options: {
|
||||
collectionId: 'links',
|
||||
cascadeDelete: true,
|
||||
maxSelect: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'ip_hash',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'user_agent',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'referer',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'browser',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'device_type',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'os',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'country',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'city',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'clicked_at',
|
||||
type: 'date',
|
||||
required: false
|
||||
}
|
||||
],
|
||||
listRule: '',
|
||||
viewRule: '',
|
||||
createRule: '',
|
||||
updateRule: null,
|
||||
deleteRule: '@request.auth.id = link_id.user_id'
|
||||
});
|
||||
console.log('✅ Clicks collection created');
|
||||
} catch (e) {
|
||||
if (e.response?.message?.includes('already exists')) {
|
||||
console.log('⚠️ Clicks collection already exists');
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Create Accounts Collection
|
||||
console.log('📦 Creating Accounts collection...');
|
||||
try {
|
||||
await pb.collections.create({
|
||||
name: 'accounts',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'owner',
|
||||
type: 'relation',
|
||||
required: true,
|
||||
options: {
|
||||
collectionId: '_pb_users_auth_',
|
||||
cascadeDelete: true,
|
||||
maxSelect: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'members',
|
||||
type: 'relation',
|
||||
required: false,
|
||||
options: {
|
||||
collectionId: '_pb_users_auth_',
|
||||
cascadeDelete: false,
|
||||
maxSelect: null
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'isActive',
|
||||
type: 'bool',
|
||||
required: false,
|
||||
options: {
|
||||
default: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'planType',
|
||||
type: 'select',
|
||||
required: false,
|
||||
options: {
|
||||
values: ['free', 'team', 'enterprise']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
type: 'json',
|
||||
required: false
|
||||
}
|
||||
],
|
||||
listRule: '@request.auth.id = owner || @request.auth.id ?~ members',
|
||||
viewRule: '@request.auth.id = owner || @request.auth.id ?~ members',
|
||||
createRule: '@request.auth.id != ""',
|
||||
updateRule: '@request.auth.id = owner',
|
||||
deleteRule: '@request.auth.id = owner'
|
||||
});
|
||||
console.log('✅ Accounts collection created');
|
||||
} catch (e) {
|
||||
if (e.response?.message?.includes('already exists')) {
|
||||
console.log('⚠️ Accounts collection already exists');
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Update Users Collection
|
||||
console.log('📦 Updating Users collection...');
|
||||
try {
|
||||
const usersCollection = await pb.collections.getOne('_pb_users_auth_');
|
||||
|
||||
// Add custom fields to users
|
||||
const updatedFields = [
|
||||
...usersCollection.fields,
|
||||
{
|
||||
name: 'bio',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
type: 'url',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'location',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'github',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'twitter',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'linkedin',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'instagram',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'publicProfile',
|
||||
type: 'bool',
|
||||
required: false,
|
||||
options: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'showClickStats',
|
||||
type: 'bool',
|
||||
required: false,
|
||||
options: {
|
||||
default: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'isPremium',
|
||||
type: 'bool',
|
||||
required: false,
|
||||
options: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'stripeCustomerId',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'stripeSubscriptionId',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'subscriptionStatus',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'planType',
|
||||
type: 'select',
|
||||
required: false,
|
||||
options: {
|
||||
values: ['free', 'monthly', 'yearly', 'lifetime']
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Filter out duplicates
|
||||
const fieldNames = new Set();
|
||||
const uniqueFields = updatedFields.filter(field => {
|
||||
if (fieldNames.has(field.name)) {
|
||||
return false;
|
||||
}
|
||||
fieldNames.add(field.name);
|
||||
return true;
|
||||
});
|
||||
|
||||
await pb.collections.update('_pb_users_auth_', {
|
||||
...usersCollection,
|
||||
fields: uniqueFields
|
||||
});
|
||||
console.log('✅ Users collection updated');
|
||||
} catch (e) {
|
||||
console.log('⚠️ Could not update Users collection:', e.message);
|
||||
}
|
||||
|
||||
console.log('\n✅ All collections created successfully!');
|
||||
console.log('\n📝 Next step: Run the seed script');
|
||||
console.log(' node scripts/seed-local-db.js');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
createCollections();
|
||||
320
apps/uload/scripts/create-default-templates.cjs
Executable file
320
apps/uload/scripts/create-default-templates.cjs
Executable file
|
|
@ -0,0 +1,320 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script to create default templates in the unified cards collection
|
||||
* Run this with: node scripts/create-default-templates.js
|
||||
*/
|
||||
|
||||
const PocketBase = require('pocketbase').default;
|
||||
|
||||
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||
|
||||
async function createDefaultTemplates() {
|
||||
try {
|
||||
// Admin authentication
|
||||
await pb.admins.authWithPassword(
|
||||
process.env.POCKETBASE_ADMIN_EMAIL || 'admin@example.com',
|
||||
process.env.POCKETBASE_ADMIN_PASSWORD || 'admin123456'
|
||||
);
|
||||
|
||||
console.log('✅ Authenticated as admin');
|
||||
|
||||
// Default template configurations
|
||||
const templates = [
|
||||
{
|
||||
type: 'template',
|
||||
visibility: 'public',
|
||||
is_featured: true,
|
||||
allow_duplication: true,
|
||||
category: 'personal',
|
||||
variant: 'default',
|
||||
config: {
|
||||
mode: 'beginner',
|
||||
modules: [
|
||||
{
|
||||
id: 'header-1',
|
||||
type: 'header',
|
||||
props: {
|
||||
title: 'John Doe',
|
||||
subtitle: 'Software Developer',
|
||||
avatar: '/api/files/_pb_users_auth_/placeholder/avatar.jpg'
|
||||
},
|
||||
order: 0
|
||||
},
|
||||
{
|
||||
id: 'content-1',
|
||||
type: 'content',
|
||||
props: {
|
||||
text: 'Passionate about creating amazing digital experiences and building innovative solutions.'
|
||||
},
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'links-1',
|
||||
type: 'links',
|
||||
props: {
|
||||
links: [
|
||||
{ label: 'GitHub', href: 'https://github.com', icon: '🔗' },
|
||||
{ label: 'LinkedIn', href: 'https://linkedin.com', icon: '💼' },
|
||||
{ label: 'Portfolio', href: 'https://example.com', icon: '🌐' }
|
||||
],
|
||||
style: 'button',
|
||||
layout: 'vertical'
|
||||
},
|
||||
order: 2
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
padding: '1.5rem',
|
||||
gap: '1rem',
|
||||
maxWidth: '400px'
|
||||
},
|
||||
animations: {
|
||||
hover: true,
|
||||
entrance: 'fade'
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
name: 'Simple Profile',
|
||||
description: 'A clean and simple profile card perfect for personal branding',
|
||||
version: '1.0.0'
|
||||
},
|
||||
constraints: {
|
||||
aspectRatio: '16/9'
|
||||
},
|
||||
tags: ['profile', 'minimal', 'clean', 'personal'],
|
||||
usage_count: 0,
|
||||
likes_count: 0
|
||||
},
|
||||
{
|
||||
type: 'template',
|
||||
visibility: 'public',
|
||||
is_featured: true,
|
||||
allow_duplication: true,
|
||||
category: 'creative',
|
||||
variant: 'glass',
|
||||
config: {
|
||||
mode: 'beginner',
|
||||
modules: [
|
||||
{
|
||||
id: 'header-1',
|
||||
type: 'header',
|
||||
props: {
|
||||
title: 'Creative Portfolio',
|
||||
subtitle: 'Design • Photography • Art',
|
||||
avatar: '/api/files/_pb_users_auth_/placeholder/creative-avatar.jpg'
|
||||
},
|
||||
order: 0
|
||||
},
|
||||
{
|
||||
id: 'gallery-1',
|
||||
type: 'gallery',
|
||||
props: {
|
||||
images: [
|
||||
'/api/files/_pb_users_auth_/placeholder/work1.jpg',
|
||||
'/api/files/_pb_users_auth_/placeholder/work2.jpg',
|
||||
'/api/files/_pb_users_auth_/placeholder/work3.jpg'
|
||||
],
|
||||
layout: 'grid',
|
||||
columns: 3
|
||||
},
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'content-1',
|
||||
type: 'content',
|
||||
props: {
|
||||
text: 'Bringing ideas to life through visual storytelling and innovative design solutions.'
|
||||
},
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
id: 'links-1',
|
||||
type: 'links',
|
||||
props: {
|
||||
links: [
|
||||
{ label: 'Behance', href: 'https://behance.net', icon: '🎨' },
|
||||
{ label: 'Instagram', href: 'https://instagram.com', icon: '📸' },
|
||||
{ label: 'Dribbble', href: 'https://dribbble.com', icon: '🏀' }
|
||||
],
|
||||
style: 'minimal',
|
||||
layout: 'horizontal'
|
||||
},
|
||||
order: 3
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
padding: '1.5rem',
|
||||
gap: '1.2rem',
|
||||
maxWidth: '450px'
|
||||
},
|
||||
animations: {
|
||||
hover: true,
|
||||
entrance: 'slide'
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
name: 'Creative Portfolio',
|
||||
description: 'Showcase your creative work with this visually appealing portfolio card',
|
||||
version: '1.0.0'
|
||||
},
|
||||
constraints: {
|
||||
aspectRatio: '4/3'
|
||||
},
|
||||
tags: ['creative', 'portfolio', 'art', 'design', 'gallery'],
|
||||
usage_count: 0,
|
||||
likes_count: 0
|
||||
},
|
||||
{
|
||||
type: 'template',
|
||||
visibility: 'public',
|
||||
is_featured: false,
|
||||
allow_duplication: true,
|
||||
category: 'minimal',
|
||||
variant: 'minimal',
|
||||
config: {
|
||||
mode: 'beginner',
|
||||
modules: [
|
||||
{
|
||||
id: 'header-1',
|
||||
type: 'header',
|
||||
props: {
|
||||
title: 'Jane Smith',
|
||||
subtitle: 'Designer & Developer'
|
||||
},
|
||||
order: 0
|
||||
},
|
||||
{
|
||||
id: 'links-1',
|
||||
type: 'links',
|
||||
props: {
|
||||
links: [
|
||||
{ label: 'Website', href: 'https://janesmith.dev', icon: '🌐' },
|
||||
{ label: 'Email', href: 'mailto:jane@example.com', icon: '✉️' }
|
||||
],
|
||||
style: 'text',
|
||||
layout: 'vertical'
|
||||
},
|
||||
order: 1
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
padding: '1rem',
|
||||
gap: '0.8rem',
|
||||
maxWidth: '300px'
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
name: 'Minimal Card',
|
||||
description: 'Clean and minimal design focusing on essential information only',
|
||||
version: '1.0.0'
|
||||
},
|
||||
constraints: {
|
||||
aspectRatio: '1/1'
|
||||
},
|
||||
tags: ['minimal', 'simple', 'clean', 'text'],
|
||||
usage_count: 0,
|
||||
likes_count: 0
|
||||
},
|
||||
{
|
||||
type: 'template',
|
||||
visibility: 'public',
|
||||
is_featured: false,
|
||||
allow_duplication: true,
|
||||
category: 'social',
|
||||
variant: 'hero',
|
||||
config: {
|
||||
mode: 'beginner',
|
||||
modules: [
|
||||
{
|
||||
id: 'header-1',
|
||||
type: 'header',
|
||||
props: {
|
||||
title: '@socialinfluencer',
|
||||
subtitle: 'Content Creator & Influencer',
|
||||
avatar: '/api/files/_pb_users_auth_/placeholder/influencer-avatar.jpg',
|
||||
verified: true
|
||||
},
|
||||
order: 0
|
||||
},
|
||||
{
|
||||
id: 'stats-1',
|
||||
type: 'stats',
|
||||
props: {
|
||||
stats: [
|
||||
{ label: 'Followers', value: '10.2K', icon: '👥' },
|
||||
{ label: 'Posts', value: '450', icon: '📱' },
|
||||
{ label: 'Engagement', value: '5.8%', icon: '💝' }
|
||||
],
|
||||
layout: 'compact'
|
||||
},
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'content-1',
|
||||
type: 'content',
|
||||
props: {
|
||||
text: 'Sharing daily inspiration, lifestyle tips, and behind-the-scenes moments ✨'
|
||||
},
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
id: 'links-1',
|
||||
type: 'links',
|
||||
props: {
|
||||
links: [
|
||||
{ label: 'Instagram', href: 'https://instagram.com', icon: '📸' },
|
||||
{ label: 'TikTok', href: 'https://tiktok.com', icon: '🎵' },
|
||||
{ label: 'YouTube', href: 'https://youtube.com', icon: '🎥' },
|
||||
{ label: 'Patreon', href: 'https://patreon.com', icon: '💖' }
|
||||
],
|
||||
style: 'button',
|
||||
layout: 'grid',
|
||||
columns: 2
|
||||
},
|
||||
order: 3
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
padding: '1.5rem',
|
||||
gap: '1rem',
|
||||
maxWidth: '400px'
|
||||
},
|
||||
animations: {
|
||||
hover: true,
|
||||
entrance: 'bounce'
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
name: 'Social Media Hub',
|
||||
description:
|
||||
'Perfect for influencers and content creators to showcase their social presence',
|
||||
version: '1.0.0'
|
||||
},
|
||||
constraints: {
|
||||
aspectRatio: '9/16'
|
||||
},
|
||||
tags: ['social', 'influencer', 'content', 'creator', 'instagram'],
|
||||
usage_count: 0,
|
||||
likes_count: 0
|
||||
}
|
||||
];
|
||||
|
||||
// Create templates
|
||||
for (const template of templates) {
|
||||
try {
|
||||
const result = await pb.collection('cards').create(template);
|
||||
console.log(`✅ Created template: ${template.metadata.name} (ID: ${result.id})`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to create template: ${template.metadata.name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎉 Successfully created default templates!');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating templates:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
createDefaultTemplates();
|
||||
389
apps/uload/scripts/create-unified-cards-collection.js
Normal file
389
apps/uload/scripts/create-unified-cards-collection.js
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script to create the unified cards collection in PocketBase
|
||||
* Run this with: node scripts/create-unified-cards-collection.js
|
||||
*/
|
||||
|
||||
const PocketBase = require('pocketbase');
|
||||
|
||||
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||
|
||||
async function createUnifiedCardsCollection() {
|
||||
try {
|
||||
// Admin authentication
|
||||
await pb.admins.authWithPassword(
|
||||
process.env.POCKETBASE_ADMIN_EMAIL || 'admin@example.com',
|
||||
process.env.POCKETBASE_ADMIN_PASSWORD || 'admin123456'
|
||||
);
|
||||
|
||||
console.log('✅ Authenticated as admin');
|
||||
|
||||
// Define the unified cards collection schema
|
||||
const collection = {
|
||||
name: 'cards',
|
||||
type: 'base',
|
||||
schema: [
|
||||
// Core relationships
|
||||
{
|
||||
name: 'user_id',
|
||||
type: 'relation',
|
||||
required: false, // null for system templates
|
||||
options: {
|
||||
collectionId: '_pb_users_auth_',
|
||||
cascadeDelete: true,
|
||||
minSelect: null,
|
||||
maxSelect: 1,
|
||||
displayFields: ['email', 'username']
|
||||
}
|
||||
},
|
||||
|
||||
// Card type and origin
|
||||
{
|
||||
name: 'type',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: {
|
||||
values: ['user', 'template', 'system'],
|
||||
maxSelect: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'source',
|
||||
type: 'select',
|
||||
required: false,
|
||||
options: {
|
||||
values: ['created', 'duplicated', 'imported', 'migrated'],
|
||||
maxSelect: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'template_id',
|
||||
type: 'relation',
|
||||
required: false,
|
||||
options: {
|
||||
collectionId: 'cards', // Self-reference
|
||||
cascadeDelete: false,
|
||||
minSelect: null,
|
||||
maxSelect: 1,
|
||||
displayFields: ['metadata']
|
||||
}
|
||||
},
|
||||
|
||||
// Card configuration (unified structure)
|
||||
{
|
||||
name: 'config',
|
||||
type: 'json',
|
||||
required: true,
|
||||
options: {
|
||||
maxSize: 1000000 // 1MB max
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'metadata',
|
||||
type: 'json',
|
||||
required: false,
|
||||
options: {
|
||||
maxSize: 100000 // 100KB max
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'constraints',
|
||||
type: 'json',
|
||||
required: false,
|
||||
options: {
|
||||
maxSize: 10000 // 10KB max
|
||||
}
|
||||
},
|
||||
|
||||
// Organization
|
||||
{
|
||||
name: 'page',
|
||||
type: 'text',
|
||||
required: false,
|
||||
options: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
pattern: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'position',
|
||||
type: 'number',
|
||||
required: false,
|
||||
options: {
|
||||
min: 0,
|
||||
max: 9999,
|
||||
noDecimal: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'folder_id',
|
||||
type: 'relation',
|
||||
required: false,
|
||||
options: {
|
||||
collectionId: 'folders',
|
||||
cascadeDelete: false,
|
||||
minSelect: null,
|
||||
maxSelect: 1,
|
||||
displayFields: ['name']
|
||||
}
|
||||
},
|
||||
|
||||
// Visibility and sharing
|
||||
{
|
||||
name: 'visibility',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: {
|
||||
values: ['private', 'public', 'unlisted'],
|
||||
maxSelect: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'is_featured',
|
||||
type: 'bool',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'allow_duplication',
|
||||
type: 'bool',
|
||||
required: false
|
||||
},
|
||||
|
||||
// Statistics
|
||||
{
|
||||
name: 'usage_count',
|
||||
type: 'number',
|
||||
required: false,
|
||||
options: {
|
||||
min: 0,
|
||||
max: 999999,
|
||||
noDecimal: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'likes_count',
|
||||
type: 'number',
|
||||
required: false,
|
||||
options: {
|
||||
min: 0,
|
||||
max: 999999,
|
||||
noDecimal: true
|
||||
}
|
||||
},
|
||||
|
||||
// Search and categorization
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'json',
|
||||
required: false,
|
||||
options: {
|
||||
maxSize: 10000
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'category',
|
||||
type: 'select',
|
||||
required: false,
|
||||
options: {
|
||||
values: ['personal', 'creative', 'minimal', 'social', 'portfolio', 'other'],
|
||||
maxSelect: 1
|
||||
}
|
||||
},
|
||||
|
||||
// Variant for styling
|
||||
{
|
||||
name: 'variant',
|
||||
type: 'select',
|
||||
required: false,
|
||||
options: {
|
||||
values: ['default', 'compact', 'hero', 'minimal', 'glass', 'gradient'],
|
||||
maxSelect: 1
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// Indexes for performance
|
||||
indexes: [
|
||||
'CREATE INDEX idx_cards_user_id ON cards (user_id)',
|
||||
'CREATE INDEX idx_cards_type ON cards (type)',
|
||||
'CREATE INDEX idx_cards_page ON cards (page)',
|
||||
'CREATE INDEX idx_cards_visibility ON cards (visibility)',
|
||||
'CREATE INDEX idx_cards_template_id ON cards (template_id)',
|
||||
'CREATE INDEX idx_cards_position ON cards (position)'
|
||||
],
|
||||
|
||||
// API Rules
|
||||
listRule:
|
||||
'@request.auth.id = user_id || visibility = "public" || (visibility = "unlisted" && @request.query.id != "")',
|
||||
viewRule: '@request.auth.id = user_id || visibility != "private"',
|
||||
createRule: '@request.auth.id != ""',
|
||||
updateRule:
|
||||
'@request.auth.id = user_id || (@request.auth.id != "" && type = "system" && @request.auth.admin = true)',
|
||||
deleteRule: '@request.auth.id = user_id && type != "system"',
|
||||
|
||||
options: {}
|
||||
};
|
||||
|
||||
// Create the collection
|
||||
const result = await pb.collections.create(collection);
|
||||
console.log('✅ Created unified cards collection:', result.name);
|
||||
|
||||
// Create some default system templates
|
||||
await createDefaultTemplates(pb);
|
||||
|
||||
console.log('🎉 Successfully created unified cards collection!');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating collection:', error);
|
||||
|
||||
// If collection already exists, try to update it
|
||||
if (error.response?.code === 400) {
|
||||
console.log('Collection might already exist. Trying to update...');
|
||||
try {
|
||||
const existing = await pb.collections.getOne('cards');
|
||||
await pb.collections.update(existing.id, collection);
|
||||
console.log('✅ Updated existing cards collection');
|
||||
} catch (updateError) {
|
||||
console.error('❌ Failed to update:', updateError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createDefaultTemplates(pb) {
|
||||
const templates = [
|
||||
{
|
||||
type: 'template',
|
||||
visibility: 'public',
|
||||
is_featured: true,
|
||||
allow_duplication: true,
|
||||
category: 'personal',
|
||||
variant: 'default',
|
||||
config: {
|
||||
mode: 'beginner',
|
||||
modules: [
|
||||
{
|
||||
id: 'header-1',
|
||||
type: 'header',
|
||||
props: {
|
||||
title: 'John Doe',
|
||||
subtitle: 'Software Developer'
|
||||
},
|
||||
order: 0
|
||||
},
|
||||
{
|
||||
id: 'content-1',
|
||||
type: 'content',
|
||||
props: {
|
||||
text: 'Passionate about creating amazing digital experiences.'
|
||||
},
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'links-1',
|
||||
type: 'links',
|
||||
props: {
|
||||
links: [
|
||||
{ label: 'GitHub', href: 'https://github.com', icon: '🔗' },
|
||||
{ label: 'LinkedIn', href: 'https://linkedin.com', icon: '💼' }
|
||||
],
|
||||
style: 'button'
|
||||
},
|
||||
order: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
metadata: {
|
||||
name: 'Simple Profile',
|
||||
description: 'A clean and simple profile card',
|
||||
version: '1.0.0',
|
||||
tags: ['profile', 'minimal', 'clean']
|
||||
},
|
||||
constraints: {
|
||||
aspectRatio: '16/9'
|
||||
},
|
||||
usage_count: 0,
|
||||
likes_count: 0
|
||||
},
|
||||
{
|
||||
type: 'template',
|
||||
visibility: 'public',
|
||||
is_featured: true,
|
||||
allow_duplication: true,
|
||||
category: 'personal',
|
||||
variant: 'gradient',
|
||||
config: {
|
||||
mode: 'advanced',
|
||||
template: `
|
||||
<div class="professional-card">
|
||||
<h1>{{name}}</h1>
|
||||
<p class="tagline">{{tagline}}</p>
|
||||
<div class="contact">
|
||||
<a href="mailto:{{email}}">{{email}}</a>
|
||||
<a href="tel:{{phone}}">{{phone}}</a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
css: `
|
||||
.professional-card {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
.tagline {
|
||||
font-size: 1.2rem;
|
||||
margin: 1rem 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.contact {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.contact a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
variables: [
|
||||
{ name: 'name', type: 'text', label: 'Your Name' },
|
||||
{ name: 'tagline', type: 'text', label: 'Tagline' },
|
||||
{ name: 'email', type: 'text', label: 'Email' },
|
||||
{ name: 'phone', type: 'text', label: 'Phone' }
|
||||
],
|
||||
values: {
|
||||
name: 'Your Name',
|
||||
tagline: 'Your tagline here',
|
||||
email: 'contact@example.com',
|
||||
phone: '+1 234 567 890'
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
name: 'Professional Card',
|
||||
description: 'Professional contact card template',
|
||||
version: '1.0.0',
|
||||
tags: ['professional', 'contact']
|
||||
},
|
||||
constraints: {
|
||||
aspectRatio: '16/9'
|
||||
},
|
||||
usage_count: 0,
|
||||
likes_count: 0
|
||||
}
|
||||
];
|
||||
|
||||
for (const template of templates) {
|
||||
try {
|
||||
await pb.collection('cards').create(template);
|
||||
console.log(`✅ Created template: ${template.metadata.name}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to create template: ${template.metadata.name}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
createUnifiedCardsCollection();
|
||||
106
apps/uload/scripts/debug-auth.mjs
Normal file
106
apps/uload/scripts/debug-auth.mjs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('https://pb.ulo.ad');
|
||||
|
||||
console.log('Testing authentication...\n');
|
||||
|
||||
// Test 1: Try to create a new test user
|
||||
async function testRegistration() {
|
||||
const testEmail = `test_${Date.now()}@example.com`;
|
||||
const testPassword = 'TestPassword123!';
|
||||
|
||||
console.log('1. Testing registration with:', testEmail);
|
||||
try {
|
||||
// Generate a random ID for PocketBase
|
||||
const randomId = Math.random().toString(36).substring(2, 17);
|
||||
const userData = {
|
||||
id: randomId,
|
||||
email: testEmail,
|
||||
password: testPassword,
|
||||
passwordConfirm: testPassword,
|
||||
emailVisibility: true
|
||||
};
|
||||
|
||||
const newUser = await pb.collection('users').create(userData);
|
||||
console.log('✅ Registration successful! User ID:', newUser.id);
|
||||
|
||||
// Try to login with the new user
|
||||
console.log('\n2. Testing login with newly created user...');
|
||||
const authData = await pb.collection('users').authWithPassword(testEmail, testPassword);
|
||||
console.log('✅ Login successful! Token:', authData.token.substring(0, 20) + '...');
|
||||
|
||||
return { email: testEmail, password: testPassword };
|
||||
} catch (err) {
|
||||
console.error('❌ Registration failed:', err.response || err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: Try existing user
|
||||
async function testExistingUser() {
|
||||
console.log('\n3. Testing with existing user: tills95@gmail.com');
|
||||
const passwords = ['dev123456', 'password', '12345678', 'admin123'];
|
||||
|
||||
for (const password of passwords) {
|
||||
try {
|
||||
console.log(` Trying password: ${password}`);
|
||||
const authData = await pb.collection('users').authWithPassword('tills95@gmail.com', password);
|
||||
console.log('✅ Login successful with password:', password);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(` ❌ Failed with: ${password}`);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test 3: Check collection rules
|
||||
async function checkCollectionRules() {
|
||||
console.log('\n4. Checking collection configuration...');
|
||||
try {
|
||||
// This will fail without admin auth, but we can see the error
|
||||
const response = await fetch('https://pb.ulo.ad/api/collections/users', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 403) {
|
||||
console.log(' ⚠️ Collection info requires admin auth (expected)');
|
||||
} else {
|
||||
const data = await response.json();
|
||||
console.log(' Collection info:', data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(' Error checking collection:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
async function runTests() {
|
||||
console.log('🔍 Starting authentication debug...\n');
|
||||
console.log('PocketBase URL: https://pb.ulo.ad');
|
||||
console.log('=====================================\n');
|
||||
|
||||
// Test registration and login
|
||||
const newUser = await testRegistration();
|
||||
|
||||
// Test existing user
|
||||
await testExistingUser();
|
||||
|
||||
// Check collection
|
||||
await checkCollectionRules();
|
||||
|
||||
if (newUser) {
|
||||
console.log('\n✅ Test user created successfully!');
|
||||
console.log('Email:', newUser.email);
|
||||
console.log('Password:', newUser.password);
|
||||
console.log('\nYou can use these credentials to test login in the app.');
|
||||
}
|
||||
|
||||
console.log('\n=====================================');
|
||||
console.log('Debug complete!\n');
|
||||
}
|
||||
|
||||
runTests().catch(console.error);
|
||||
41
apps/uload/scripts/extract-templates.js
Normal file
41
apps/uload/scripts/extract-templates.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import fs from 'fs';
|
||||
|
||||
// Read the markdown file
|
||||
const content = fs.readFileSync('docs/mail/email-templates-bilingual.md', 'utf8');
|
||||
|
||||
// Extract templates
|
||||
const templates = {};
|
||||
|
||||
// Find verification template
|
||||
const verificationMatch = content.match(/## 1\. E-Mail-Verifizierung.*?```html\n([\s\S]*?)```/);
|
||||
if (verificationMatch) {
|
||||
templates.verification = verificationMatch[1].trim();
|
||||
}
|
||||
|
||||
// Find password reset template
|
||||
const passwordMatch = content.match(/## 2\. Passwort-Reset.*?```html\n([\s\S]*?)```/);
|
||||
if (passwordMatch) {
|
||||
templates.passwordReset = passwordMatch[1].trim();
|
||||
}
|
||||
|
||||
// Find email change template
|
||||
const emailChangeMatch = content.match(/## 3\. E-Mail-Änderung.*?```html\n([\s\S]*?)```/);
|
||||
if (emailChangeMatch) {
|
||||
templates.emailChange = emailChangeMatch[1].trim();
|
||||
}
|
||||
|
||||
// Find OTP template
|
||||
const otpMatch = content.match(/## 4\. OTP.*?```html\n([\s\S]*?)```/);
|
||||
if (otpMatch) {
|
||||
templates.otp = otpMatch[1].trim();
|
||||
}
|
||||
|
||||
// Find login alert template
|
||||
const loginAlertMatch = content.match(/## 5\. Login-Alert.*?```html\n([\s\S]*?)```/);
|
||||
if (loginAlertMatch) {
|
||||
templates.loginAlert = loginAlertMatch[1].trim();
|
||||
}
|
||||
|
||||
// Save to JSON file
|
||||
fs.writeFileSync('email-templates.json', JSON.stringify(templates, null, 2));
|
||||
console.log('Templates extracted to email-templates.json');
|
||||
23
apps/uload/scripts/fix-field-names.sh
Executable file
23
apps/uload/scripts/fix-field-names.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Fix field names to match PocketBase schema
|
||||
|
||||
echo "Fixing field names in source files..."
|
||||
|
||||
# Fix user -> user_id in filters and create statements
|
||||
find src -type f \( -name "*.ts" -o -name "*.svelte" \) -exec sed -i '' \
|
||||
-e 's/filter: `user="/filter: `user_id="/g' \
|
||||
-e 's/filter: `link="/filter: `link_id="/g' \
|
||||
-e 's/filter: `folder="/filter: `folder_id="/g' \
|
||||
-e 's/\&\& user="/\&\& user_id="/g' \
|
||||
-e 's/\&\& link="/\&\& link_id="/g' \
|
||||
-e 's/\&\& folder="/\&\& folder_id="/g' {} \;
|
||||
|
||||
# Fix field names in create/update operations
|
||||
find src -type f \( -name "*.ts" -o -name "*.svelte" \) -exec sed -i '' \
|
||||
-e 's/^\([[:space:]]*\)user: /\1user_id: /g' \
|
||||
-e 's/^\([[:space:]]*\)link: /\1link_id: /g' \
|
||||
-e 's/^\([[:space:]]*\)folder: /\1folder_id: /g' {} \;
|
||||
|
||||
echo "Done! Field names have been updated."
|
||||
echo "Please restart your dev server to apply changes."
|
||||
25
apps/uload/scripts/fix-imports.sh
Executable file
25
apps/uload/scripts/fix-imports.sh
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Fix all problematic card imports
|
||||
FILES=$(find src -name "*.svelte" -type f -exec grep -l "from '\$lib/components/cards/\(BaseCard\|ThemeProvider\|SafeHTMLCard\|CardListV2\|Card\)\.svelte'" {} \; 2>/dev/null)
|
||||
|
||||
for file in $FILES; do
|
||||
echo "Fixing imports in: $file"
|
||||
|
||||
# Replace imports
|
||||
sed -i '' "s|import.*from '\$lib/components/cards/Card\.svelte'.*|import CardRenderer from '\$lib/components/cards/CardRenderer.svelte';|g" "$file"
|
||||
sed -i '' "s|import.*from '\$lib/components/cards/BaseCard\.svelte'.*||g" "$file"
|
||||
sed -i '' "s|import.*from '\$lib/components/cards/ThemeProvider\.svelte'.*||g" "$file"
|
||||
sed -i '' "s|import.*from '\$lib/components/cards/SafeHTMLCard\.svelte'.*||g" "$file"
|
||||
sed -i '' "s|import.*from '\$lib/components/cards/CardListV2\.svelte'.*||g" "$file"
|
||||
|
||||
# Replace component usage (common patterns)
|
||||
sed -i '' "s|<Card |<CardRenderer |g" "$file"
|
||||
sed -i '' "s|<BaseCard |<CardRenderer |g" "$file"
|
||||
sed -i '' "s|<SafeHTMLCard |<CardRenderer |g" "$file"
|
||||
|
||||
# Fix type imports
|
||||
sed -i '' "s|UnifiedCard|Card|g" "$file"
|
||||
done
|
||||
|
||||
echo "Import fixes completed!"
|
||||
86
apps/uload/scripts/fix-links-collection.sh
Executable file
86
apps/uload/scripts/fix-links-collection.sh
Executable file
|
|
@ -0,0 +1,86 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Script to update the links collection in PocketBase
|
||||
# This adds missing fields to improve the structure
|
||||
|
||||
echo "Updating links collection structure..."
|
||||
|
||||
# PocketBase URL
|
||||
PB_URL="https://pb.ulo.ad"
|
||||
|
||||
# Note: This script outlines the manual steps needed
|
||||
# Since we can't modify the collection directly through the API due to references,
|
||||
# these changes need to be made in the PocketBase Admin UI
|
||||
|
||||
cat << EOF
|
||||
|
||||
===========================================
|
||||
MANUAL STEPS FOR POCKETBASE ADMIN UI
|
||||
===========================================
|
||||
|
||||
Please log into PocketBase Admin at: ${PB_URL}/_/
|
||||
|
||||
1. Navigate to Collections > links
|
||||
|
||||
2. Add the following NEW fields:
|
||||
|
||||
a) use_username (Bool)
|
||||
- Required: No
|
||||
- Default: false
|
||||
|
||||
b) click_count (Number)
|
||||
- Required: No
|
||||
- Default: 0
|
||||
- Note: Will be calculated from clicks collection
|
||||
|
||||
c) last_clicked_at (Date)
|
||||
- Required: No
|
||||
|
||||
d) utm_source (Text)
|
||||
- Required: No
|
||||
- Max length: 255
|
||||
|
||||
e) utm_medium (Text)
|
||||
- Required: No
|
||||
- Max length: 255
|
||||
|
||||
f) utm_campaign (Text)
|
||||
- Required: No
|
||||
- Max length: 255
|
||||
|
||||
3. Rename field (if possible):
|
||||
- "password" → "password_hash"
|
||||
- Note: If renaming breaks references, keep as "password"
|
||||
|
||||
4. Update field settings:
|
||||
- max_clicks: Set "Only integers" to true, Min value: 0
|
||||
- click_count: Set "Only integers" to true, Min value: 0
|
||||
|
||||
5. Add Indexes (under "Indexes" tab):
|
||||
- short_code (unique)
|
||||
- user_id
|
||||
- is_active
|
||||
- created_by
|
||||
|
||||
6. Save the collection
|
||||
|
||||
===========================================
|
||||
CODE CHANGES NEEDED AFTER DB UPDATE:
|
||||
===========================================
|
||||
|
||||
After updating the database, update these files:
|
||||
|
||||
1. /src/lib/pocketbase/types.ts
|
||||
- Add new fields to Link interface
|
||||
|
||||
2. /src/routes/(app)/my/links/+page.server.ts
|
||||
- Handle use_username field in create action
|
||||
- Remove references to folder_id
|
||||
|
||||
3. /src/routes/[code]/+page.server.ts
|
||||
- Update click tracking to increment click_count
|
||||
- Set last_clicked_at on each click
|
||||
|
||||
EOF
|
||||
|
||||
echo "Instructions printed. Please follow the manual steps above."
|
||||
65
apps/uload/scripts/generate-pwa-icons.js
Normal file
65
apps/uload/scripts/generate-pwa-icons.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Script to generate PWA icons from logo
|
||||
// Since we don't have image processing libs, we'll create placeholder SVGs
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// SVG Logo content (simple U for uLoad)
|
||||
const createSvgIcon = (size) => `
|
||||
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="${size}" height="${size}" fill="#3B82F6" rx="${size * 0.15}"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="${size * 0.5}px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Icon sizes needed for PWA
|
||||
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
|
||||
|
||||
// Ensure directory exists
|
||||
const iconsDir = path.join(__dirname, '..', 'static', 'icons');
|
||||
if (!fs.existsSync(iconsDir)) {
|
||||
fs.mkdirSync(iconsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate SVG icons
|
||||
sizes.forEach(size => {
|
||||
const filename = `icon-${size}x${size}.svg`;
|
||||
const filepath = path.join(iconsDir, filename);
|
||||
const content = createSvgIcon(size);
|
||||
|
||||
fs.writeFileSync(filepath, content.trim());
|
||||
console.log(`Generated ${filename}`);
|
||||
});
|
||||
|
||||
// Also create apple-touch-icon
|
||||
const appleIcon = createSvgIcon(180);
|
||||
fs.writeFileSync(path.join(iconsDir, 'apple-touch-icon.svg'), appleIcon.trim());
|
||||
console.log('Generated apple-touch-icon.svg');
|
||||
|
||||
// Create maskable icon (with safe area padding)
|
||||
const createMaskableIcon = (size) => {
|
||||
const safeArea = size * 0.8; // 80% safe area
|
||||
const padding = (size - safeArea) / 2;
|
||||
|
||||
return `
|
||||
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="${size}" height="${size}" fill="#3B82F6"/>
|
||||
<rect x="${padding}" y="${padding}" width="${safeArea}" height="${safeArea}" fill="#3B82F6" rx="${safeArea * 0.15}"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="${safeArea * 0.5}px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
// Generate maskable icons
|
||||
[192, 512].forEach(size => {
|
||||
const filename = `icon-maskable-${size}x${size}.svg`;
|
||||
const filepath = path.join(iconsDir, filename);
|
||||
const content = createMaskableIcon(size);
|
||||
|
||||
fs.writeFileSync(filepath, content.trim());
|
||||
console.log(`Generated ${filename}`);
|
||||
});
|
||||
|
||||
console.log('\n✅ All PWA icons generated successfully!');
|
||||
72
apps/uload/scripts/generate-pwa-icons.mjs
Normal file
72
apps/uload/scripts/generate-pwa-icons.mjs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Script to generate PWA icons from logo
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// SVG Logo content (simple U for uLoad)
|
||||
const createSvgIcon = (size) => `
|
||||
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="${size}" height="${size}" fill="#3B82F6" rx="${size * 0.15}"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="${size * 0.5}px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Icon sizes needed for PWA
|
||||
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
|
||||
|
||||
// Ensure directory exists
|
||||
const iconsDir = path.join(__dirname, '..', 'static', 'icons');
|
||||
if (!fs.existsSync(iconsDir)) {
|
||||
fs.mkdirSync(iconsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate SVG icons - but also need PNG for manifest
|
||||
sizes.forEach(size => {
|
||||
const filename = `icon-${size}x${size}.svg`;
|
||||
const filepath = path.join(iconsDir, filename);
|
||||
const content = createSvgIcon(size);
|
||||
|
||||
fs.writeFileSync(filepath, content.trim());
|
||||
console.log(`Generated ${filename}`);
|
||||
|
||||
// Also create PNG placeholder (we'll use SVG as PNG alternative)
|
||||
const pngFilename = `icon-${size}x${size}.png`;
|
||||
const pngFilepath = path.join(iconsDir, pngFilename);
|
||||
// Create a symlink or copy the SVG as PNG won't work, so we'll update manifest instead
|
||||
});
|
||||
|
||||
// Also create apple-touch-icon
|
||||
const appleIcon = createSvgIcon(180);
|
||||
fs.writeFileSync(path.join(iconsDir, 'apple-touch-icon.svg'), appleIcon.trim());
|
||||
console.log('Generated apple-touch-icon.svg');
|
||||
|
||||
// Create maskable icon (with safe area padding)
|
||||
const createMaskableIcon = (size) => {
|
||||
const safeArea = size * 0.8; // 80% safe area
|
||||
const padding = (size - safeArea) / 2;
|
||||
|
||||
return `
|
||||
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="${size}" height="${size}" fill="#3B82F6"/>
|
||||
<rect x="${padding}" y="${padding}" width="${safeArea}" height="${safeArea}" fill="#3B82F6" rx="${safeArea * 0.15}"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="${safeArea * 0.5}px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
// Generate maskable icons
|
||||
[192, 512].forEach(size => {
|
||||
const filename = `icon-maskable-${size}x${size}.svg`;
|
||||
const filepath = path.join(iconsDir, filename);
|
||||
const content = createMaskableIcon(size);
|
||||
|
||||
fs.writeFileSync(filepath, content.trim());
|
||||
console.log(`Generated ${filename}`);
|
||||
});
|
||||
|
||||
console.log('\n✅ All PWA icons generated successfully!');
|
||||
63
apps/uload/scripts/migrate-links-collection.sh
Executable file
63
apps/uload/scripts/migrate-links-collection.sh
Executable file
|
|
@ -0,0 +1,63 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Migration script for PocketBase links collection
|
||||
# This script documents the manual steps needed to replace the old links collection
|
||||
|
||||
echo "🔄 Links Collection Migration Guide"
|
||||
echo "=================================="
|
||||
|
||||
cat << EOF
|
||||
|
||||
⚠️ IMPORTANT: Database Migration Required
|
||||
|
||||
The new 'links_improved' collection has been created with all the enhanced fields:
|
||||
✅ created/updated timestamps (automatic)
|
||||
✅ use_username (bool)
|
||||
✅ click_count (number)
|
||||
✅ last_clicked_at (date)
|
||||
✅ utm_source, utm_medium, utm_campaign (text)
|
||||
|
||||
MANUAL STEPS REQUIRED IN POCKETBASE ADMIN:
|
||||
|
||||
1. BACKUP FIRST (!)
|
||||
- Go to: https://pb.ulo.ad/_/
|
||||
- Create a backup before proceeding
|
||||
|
||||
2. UPDATE REFERENCES:
|
||||
Because the old 'links' collection has references (clicks, linktags),
|
||||
we need to update the application code first:
|
||||
|
||||
a) Update all references in the app from 'links' to 'links_improved'
|
||||
b) Test with the new collection
|
||||
c) When everything works, delete the old collection
|
||||
|
||||
3. COLLECTION RENAMING:
|
||||
After updating the code:
|
||||
- Delete the old 'links' collection
|
||||
- Rename 'links_improved' to 'links'
|
||||
|
||||
ALTERNATIVE APPROACH (RECOMMENDED):
|
||||
Instead of renaming, update the application code to use 'links_improved'
|
||||
and keep it as the new name.
|
||||
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "📋 Migration Status:"
|
||||
echo "✅ New collection 'links_improved' created"
|
||||
echo "✅ Test data migrated successfully"
|
||||
echo "✅ All new fields working correctly"
|
||||
echo "⏳ Manual steps required (see above)"
|
||||
echo ""
|
||||
echo "🔍 Test the new collection:"
|
||||
echo "Collection ID: pbc_394542459"
|
||||
echo "Test records created: 2"
|
||||
echo ""
|
||||
|
||||
# Show the test data
|
||||
echo "📊 Sample data from new collection:"
|
||||
echo "=================================="
|
||||
echo "Test Link 1: TEST001 - https://example.com (with UTM tracking)"
|
||||
echo "Test Link 2: JxBSn7 - https://ulo.ad/my/links (migrated)"
|
||||
echo ""
|
||||
echo "Both records have automatic timestamps ✅"
|
||||
79
apps/uload/scripts/migrate-links.js
Normal file
79
apps/uload/scripts/migrate-links.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Migration script to convert use_username links to username-prefixed short_codes
|
||||
// Run this script to update existing links in the database
|
||||
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('http://localhost:8090');
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
console.log('Starting migration...');
|
||||
console.log('This script will update links with use_username=true to use the new format');
|
||||
|
||||
// Get all links with use_username=true
|
||||
const links = await pb.collection('links').getFullList({
|
||||
filter: 'use_username = true',
|
||||
expand: 'user_id'
|
||||
});
|
||||
|
||||
console.log(`Found ${links.length} links to migrate`);
|
||||
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const link of links) {
|
||||
try {
|
||||
// Get the username from the expanded user
|
||||
const username = link.expand?.user_id?.username;
|
||||
|
||||
if (!username) {
|
||||
console.error(`No username found for link ${link.id} (user: ${link.user_id})`);
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if already migrated (contains slash)
|
||||
if (link.short_code.includes('/')) {
|
||||
console.log(`Link ${link.id} already migrated: ${link.short_code}`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create new short_code with username prefix
|
||||
const newShortCode = `${username}/${link.short_code}`;
|
||||
|
||||
console.log(`Updating link ${link.id}: ${link.short_code} -> ${newShortCode}`);
|
||||
|
||||
// Update the link
|
||||
await pb.collection('links').update(link.id, {
|
||||
short_code: newShortCode
|
||||
});
|
||||
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error(`Error migrating link ${link.id}:`, error.message);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Migration Summary ===');
|
||||
console.log(`✅ Successfully migrated: ${successCount} links`);
|
||||
console.log(`⏭️ Skipped (already migrated): ${skippedCount} links`);
|
||||
console.log(`❌ Errors: ${errorCount} links`);
|
||||
|
||||
if (successCount > 0) {
|
||||
console.log('\n⚠️ Important: The use_username field should be removed from the collection schema');
|
||||
console.log('You can do this manually in PocketBase Admin UI');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run migration
|
||||
migrate();
|
||||
77
apps/uload/scripts/migrate-to-username-prefix.js
Normal file
77
apps/uload/scripts/migrate-to-username-prefix.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Migration script to convert use_username links to username-prefixed short_codes
|
||||
// This script:
|
||||
// 1. Finds all links with use_username=true
|
||||
// 2. Updates their short_code to include the username prefix
|
||||
// 3. Removes the use_username field (done via PocketBase admin)
|
||||
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('http://localhost:8090');
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
// Authenticate as admin (update with your credentials)
|
||||
await pb.admins.authWithPassword('admin@example.com', 'your-password');
|
||||
|
||||
console.log('Fetching all links with use_username=true...');
|
||||
|
||||
// Get all links with use_username=true
|
||||
const links = await pb.collection('links').getFullList({
|
||||
filter: 'use_username = true',
|
||||
expand: 'user_id'
|
||||
});
|
||||
|
||||
console.log(`Found ${links.length} links to migrate`);
|
||||
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const link of links) {
|
||||
try {
|
||||
// Get the username from the expanded user
|
||||
const username = link.expand?.user_id?.username;
|
||||
|
||||
if (!username) {
|
||||
console.error(`No username found for link ${link.id} (user: ${link.user_id})`);
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create new short_code with username prefix
|
||||
const newShortCode = `${username}/${link.short_code}`;
|
||||
|
||||
console.log(`Updating link ${link.id}: ${link.short_code} -> ${newShortCode}`);
|
||||
|
||||
// Update the link
|
||||
await pb.collection('links').update(link.id, {
|
||||
short_code: newShortCode
|
||||
});
|
||||
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error(`Error migrating link ${link.id}:`, error);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nMigration complete!');
|
||||
console.log(`✅ Successfully migrated: ${successCount} links`);
|
||||
console.log(`❌ Errors: ${errorCount} links`);
|
||||
|
||||
if (successCount > 0) {
|
||||
console.log('\n⚠️ Next steps:');
|
||||
console.log('1. Remove the use_username field from the links collection in PocketBase admin');
|
||||
console.log('2. Test that all migrated links still work');
|
||||
console.log('3. Deploy the updated application code');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run migration
|
||||
migrate();
|
||||
157
apps/uload/scripts/migrate-to-workspaces.js
Normal file
157
apps/uload/scripts/migrate-to-workspaces.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Migration script to convert from shared_access to workspace system
|
||||
* Run this after deploying the new workspace collections
|
||||
*/
|
||||
|
||||
import PocketBase from 'pocketbase';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config({ path: '.env.production' });
|
||||
|
||||
const pb = new PocketBase(process.env.PUBLIC_POCKETBASE_URL || 'https://pb.ulo.ad');
|
||||
|
||||
async function migrate() {
|
||||
console.log('Starting migration to workspace system...');
|
||||
|
||||
try {
|
||||
// Authenticate as admin
|
||||
const adminEmail = process.env.ADMIN_EMAIL;
|
||||
const adminPassword = process.env.ADMIN_PASSWORD;
|
||||
|
||||
if (!adminEmail || !adminPassword) {
|
||||
console.error('Please set ADMIN_EMAIL and ADMIN_PASSWORD in .env.production');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await pb.admins.authWithPassword(adminEmail, adminPassword);
|
||||
console.log('✓ Authenticated as admin');
|
||||
|
||||
// Get all users
|
||||
const users = await pb.collection('users').getFullList();
|
||||
console.log(`Found ${users.length} users to migrate`);
|
||||
|
||||
// Create personal workspace for each user
|
||||
for (const user of users) {
|
||||
try {
|
||||
// Check if personal workspace already exists
|
||||
const existingWorkspaces = await pb.collection('workspaces').getList(1, 1, {
|
||||
filter: `owner="${user.id}" && type="personal"`
|
||||
});
|
||||
|
||||
if (existingWorkspaces.items.length === 0) {
|
||||
// Create personal workspace
|
||||
const workspace = await pb.collection('workspaces').create({
|
||||
name: `${user.name || user.email}'s Workspace`,
|
||||
owner: user.id,
|
||||
type: 'personal',
|
||||
subscription_status: user.subscription_status || 'free',
|
||||
description: 'Personal workspace'
|
||||
});
|
||||
|
||||
console.log(`✓ Created personal workspace for ${user.email}`);
|
||||
|
||||
// Add user as owner in workspace_members
|
||||
await pb.collection('workspace_members').create({
|
||||
workspace: workspace.id,
|
||||
user: user.id,
|
||||
role: 'owner',
|
||||
invitation_status: 'accepted',
|
||||
accepted_at: new Date().toISOString()
|
||||
});
|
||||
} else {
|
||||
console.log(`- Personal workspace already exists for ${user.email}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`✗ Error creating workspace for ${user.email}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate shared_access to workspace_members
|
||||
console.log('\nMigrating shared access...');
|
||||
|
||||
try {
|
||||
const sharedAccess = await pb.collection('shared_access').getFullList({
|
||||
expand: 'owner,user'
|
||||
});
|
||||
|
||||
console.log(`Found ${sharedAccess.length} shared access records`);
|
||||
|
||||
for (const access of sharedAccess) {
|
||||
try {
|
||||
// Find or create team workspace for the owner
|
||||
let teamWorkspace;
|
||||
const existingTeamWorkspaces = await pb.collection('workspaces').getList(1, 1, {
|
||||
filter: `owner="${access.owner}" && type="team"`
|
||||
});
|
||||
|
||||
if (existingTeamWorkspaces.items.length === 0) {
|
||||
// Create team workspace
|
||||
const ownerData = access.expand?.owner;
|
||||
teamWorkspace = await pb.collection('workspaces').create({
|
||||
name: `${ownerData?.name || ownerData?.email}'s Team`,
|
||||
owner: access.owner,
|
||||
type: 'team',
|
||||
description: 'Team workspace for collaboration'
|
||||
});
|
||||
|
||||
console.log(`✓ Created team workspace for owner ${access.owner}`);
|
||||
|
||||
// Add owner as owner in workspace_members
|
||||
await pb.collection('workspace_members').create({
|
||||
workspace: teamWorkspace.id,
|
||||
user: access.owner,
|
||||
role: 'owner',
|
||||
invitation_status: 'accepted',
|
||||
accepted_at: new Date().toISOString()
|
||||
});
|
||||
} else {
|
||||
teamWorkspace = existingTeamWorkspaces.items[0];
|
||||
}
|
||||
|
||||
// Check if member already exists
|
||||
const existingMembers = await pb.collection('workspace_members').getList(1, 1, {
|
||||
filter: `workspace="${teamWorkspace.id}" && user="${access.user}"`
|
||||
});
|
||||
|
||||
if (existingMembers.items.length === 0) {
|
||||
// Add team member
|
||||
await pb.collection('workspace_members').create({
|
||||
workspace: teamWorkspace.id,
|
||||
user: access.user,
|
||||
role: access.permissions?.manage_team ? 'admin' : 'member',
|
||||
permissions: access.permissions,
|
||||
invitation_status: access.invitation_status,
|
||||
invitation_token: access.invitation_token,
|
||||
invited_at: access.invited_at,
|
||||
accepted_at: access.accepted_at
|
||||
});
|
||||
|
||||
console.log(`✓ Migrated team member ${access.user} to workspace ${teamWorkspace.id}`);
|
||||
} else {
|
||||
console.log(`- Team member ${access.user} already exists in workspace`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`✗ Error migrating shared access ${access.id}:`, error.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('✗ Error fetching shared access:', error.message);
|
||||
}
|
||||
|
||||
console.log('\n✅ Migration completed successfully!');
|
||||
console.log('\nNext steps:');
|
||||
console.log('1. Test the new workspace system');
|
||||
console.log('2. Update any links/cards to reference workspace instead of owner');
|
||||
console.log('3. Once verified, you can remove the old shared_access collection');
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run migration
|
||||
migrate().catch(console.error);
|
||||
69
apps/uload/scripts/optimize-database.sql
Normal file
69
apps/uload/scripts/optimize-database.sql
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
-- ULoad Database Performance Optimizations
|
||||
-- Diese SQL-Befehle optimieren die SQLite-Datenbank für bessere Performance
|
||||
|
||||
-- 1. Aktiviere WAL-Modus für bessere Concurrency
|
||||
PRAGMA journal_mode=WAL;
|
||||
|
||||
-- 2. Optimiere Cache-Größe (8MB)
|
||||
PRAGMA cache_size=8000;
|
||||
|
||||
-- 3. Synchronisation optimieren für bessere Performance
|
||||
PRAGMA synchronous=NORMAL;
|
||||
|
||||
-- 4. Memory-mapped I/O aktivieren (256MB)
|
||||
PRAGMA mmap_size=268435456;
|
||||
|
||||
-- 5. Auto-Vacuum optimieren
|
||||
PRAGMA auto_vacuum=INCREMENTAL;
|
||||
|
||||
-- 6. Temp Store in Memory
|
||||
PRAGMA temp_store=MEMORY;
|
||||
|
||||
-- 7. Analyze Statistiken aktualisieren
|
||||
ANALYZE;
|
||||
|
||||
-- 8. Erstelle fehlende Indizes für bessere Performance
|
||||
|
||||
-- Links Collection Indizes
|
||||
CREATE INDEX IF NOT EXISTS idx_links_user ON links(user);
|
||||
CREATE INDEX IF NOT EXISTS idx_links_short_code ON links(short_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_links_active ON links(active);
|
||||
CREATE INDEX IF NOT EXISTS idx_links_created ON links(created);
|
||||
CREATE INDEX IF NOT EXISTS idx_links_user_active ON links(user, active);
|
||||
CREATE INDEX IF NOT EXISTS idx_links_user_created ON links(user, created DESC);
|
||||
|
||||
-- Analytics Collection Indizes
|
||||
CREATE INDEX IF NOT EXISTS idx_analytics_link ON analytics(link);
|
||||
CREATE INDEX IF NOT EXISTS idx_analytics_created ON analytics(created);
|
||||
CREATE INDEX IF NOT EXISTS idx_analytics_link_created ON analytics(link, created DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_analytics_country ON analytics(country);
|
||||
CREATE INDEX IF NOT EXISTS idx_analytics_device ON analytics(device);
|
||||
|
||||
-- Users Collection Indizes (falls vorhanden)
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_created ON users(created);
|
||||
|
||||
-- Tags Collection Indizes (falls vorhanden)
|
||||
CREATE INDEX IF NOT EXISTS idx_tags_user_id ON tags(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tags_slug ON tags(slug);
|
||||
CREATE INDEX IF NOT EXISTS idx_tags_is_public ON tags(is_public);
|
||||
|
||||
-- Link Tags Junction Table Indizes
|
||||
CREATE INDEX IF NOT EXISTS idx_linktags_link_id ON linktags(link_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_linktags_tag_id ON linktags(tag_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_linktags_composite ON linktags(link_id, tag_id);
|
||||
|
||||
-- Clicks/Analytics Performance Indizes für häufige Queries
|
||||
CREATE INDEX IF NOT EXISTS idx_analytics_link_country ON analytics(link, country);
|
||||
CREATE INDEX IF NOT EXISTS idx_analytics_link_device ON analytics(link, device);
|
||||
|
||||
-- Composite Index für Dashboard Queries
|
||||
CREATE INDEX IF NOT EXISTS idx_links_user_active_created ON links(user, active, created DESC);
|
||||
|
||||
-- Vacuum und Reindex für sofortige Verbesserung
|
||||
VACUUM;
|
||||
REINDEX;
|
||||
|
||||
-- Statistiken neu berechnen
|
||||
ANALYZE;
|
||||
243
apps/uload/scripts/seed-local-db.js
Executable file
243
apps/uload/scripts/seed-local-db.js
Executable file
|
|
@ -0,0 +1,243 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Seed Script for Local PocketBase Development
|
||||
*
|
||||
* This script creates test data for local development.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Make sure PocketBase is running locally (http://localhost:8090)
|
||||
* 2. Run: node scripts/seed-local-db.js
|
||||
*/
|
||||
|
||||
import PocketBase from 'pocketbase';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
// Configuration
|
||||
const POCKETBASE_URL = process.env.PUBLIC_POCKETBASE_URL || 'http://localhost:8090';
|
||||
const ADMIN_EMAIL = process.env.POCKETBASE_ADMIN_EMAIL || 'admin@localhost';
|
||||
const ADMIN_PASSWORD = process.env.POCKETBASE_ADMIN_PASSWORD || 'admin123456';
|
||||
|
||||
console.log('🌱 Seeding Local PocketBase Database');
|
||||
console.log('=====================================\n');
|
||||
console.log(`📍 PocketBase URL: ${POCKETBASE_URL}`);
|
||||
|
||||
const pb = new PocketBase(POCKETBASE_URL);
|
||||
pb.autoCancellation(false);
|
||||
|
||||
// Test data
|
||||
const testUsers = [
|
||||
{
|
||||
email: 'test@localhost',
|
||||
password: 'test123456',
|
||||
passwordConfirm: 'test123456',
|
||||
username: 'testuser',
|
||||
name: 'Test User',
|
||||
emailVisibility: true
|
||||
},
|
||||
{
|
||||
email: 'demo@localhost',
|
||||
password: 'demo123456',
|
||||
passwordConfirm: 'demo123456',
|
||||
username: 'demouser',
|
||||
name: 'Demo User',
|
||||
emailVisibility: true
|
||||
}
|
||||
];
|
||||
|
||||
const testLinks = [
|
||||
{
|
||||
short_code: 'test1',
|
||||
original_url: 'https://example.com',
|
||||
title: 'Test Link 1',
|
||||
description: 'This is a test link for development',
|
||||
is_active: true,
|
||||
click_limit: null,
|
||||
expires_at: null,
|
||||
password: null
|
||||
},
|
||||
{
|
||||
short_code: 'test2',
|
||||
original_url: 'https://google.com',
|
||||
title: 'Google Test',
|
||||
description: 'Link to Google for testing',
|
||||
is_active: true,
|
||||
click_limit: 100,
|
||||
expires_at: null,
|
||||
password: null
|
||||
},
|
||||
{
|
||||
short_code: 'protected',
|
||||
original_url: 'https://github.com',
|
||||
title: 'Protected Link',
|
||||
description: 'Password protected link',
|
||||
is_active: true,
|
||||
click_limit: null,
|
||||
expires_at: null,
|
||||
password: 'secret123'
|
||||
},
|
||||
{
|
||||
short_code: 'expired',
|
||||
original_url: 'https://stackoverflow.com',
|
||||
title: 'Expired Link',
|
||||
description: 'This link has expired',
|
||||
is_active: true,
|
||||
click_limit: null,
|
||||
expires_at: new Date(Date.now() - 86400000).toISOString(), // Yesterday
|
||||
password: null
|
||||
}
|
||||
];
|
||||
|
||||
async function seedDatabase() {
|
||||
try {
|
||||
// Step 1: Try to authenticate as admin
|
||||
console.log('🔐 Authenticating as admin...');
|
||||
try {
|
||||
await pb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
|
||||
console.log('✅ Admin authenticated\n');
|
||||
} catch (error) {
|
||||
console.log('⚠️ Admin auth failed. You may need to:');
|
||||
console.log(' 1. Create admin account at http://localhost:8090/_/');
|
||||
console.log(' 2. Update POCKETBASE_ADMIN_EMAIL and POCKETBASE_ADMIN_PASSWORD\n');
|
||||
|
||||
// Try to continue without admin auth (some operations might fail)
|
||||
}
|
||||
|
||||
// Step 2: Create test users
|
||||
console.log('👥 Creating test users...');
|
||||
const createdUsers = [];
|
||||
|
||||
for (const userData of testUsers) {
|
||||
try {
|
||||
const user = await pb.collection('users').create(userData);
|
||||
createdUsers.push(user);
|
||||
console.log(` ✅ Created user: ${userData.email}`);
|
||||
} catch (error) {
|
||||
if (error.response?.data?.email?.message?.includes('already exists')) {
|
||||
console.log(` ⚠️ User ${userData.email} already exists`);
|
||||
// Try to get existing user
|
||||
try {
|
||||
const users = await pb.collection('users').getList(1, 1, {
|
||||
filter: `email = "${userData.email}"`
|
||||
});
|
||||
if (users.items.length > 0) {
|
||||
createdUsers.push(users.items[0]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(` ❌ Could not fetch existing user: ${userData.email}`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ❌ Failed to create user ${userData.email}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Step 3: Create test links
|
||||
console.log('🔗 Creating test links...');
|
||||
|
||||
// Use the first created user as the owner
|
||||
const ownerId = createdUsers[0]?.id;
|
||||
|
||||
for (const linkData of testLinks) {
|
||||
try {
|
||||
// Add owner if we have one
|
||||
if (ownerId) {
|
||||
linkData.user_id = ownerId;
|
||||
}
|
||||
|
||||
// Generate a random custom code if needed
|
||||
if (!linkData.custom_code) {
|
||||
linkData.custom_code = linkData.short_code;
|
||||
}
|
||||
|
||||
const link = await pb.collection('links').create(linkData);
|
||||
console.log(` ✅ Created link: ${linkData.short_code} -> ${linkData.original_url}`);
|
||||
} catch (error) {
|
||||
if (error.response?.data?.short_code?.message?.includes('already exists')) {
|
||||
console.log(` ⚠️ Link ${linkData.short_code} already exists`);
|
||||
} else {
|
||||
console.log(` ❌ Failed to create link ${linkData.short_code}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Step 4: Create some test clicks
|
||||
console.log('📊 Creating test click data...');
|
||||
|
||||
try {
|
||||
// Get one of the links we created
|
||||
const links = await pb.collection('links').getList(1, 1, {
|
||||
filter: 'short_code = "test1"'
|
||||
});
|
||||
|
||||
if (links.items.length > 0) {
|
||||
const link = links.items[0];
|
||||
|
||||
// Create some fake clicks
|
||||
const clickData = [
|
||||
{
|
||||
link_id: link.id,
|
||||
ip_hash: '127.0.0.1',
|
||||
user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
|
||||
browser: 'Chrome',
|
||||
device_type: 'Desktop',
|
||||
os: 'macOS',
|
||||
country: 'Germany',
|
||||
city: 'Munich',
|
||||
clicked_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
link_id: link.id,
|
||||
ip_hash: '192.168.1.1',
|
||||
user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0)',
|
||||
browser: 'Safari',
|
||||
device_type: 'Mobile',
|
||||
os: 'iOS',
|
||||
country: 'USA',
|
||||
city: 'New York',
|
||||
clicked_at: new Date(Date.now() - 3600000).toISOString() // 1 hour ago
|
||||
}
|
||||
];
|
||||
|
||||
for (const click of clickData) {
|
||||
try {
|
||||
await pb.collection('clicks').create(click);
|
||||
console.log(` ✅ Created test click from ${click.country}`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ Failed to create click:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ Could not create click data:`, error.message);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Summary
|
||||
console.log('=====================================');
|
||||
console.log('🎉 Database seeding complete!\n');
|
||||
console.log('📝 Test Accounts:');
|
||||
console.log(' Email: test@localhost');
|
||||
console.log(' Password: test123456\n');
|
||||
console.log('🔗 Test Links:');
|
||||
console.log(' http://localhost:5173/test1 - Normal link');
|
||||
console.log(' http://localhost:5173/test2 - Link with click limit');
|
||||
console.log(' http://localhost:5173/protected - Password: secret123');
|
||||
console.log(' http://localhost:5173/expired - Expired link\n');
|
||||
console.log('👉 Next: Open http://localhost:5173 and test the app!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Seeding failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the seeding
|
||||
seedDatabase().then(() => {
|
||||
process.exit(0);
|
||||
}).catch(error => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
59
apps/uload/scripts/setup-test-user.mjs
Normal file
59
apps/uload/scripts/setup-test-user.mjs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('https://pb.ulo.ad');
|
||||
|
||||
async function createTestUser() {
|
||||
const email = 'test@example.com';
|
||||
const password = 'test123456';
|
||||
const randomId = Math.random().toString(36).substring(2, 17);
|
||||
|
||||
console.log('Creating test user...');
|
||||
console.log('Email:', email);
|
||||
console.log('Password:', password);
|
||||
|
||||
try {
|
||||
// First check if user already exists
|
||||
const existing = await pb.collection('users').getList(1, 1, {
|
||||
filter: `email = "${email}"`
|
||||
});
|
||||
|
||||
if (existing.items.length > 0) {
|
||||
console.log('✅ Test user already exists!');
|
||||
console.log('ID:', existing.items[0].id);
|
||||
|
||||
// Try to login
|
||||
try {
|
||||
await pb.collection('users').authWithPassword(email, password);
|
||||
console.log('✅ Login successful with existing user!');
|
||||
} catch (err) {
|
||||
console.log('⚠️ User exists but password might be different');
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
// User doesn't exist, continue to create
|
||||
}
|
||||
|
||||
try {
|
||||
const userData = {
|
||||
id: randomId,
|
||||
email: email,
|
||||
password: password,
|
||||
passwordConfirm: password,
|
||||
emailVisibility: true,
|
||||
username: 'testuser'
|
||||
};
|
||||
|
||||
const newUser = await pb.collection('users').create(userData);
|
||||
console.log('✅ Test user created successfully!');
|
||||
console.log('ID:', newUser.id);
|
||||
|
||||
// Verify login works
|
||||
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||
console.log('✅ Login verified! Token:', authData.token.substring(0, 20) + '...');
|
||||
} catch (err) {
|
||||
console.error('❌ Failed to create test user:', err.response || err);
|
||||
}
|
||||
}
|
||||
|
||||
createTestUser().catch(console.error);
|
||||
54
apps/uload/scripts/test-email.js
Normal file
54
apps/uload/scripts/test-email.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('http://localhost:8090');
|
||||
|
||||
// Test email - ersetze mit deiner E-Mail
|
||||
const testEmail = 'test@example.com'; // HIER DEINE EMAIL EINGEBEN!
|
||||
|
||||
async function testEmailFunctions() {
|
||||
console.log('🔧 Testing PocketBase Email Functions...\n');
|
||||
|
||||
try {
|
||||
// 1. Test Password Reset
|
||||
console.log('1️⃣ Testing Password Reset Email...');
|
||||
try {
|
||||
await pb.collection('users').requestPasswordReset(testEmail);
|
||||
console.log('✅ Password reset email request sent successfully');
|
||||
console.log(' Check your inbox for the password reset email\n');
|
||||
} catch (error) {
|
||||
console.error('❌ Password reset failed:', error.message);
|
||||
console.log(' Error details:', error.response?.data || error);
|
||||
}
|
||||
|
||||
// 2. Test Verification Email (needs existing unverified user)
|
||||
console.log('2️⃣ Testing Verification Email...');
|
||||
try {
|
||||
await pb.collection('users').requestVerification(testEmail);
|
||||
console.log('✅ Verification email request sent successfully');
|
||||
console.log(' Check your inbox for the verification email\n');
|
||||
} catch (error) {
|
||||
console.error('❌ Verification email failed:', error.message);
|
||||
console.log(' Error details:', error.response?.data || error);
|
||||
}
|
||||
|
||||
// 3. Check PocketBase health
|
||||
console.log('3️⃣ Checking PocketBase health...');
|
||||
try {
|
||||
const health = await pb.health.check();
|
||||
console.log('✅ PocketBase is healthy:', health);
|
||||
} catch (error) {
|
||||
console.error('❌ PocketBase health check failed:', error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ General error:', error);
|
||||
}
|
||||
|
||||
console.log('\n📌 Important checks:');
|
||||
console.log('1. Is PocketBase running? (http://localhost:8090)');
|
||||
console.log('2. Are SMTP settings configured in PocketBase?');
|
||||
console.log('3. Is the Application URL set in PocketBase settings?');
|
||||
console.log('4. Check PocketBase logs: Admin → Logs');
|
||||
}
|
||||
|
||||
// Run tests
|
||||
testEmailFunctions();
|
||||
30
apps/uload/scripts/test-env.js
Normal file
30
apps/uload/scripts/test-env.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Test which PocketBase URL is being used
|
||||
console.log('Testing Environment Variables:');
|
||||
console.log('----------------------------');
|
||||
console.log('NODE_ENV:', process.env.NODE_ENV);
|
||||
console.log('PUBLIC_POCKETBASE_URL:', process.env.PUBLIC_POCKETBASE_URL);
|
||||
|
||||
// Check if .env file exists
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const envFiles = ['.env', '.env.development', '.env.production'];
|
||||
|
||||
envFiles.forEach(file => {
|
||||
const filePath = path.join(__dirname, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.log(`\n${file} exists`);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
lines.forEach(line => {
|
||||
if (line.includes('POCKETBASE_URL')) {
|
||||
console.log(` -> ${line}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\nNOTE: SvelteKit/Vite loads environment variables differently.');
|
||||
console.log('The app should use:');
|
||||
console.log('- Development: http://localhost:8090 (from .env or fallback)');
|
||||
console.log('- Production: https://pb.ulo.ad (from .env.production)');
|
||||
34
apps/uload/scripts/test-env.mjs
Normal file
34
apps/uload/scripts/test-env.mjs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Test which PocketBase URL is being used
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
console.log('Testing Environment Variables:');
|
||||
console.log('----------------------------');
|
||||
console.log('NODE_ENV:', process.env.NODE_ENV);
|
||||
console.log('PUBLIC_POCKETBASE_URL:', process.env.PUBLIC_POCKETBASE_URL);
|
||||
|
||||
const envFiles = ['.env', '.env.development', '.env.production'];
|
||||
|
||||
envFiles.forEach(file => {
|
||||
const filePath = join(__dirname, file);
|
||||
if (existsSync(filePath)) {
|
||||
console.log(`\n${file} exists`);
|
||||
const content = readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
lines.forEach(line => {
|
||||
if (line.includes('POCKETBASE_URL')) {
|
||||
console.log(` -> ${line}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\nNOTE: SvelteKit/Vite loads environment variables differently.');
|
||||
console.log('The app should use:');
|
||||
console.log('- Development: http://localhost:8090 (from .env or fallback)');
|
||||
console.log('- Production: https://pb.ulo.ad (from .env.production)');
|
||||
48
apps/uload/scripts/test-local-redis.mjs
Normal file
48
apps/uload/scripts/test-local-redis.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import Redis from 'ioredis';
|
||||
|
||||
console.log('🔍 Testing Local Redis Connection\n');
|
||||
console.log('==================================\n');
|
||||
|
||||
// Local Redis configuration
|
||||
const redis = new Redis({
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
// No password for local Redis
|
||||
});
|
||||
|
||||
async function testConnection() {
|
||||
try {
|
||||
// Test ping
|
||||
const pong = await redis.ping();
|
||||
console.log('✅ Redis is running locally!');
|
||||
console.log(` Response: ${pong}`);
|
||||
|
||||
// Test set/get
|
||||
await redis.set('test:local', 'Hello from local Redis!');
|
||||
const value = await redis.get('test:local');
|
||||
console.log(` Test value: ${value}`);
|
||||
|
||||
// Clean up
|
||||
await redis.del('test:local');
|
||||
|
||||
console.log('\n✅ All tests passed! Local Redis is working.');
|
||||
console.log('\n📝 Next steps:');
|
||||
console.log(' 1. Run "npm run dev" to start the app');
|
||||
console.log(' 2. Visit a short link');
|
||||
console.log(' 3. Check console for "Redis: Connected successfully"');
|
||||
console.log(' 4. Refresh the link - should be faster (cache hit)');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Redis connection failed:', error.message);
|
||||
console.log('\nTroubleshooting:');
|
||||
console.log(' 1. Check if Redis is running: brew services list');
|
||||
console.log(' 2. Start Redis: brew services start redis');
|
||||
console.log(' 3. Test connection: redis-cli ping');
|
||||
} finally {
|
||||
redis.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
testConnection();
|
||||
35
apps/uload/scripts/test-pb-connection.js
Normal file
35
apps/uload/scripts/test-pb-connection.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('http://localhost:8090');
|
||||
|
||||
console.log('Testing PocketBase connection...');
|
||||
|
||||
try {
|
||||
// Test health endpoint
|
||||
const health = await fetch('http://localhost:8090/api/health');
|
||||
const healthData = await health.json();
|
||||
console.log('Health check:', healthData);
|
||||
|
||||
// Try to create a test user
|
||||
const testUser = await pb.collection('users').create({
|
||||
email: `test${Date.now()}@example.com`,
|
||||
password: 'testpassword123',
|
||||
passwordConfirm: 'testpassword123',
|
||||
username: `testuser${Date.now()}`,
|
||||
name: 'Test User',
|
||||
emailVisibility: true
|
||||
});
|
||||
|
||||
console.log('✅ User created successfully:', testUser.id);
|
||||
console.log('Username:', testUser.username);
|
||||
console.log('Email:', testUser.email);
|
||||
|
||||
// Clean up - delete test user
|
||||
await pb.collection('users').delete(testUser.id);
|
||||
console.log('✅ Test user deleted');
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error.message);
|
||||
if (error.data) {
|
||||
console.error('Error details:', JSON.stringify(error.data, null, 2));
|
||||
}
|
||||
}
|
||||
40
apps/uload/scripts/test-pb-prod.js
Normal file
40
apps/uload/scripts/test-pb-prod.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('https://pb.ulo.ad');
|
||||
|
||||
async function test() {
|
||||
try {
|
||||
console.log('Testing connection to PocketBase Production...');
|
||||
|
||||
// Test 1: Get all users
|
||||
const users = await pb.collection('users').getList(1, 10);
|
||||
console.log('Found users:', users.items.length);
|
||||
users.items.forEach((u) => console.log(` - username: "${u.username}" (${u.email})`));
|
||||
|
||||
// Test 2: Find specific user
|
||||
try {
|
||||
const user = await pb.collection('users').getFirstListItem('username="memoro"');
|
||||
console.log('\nFound specific user:', user.username, user.id);
|
||||
|
||||
// Test 3: Get folders for user
|
||||
const folders = await pb.collection('folders').getList(1, 50, {
|
||||
filter: `user_id="${user.id}" && is_public=true`
|
||||
});
|
||||
console.log('Public folders:', folders.items.length);
|
||||
|
||||
// Test 4: Get links for user
|
||||
const links = await pb.collection('links').getList(1, 100, {
|
||||
filter: `user_id="${user.id}" && is_active=true`
|
||||
});
|
||||
console.log('Active links:', links.items.length);
|
||||
} catch (err) {
|
||||
console.log('\nCould not find user "memoro"');
|
||||
console.log('Error:', err.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error:', err.message);
|
||||
console.error('Full error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
test();
|
||||
35
apps/uload/scripts/test-pb.js
Normal file
35
apps/uload/scripts/test-pb.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||
|
||||
async function test() {
|
||||
try {
|
||||
console.log('Testing connection to PocketBase...');
|
||||
|
||||
// Test 1: Get all users
|
||||
const users = await pb.collection('users').getList(1, 10);
|
||||
console.log('Found users:', users.items.length);
|
||||
users.items.forEach((u) => console.log(` - username: "${u.username}" (${u.email})`));
|
||||
|
||||
// Test 2: Find specific user
|
||||
const user = await pb.collection('users').getFirstListItem('username="memoro"');
|
||||
console.log('\nFound specific user:', user.username, user.id);
|
||||
|
||||
// Test 3: Get folders for user
|
||||
const folders = await pb.collection('folders').getList(1, 50, {
|
||||
filter: `user_id="${user.id}" && is_public=true`
|
||||
});
|
||||
console.log('Public folders:', folders.items.length);
|
||||
|
||||
// Test 4: Get links for user
|
||||
const links = await pb.collection('links').getList(1, 100, {
|
||||
filter: `user_id="${user.id}" && is_active=true`
|
||||
});
|
||||
console.log('Active links:', links.items.length);
|
||||
} catch (err) {
|
||||
console.error('Error:', err.message);
|
||||
console.error('Full error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
test();
|
||||
71
apps/uload/scripts/test-pocketbase.js
Normal file
71
apps/uload/scripts/test-pocketbase.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
const POCKETBASE_URL = process.env.PUBLIC_POCKETBASE_URL || 'http://localhost:8090';
|
||||
|
||||
console.log('Testing PocketBase connection...');
|
||||
console.log('URL:', POCKETBASE_URL);
|
||||
|
||||
const pb = new PocketBase(POCKETBASE_URL);
|
||||
|
||||
async function testConnection() {
|
||||
try {
|
||||
// Test 1: Check health endpoint
|
||||
const health = await pb.health.check();
|
||||
console.log('✓ Health check passed:', health);
|
||||
|
||||
// Test 2: List collections
|
||||
const collections = await pb.collections.getList();
|
||||
console.log(
|
||||
'✓ Collections found:',
|
||||
collections.items.map((c) => c.name)
|
||||
);
|
||||
|
||||
// Test 3: Check users collection
|
||||
const usersCollection = await pb.collections.getOne('users');
|
||||
console.log('✓ Users collection schema:', {
|
||||
name: usersCollection.name,
|
||||
fields: usersCollection.schema.map((f) => ({
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
required: f.required
|
||||
}))
|
||||
});
|
||||
|
||||
// Test 4: Try to list users (might fail due to permissions)
|
||||
try {
|
||||
const users = await pb.collection('users').getList(1, 1);
|
||||
console.log('✓ Can list users:', users.totalItems, 'total users');
|
||||
} catch (e) {
|
||||
console.log('⚠ Cannot list users (probably permission issue):', e.message);
|
||||
}
|
||||
|
||||
// Test 5: Test registration endpoint
|
||||
console.log('\nTesting registration capability...');
|
||||
const testEmail = `test${Date.now()}@example.com`;
|
||||
try {
|
||||
const result = await pb.collection('users').create({
|
||||
email: testEmail,
|
||||
password: 'Test123456!',
|
||||
passwordConfirm: 'Test123456!',
|
||||
username: `testuser${Date.now()}`
|
||||
});
|
||||
console.log('✓ Registration test successful, user created:', result.id);
|
||||
// Clean up test user
|
||||
await pb.collection('users').delete(result.id);
|
||||
console.log('✓ Test user cleaned up');
|
||||
} catch (e) {
|
||||
console.error('✗ Registration failed:', e.response || e.message);
|
||||
if (e.response?.data) {
|
||||
console.error('Error details:', JSON.stringify(e.response.data, null, 2));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('✗ Connection failed:', error.message);
|
||||
if (error.response) {
|
||||
console.error('Response:', error.response);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
testConnection();
|
||||
138
apps/uload/scripts/test-prod-pocketbase.js
Normal file
138
apps/uload/scripts/test-prod-pocketbase.js
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
// Produktions-PocketBase URL
|
||||
const PROD_POCKETBASE_URL = 'http://pocketbase-xs0ccokk8s0goko4w40gwc0w.91.99.221.179.sslip.io';
|
||||
|
||||
console.log('Testing PRODUCTION PocketBase connection...');
|
||||
console.log('URL:', PROD_POCKETBASE_URL);
|
||||
console.log('----------------------------------------\n');
|
||||
|
||||
const pb = new PocketBase(PROD_POCKETBASE_URL);
|
||||
|
||||
async function testConnection() {
|
||||
try {
|
||||
// Test 1: Check health endpoint
|
||||
console.log('1. Testing health endpoint...');
|
||||
try {
|
||||
const response = await fetch(`${PROD_POCKETBASE_URL}/api/health`);
|
||||
const health = await response.json();
|
||||
console.log('✓ Health check:', health);
|
||||
} catch (e) {
|
||||
console.log('✗ Health check failed:', e.message);
|
||||
}
|
||||
|
||||
// Test 2: List collections (using MCP)
|
||||
console.log('\n2. Testing collections access...');
|
||||
try {
|
||||
const collections = await pb.collections.getList();
|
||||
console.log(
|
||||
'✓ Collections found:',
|
||||
collections.items.map((c) => c.name)
|
||||
);
|
||||
|
||||
// Check for users collection specifically
|
||||
const hasUsers = collections.items.some((c) => c.name === 'users');
|
||||
if (hasUsers) {
|
||||
console.log('✓ Users collection exists');
|
||||
} else {
|
||||
console.log('✗ Users collection NOT found!');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('✗ Cannot list collections:', e.message);
|
||||
}
|
||||
|
||||
// Test 3: Check users collection schema
|
||||
console.log('\n3. Checking users collection schema...');
|
||||
try {
|
||||
const usersCollection = await pb.collections.getOne('users');
|
||||
console.log('✓ Users collection fields:');
|
||||
usersCollection.schema.forEach((field) => {
|
||||
console.log(
|
||||
` - ${field.name}: ${field.type} ${field.required ? '(required)' : '(optional)'}`
|
||||
);
|
||||
});
|
||||
|
||||
// Check authentication settings
|
||||
console.log('\n✓ Authentication settings:');
|
||||
console.log(
|
||||
` - Password auth enabled: ${usersCollection.options?.allowEmailAuth || false}`
|
||||
);
|
||||
console.log(` - OAuth2 enabled: ${usersCollection.options?.allowOAuth2Auth || false}`);
|
||||
} catch (e) {
|
||||
console.log('✗ Cannot get users collection:', e.message);
|
||||
}
|
||||
|
||||
// Test 4: Check API rules
|
||||
console.log('\n4. Checking API rules for users collection...');
|
||||
try {
|
||||
const usersCollection = await pb.collections.getOne('users');
|
||||
console.log('API Rules:');
|
||||
console.log(` - List rule: ${usersCollection.listRule || 'none'}`);
|
||||
console.log(` - View rule: ${usersCollection.viewRule || 'none'}`);
|
||||
console.log(` - Create rule: ${usersCollection.createRule || 'none'}`);
|
||||
console.log(` - Update rule: ${usersCollection.updateRule || 'none'}`);
|
||||
console.log(` - Delete rule: ${usersCollection.deleteRule || 'none'}`);
|
||||
} catch (e) {
|
||||
console.log('✗ Cannot check API rules:', e.message);
|
||||
}
|
||||
|
||||
// Test 5: Test registration endpoint
|
||||
console.log('\n5. Testing registration endpoint...');
|
||||
const testEmail = `test${Date.now()}@example.com`;
|
||||
const testUsername = `testuser${Date.now()}`;
|
||||
|
||||
try {
|
||||
console.log(` Attempting to register: ${testEmail}`);
|
||||
const result = await pb.collection('users').create({
|
||||
email: testEmail,
|
||||
password: 'Test123456!',
|
||||
passwordConfirm: 'Test123456!',
|
||||
username: testUsername
|
||||
});
|
||||
console.log('✓ Registration successful! User ID:', result.id);
|
||||
|
||||
// Try to delete test user
|
||||
try {
|
||||
await pb.collection('users').delete(result.id);
|
||||
console.log('✓ Test user cleaned up');
|
||||
} catch (e) {
|
||||
console.log('⚠ Could not clean up test user:', e.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('✗ Registration failed!');
|
||||
console.error(' Error:', e.message);
|
||||
if (e.response?.data) {
|
||||
console.error(' Details:', JSON.stringify(e.response.data, null, 2));
|
||||
}
|
||||
if (e.data) {
|
||||
console.error(' Data:', JSON.stringify(e.data, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
// Test 6: Check CORS settings
|
||||
console.log('\n6. Checking CORS...');
|
||||
try {
|
||||
const response = await fetch(`${PROD_POCKETBASE_URL}/api/collections`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Origin: 'https://your-frontend-domain.com'
|
||||
}
|
||||
});
|
||||
console.log(
|
||||
'✓ CORS headers present:',
|
||||
response.headers.get('access-control-allow-origin') || 'not set'
|
||||
);
|
||||
} catch (e) {
|
||||
console.log('✗ CORS check failed:', e.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('\n✗ Connection test failed:', error.message);
|
||||
if (error.response) {
|
||||
console.error('Response:', error.response);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the connection test
|
||||
testConnection();
|
||||
232
apps/uload/scripts/test-redis-cache.js
Normal file
232
apps/uload/scripts/test-redis-cache.js
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import 'dotenv/config';
|
||||
import { redis, cache, ensureRedisConnection } from './src/lib/server/redis.js';
|
||||
import { linkCache } from './src/lib/server/linkCache.js';
|
||||
|
||||
console.log('🔍 Testing Redis Cache for Link Redirections\n');
|
||||
console.log('==========================================\n');
|
||||
|
||||
async function testRedisConnection() {
|
||||
console.log('1. Testing Redis Connection...');
|
||||
const connected = await ensureRedisConnection();
|
||||
|
||||
if (connected) {
|
||||
console.log('✅ Redis connected successfully');
|
||||
console.log(` Host: ${process.env.REDIS_HOST || 'ycsoowwsc84s0s8gc8oooosk'}`);
|
||||
console.log(` Port: ${process.env.REDIS_PORT || '6379'}\n`);
|
||||
return true;
|
||||
} else {
|
||||
console.log('❌ Redis connection failed\n');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testBasicCacheOperations() {
|
||||
console.log('2. Testing Basic Cache Operations...');
|
||||
|
||||
// Test set and get
|
||||
const testKey = 'test:key';
|
||||
const testValue = { message: 'Hello Redis', timestamp: Date.now() };
|
||||
|
||||
await cache.set(testKey, testValue, 60);
|
||||
console.log(' Set test value in cache');
|
||||
|
||||
const retrieved = await cache.get(testKey);
|
||||
console.log(' Retrieved value:', retrieved);
|
||||
|
||||
if (retrieved && retrieved.message === testValue.message) {
|
||||
console.log('✅ Basic cache operations working\n');
|
||||
|
||||
// Clean up
|
||||
await cache.del(testKey);
|
||||
return true;
|
||||
} else {
|
||||
console.log('❌ Basic cache operations failed\n');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testLinkCaching() {
|
||||
console.log('3. Testing Link Cache Functions...');
|
||||
|
||||
const testShortCode = 'test123';
|
||||
const testUrl = 'https://example.com/test';
|
||||
|
||||
// Cache a redirect
|
||||
await linkCache.cacheRedirect(testShortCode, testUrl);
|
||||
console.log(` Cached redirect: ${testShortCode} -> ${testUrl}`);
|
||||
|
||||
// Try to retrieve it
|
||||
const cachedUrl = await linkCache.getRedirectUrl(testShortCode);
|
||||
console.log(` Retrieved URL: ${cachedUrl}`);
|
||||
|
||||
if (cachedUrl === testUrl) {
|
||||
console.log('✅ Link caching working correctly\n');
|
||||
|
||||
// Clean up
|
||||
await linkCache.invalidate(testShortCode);
|
||||
return true;
|
||||
} else {
|
||||
console.log('❌ Link caching failed\n');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkCachedLinks() {
|
||||
console.log('4. Checking Currently Cached Links...');
|
||||
|
||||
try {
|
||||
// Get all keys matching redirect pattern
|
||||
const keys = await redis.keys('redirect:*');
|
||||
console.log(` Found ${keys.length} cached redirects`);
|
||||
|
||||
if (keys.length > 0) {
|
||||
console.log('\n Sample cached links:');
|
||||
for (const key of keys.slice(0, 5)) {
|
||||
const url = await redis.get(key);
|
||||
const ttl = await redis.ttl(key);
|
||||
const shortCode = key.replace('redirect:', '');
|
||||
console.log(` - ${shortCode}: ${url} (TTL: ${ttl}s)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check trending links
|
||||
const trending = await linkCache.getTrendingLinks(5);
|
||||
if (trending.length > 0) {
|
||||
console.log('\n Trending links:');
|
||||
trending.forEach((code, i) => {
|
||||
console.log(` ${i + 1}. ${code}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ Cache inspection complete\n');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log('❌ Failed to inspect cache:', error.message, '\n');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function performanceTest() {
|
||||
console.log('5. Performance Test (Cache vs No Cache)...');
|
||||
|
||||
const testCode = 'perf-test';
|
||||
const testUrl = 'https://example.com/performance';
|
||||
|
||||
// Test without cache (simulate)
|
||||
const dbStart = Date.now();
|
||||
await new Promise(resolve => setTimeout(resolve, 50)); // Simulate DB latency
|
||||
const dbTime = Date.now() - dbStart;
|
||||
console.log(` Database fetch simulation: ${dbTime}ms`);
|
||||
|
||||
// Cache the URL
|
||||
await linkCache.cacheRedirect(testCode, testUrl);
|
||||
|
||||
// Test with cache
|
||||
const cacheStart = Date.now();
|
||||
await linkCache.getRedirectUrl(testCode);
|
||||
const cacheTime = Date.now() - cacheStart;
|
||||
console.log(` Cache fetch: ${cacheTime}ms`);
|
||||
|
||||
const improvement = Math.round((dbTime - cacheTime) / dbTime * 100);
|
||||
console.log(` 🚀 Performance improvement: ${improvement}%`);
|
||||
|
||||
if (cacheTime < dbTime) {
|
||||
console.log('✅ Cache is faster than database\n');
|
||||
} else {
|
||||
console.log('⚠️ Cache performance needs investigation\n');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
await linkCache.invalidate(testCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function monitorLiveTraffic() {
|
||||
console.log('6. Monitoring Live Traffic (10 seconds)...');
|
||||
console.log(' Open your app and click some links to see cache activity\n');
|
||||
|
||||
// Subscribe to Redis monitor for 10 seconds
|
||||
const monitor = await redis.monitor();
|
||||
let commandCount = 0;
|
||||
let cacheHits = 0;
|
||||
let cacheSets = 0;
|
||||
|
||||
monitor.on('monitor', (time, args) => {
|
||||
const command = args[0];
|
||||
if (command === 'get' && args[1]?.includes('redirect:')) {
|
||||
cacheHits++;
|
||||
console.log(` 🎯 Cache GET: ${args[1]}`);
|
||||
} else if (command === 'setex' && args[1]?.includes('redirect:')) {
|
||||
cacheSets++;
|
||||
console.log(` 💾 Cache SET: ${args[1]} (TTL: ${args[2]}s)`);
|
||||
}
|
||||
commandCount++;
|
||||
});
|
||||
|
||||
// Stop monitoring after 10 seconds
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
monitor.disconnect();
|
||||
|
||||
console.log(`\n Monitoring complete:`);
|
||||
console.log(` - Total Redis commands: ${commandCount}`);
|
||||
console.log(` - Cache GETs: ${cacheHits}`);
|
||||
console.log(` - Cache SETs: ${cacheSets}`);
|
||||
console.log('✅ Live monitoring complete\n');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Main test runner
|
||||
async function runAllTests() {
|
||||
console.log('Starting Redis Cache Tests...\n');
|
||||
|
||||
const tests = [
|
||||
testRedisConnection,
|
||||
testBasicCacheOperations,
|
||||
testLinkCaching,
|
||||
checkCachedLinks,
|
||||
performanceTest
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const test of tests) {
|
||||
try {
|
||||
const result = await test();
|
||||
if (result) passed++;
|
||||
else failed++;
|
||||
} catch (error) {
|
||||
console.log(`❌ Test failed with error: ${error.message}\n`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('==========================================');
|
||||
console.log(`Test Results: ${passed} passed, ${failed} failed`);
|
||||
|
||||
// Optional: Run live monitoring
|
||||
console.log('\nWould you like to monitor live traffic? (Ctrl+C to skip)');
|
||||
console.log('Starting in 3 seconds...\n');
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
try {
|
||||
await monitorLiveTraffic();
|
||||
} catch (error) {
|
||||
console.log('Monitoring cancelled');
|
||||
}
|
||||
|
||||
// Close Redis connection
|
||||
redis.disconnect();
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
// Run tests
|
||||
runAllTests().catch(error => {
|
||||
console.error('Test suite failed:', error);
|
||||
redis.disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
268
apps/uload/scripts/test-redis-cache.mjs
Normal file
268
apps/uload/scripts/test-redis-cache.mjs
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import Redis from 'ioredis';
|
||||
|
||||
console.log('🔍 Testing Redis Cache for Link Redirections\n');
|
||||
console.log('==========================================\n');
|
||||
|
||||
// Redis Configuration (same as in your app)
|
||||
const redisConfig = {
|
||||
host: process.env.REDIS_HOST || 'ycsoowwsc84s0s8gc8oooosk',
|
||||
port: parseInt(process.env.REDIS_PORT || '6379'),
|
||||
username: process.env.REDIS_USERNAME || 'default',
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
retryDelayOnFailover: 100,
|
||||
maxRetriesPerRequest: 3
|
||||
};
|
||||
|
||||
const redis = new Redis(redisConfig);
|
||||
|
||||
// Helper functions
|
||||
const cache = {
|
||||
async get(key) {
|
||||
const value = await redis.get(key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
},
|
||||
async set(key, value, ttlSeconds = 3600) {
|
||||
await redis.setex(key, ttlSeconds, JSON.stringify(value));
|
||||
},
|
||||
async del(key) {
|
||||
await redis.del(key);
|
||||
}
|
||||
};
|
||||
|
||||
async function testRedisConnection() {
|
||||
console.log('1. Testing Redis Connection...');
|
||||
|
||||
try {
|
||||
await redis.ping();
|
||||
console.log('✅ Redis connected successfully');
|
||||
console.log(` Host: ${redisConfig.host}`);
|
||||
console.log(` Port: ${redisConfig.port}\n`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log('❌ Redis connection failed:', error.message);
|
||||
console.log(' Make sure Redis is running and credentials are correct\n');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testBasicCacheOperations() {
|
||||
console.log('2. Testing Basic Cache Operations...');
|
||||
|
||||
const testKey = 'test:key';
|
||||
const testValue = { message: 'Hello Redis', timestamp: Date.now() };
|
||||
|
||||
try {
|
||||
await cache.set(testKey, testValue, 60);
|
||||
console.log(' ✓ Set test value in cache');
|
||||
|
||||
const retrieved = await cache.get(testKey);
|
||||
console.log(' ✓ Retrieved value:', retrieved);
|
||||
|
||||
if (retrieved && retrieved.message === testValue.message) {
|
||||
console.log('✅ Basic cache operations working\n');
|
||||
await cache.del(testKey);
|
||||
return true;
|
||||
} else {
|
||||
console.log('❌ Value mismatch in cache\n');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ Cache operation failed:', error.message, '\n');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testLinkCaching() {
|
||||
console.log('3. Testing Link Redirect Cache...');
|
||||
|
||||
const testShortCode = 'test123';
|
||||
const testUrl = 'https://example.com/test';
|
||||
const cacheKey = `redirect:${testShortCode}`;
|
||||
|
||||
try {
|
||||
// Cache a redirect (directly as string, not JSON)
|
||||
await redis.setex(cacheKey, 300, testUrl);
|
||||
console.log(` ✓ Cached redirect: ${testShortCode} -> ${testUrl}`);
|
||||
|
||||
// Retrieve it
|
||||
const cachedUrl = await redis.get(cacheKey);
|
||||
console.log(` ✓ Retrieved URL: ${cachedUrl}`);
|
||||
|
||||
if (cachedUrl === testUrl) {
|
||||
console.log('✅ Link caching working correctly\n');
|
||||
await redis.del(cacheKey);
|
||||
return true;
|
||||
} else {
|
||||
console.log('❌ Link cache retrieval failed\n');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ Link caching failed:', error.message, '\n');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkCachedLinks() {
|
||||
console.log('4. Checking Currently Cached Links...');
|
||||
|
||||
try {
|
||||
// Get all redirect keys
|
||||
const keys = await redis.keys('redirect:*');
|
||||
console.log(` Found ${keys.length} cached redirects`);
|
||||
|
||||
if (keys.length > 0) {
|
||||
console.log('\n Sample cached links (max 5):');
|
||||
for (const key of keys.slice(0, 5)) {
|
||||
const url = await redis.get(key);
|
||||
const ttl = await redis.ttl(key);
|
||||
const shortCode = key.replace('redirect:', '');
|
||||
console.log(` - ${shortCode}: ${url?.substring(0, 50)}... (TTL: ${ttl}s)`);
|
||||
}
|
||||
} else {
|
||||
console.log(' No cached redirects found (this is normal if cache is cold)');
|
||||
}
|
||||
|
||||
// Check trending links
|
||||
const trending = await redis.zrevrange('trending:links', 0, 4);
|
||||
if (trending.length > 0) {
|
||||
console.log('\n Trending links:');
|
||||
trending.forEach((code, i) => {
|
||||
console.log(` ${i + 1}. ${code}`);
|
||||
});
|
||||
} else {
|
||||
console.log(' No trending data yet');
|
||||
}
|
||||
|
||||
console.log('✅ Cache inspection complete\n');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log('❌ Failed to inspect cache:', error.message, '\n');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function performanceTest() {
|
||||
console.log('5. Performance Test (Cache vs Simulated DB)...');
|
||||
|
||||
const testCode = 'perf-test';
|
||||
const testUrl = 'https://example.com/performance';
|
||||
const cacheKey = `redirect:${testCode}`;
|
||||
|
||||
try {
|
||||
// Simulate database fetch
|
||||
const dbStart = Date.now();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
const dbTime = Date.now() - dbStart;
|
||||
console.log(` Database fetch simulation: ${dbTime}ms`);
|
||||
|
||||
// Cache the URL
|
||||
await redis.setex(cacheKey, 300, testUrl);
|
||||
|
||||
// Test cache fetch
|
||||
const cacheStart = Date.now();
|
||||
await redis.get(cacheKey);
|
||||
const cacheTime = Date.now() - cacheStart;
|
||||
console.log(` Cache fetch: ${cacheTime}ms`);
|
||||
|
||||
const improvement = Math.round((dbTime - cacheTime) / dbTime * 100);
|
||||
console.log(` 🚀 Performance improvement: ~${improvement}%`);
|
||||
|
||||
if (cacheTime < dbTime) {
|
||||
console.log('✅ Cache is significantly faster\n');
|
||||
} else {
|
||||
console.log('⚠️ Cache performance unexpected\n');
|
||||
}
|
||||
|
||||
await redis.del(cacheKey);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log('❌ Performance test failed:', error.message, '\n');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testRealLink() {
|
||||
console.log('6. Testing with Real Application Flow...');
|
||||
|
||||
console.log(' Instructions:');
|
||||
console.log(' 1. Start your app: npm run dev');
|
||||
console.log(' 2. Visit a short link in your browser');
|
||||
console.log(' 3. Check the console output for cache HIT/MISS messages');
|
||||
console.log(' 4. Refresh the same link - should show "Cache HIT!"\n');
|
||||
|
||||
// Check if any real links are cached
|
||||
const realKeys = await redis.keys('redirect:*');
|
||||
if (realKeys.length > 0) {
|
||||
console.log(' Currently cached real links:');
|
||||
for (const key of realKeys.slice(0, 3)) {
|
||||
const ttl = await redis.ttl(key);
|
||||
console.log(` - ${key} (expires in ${ttl}s)`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Ready for real-world testing\n');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Main test runner
|
||||
async function runAllTests() {
|
||||
const tests = [
|
||||
testRedisConnection,
|
||||
testBasicCacheOperations,
|
||||
testLinkCaching,
|
||||
checkCachedLinks,
|
||||
performanceTest,
|
||||
testRealLink
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const test of tests) {
|
||||
try {
|
||||
const result = await test();
|
||||
if (result) passed++;
|
||||
else failed++;
|
||||
} catch (error) {
|
||||
console.log(`❌ Test crashed: ${error.message}\n`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('==========================================');
|
||||
console.log(`\n📊 Test Results: ${passed} passed, ${failed} failed`);
|
||||
|
||||
if (passed === tests.length) {
|
||||
console.log('🎉 All tests passed! Redis cache is working correctly.');
|
||||
} else if (failed === tests.length) {
|
||||
console.log('⚠️ All tests failed. Check your Redis configuration.');
|
||||
} else {
|
||||
console.log('⚠️ Some tests failed. Review the output above.');
|
||||
}
|
||||
|
||||
console.log('\n💡 Tips for verifying cache in production:');
|
||||
console.log(' - Check server logs for "Cache HIT!" messages');
|
||||
console.log(' - First visit: "Cache MISS" + redirect');
|
||||
console.log(' - Second visit: "Cache HIT!" + faster redirect');
|
||||
console.log(' - Cache TTL: 5 min (unpopular) or 24h (popular links)');
|
||||
|
||||
redis.disconnect();
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
redis.on('error', (err) => {
|
||||
console.error('Redis connection error:', err.message);
|
||||
console.error('Make sure Redis is running and accessible');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Run tests
|
||||
console.log('Connecting to Redis...\n');
|
||||
runAllTests().catch(error => {
|
||||
console.error('Test suite failed:', error);
|
||||
redis.disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
64
apps/uload/scripts/test-registration.js
Normal file
64
apps/uload/scripts/test-registration.js
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('http://localhost:8090');
|
||||
|
||||
// Generiere eine zufällige Test-E-Mail
|
||||
const timestamp = Date.now();
|
||||
const testEmail = `test${timestamp}@example.com`;
|
||||
const testPassword = 'TestPassword123!';
|
||||
|
||||
async function testRegistration() {
|
||||
console.log('🔧 Testing Registration and Verification Email...\n');
|
||||
console.log(`📧 Test email: ${testEmail}`);
|
||||
console.log(`🔑 Test password: ${testPassword}\n`);
|
||||
|
||||
try {
|
||||
// 1. Erstelle einen neuen User
|
||||
console.log('1️⃣ Creating new user...');
|
||||
const newUser = await pb.collection('users').create({
|
||||
email: testEmail,
|
||||
password: testPassword,
|
||||
passwordConfirm: testPassword,
|
||||
username: `user${timestamp}`,
|
||||
emailVisibility: true
|
||||
});
|
||||
|
||||
console.log('✅ User created successfully');
|
||||
console.log(' User ID:', newUser.id);
|
||||
console.log(' Verified:', newUser.verified);
|
||||
console.log('');
|
||||
|
||||
// PocketBase sollte automatisch eine Verification-E-Mail senden
|
||||
// wenn SMTP konfiguriert ist
|
||||
|
||||
// 2. Warte kurz
|
||||
console.log('⏳ Waiting 2 seconds...\n');
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// 3. Versuche manuell eine Verification-E-Mail zu senden
|
||||
console.log('2️⃣ Manually requesting verification email...');
|
||||
try {
|
||||
await pb.collection('users').requestVerification(testEmail);
|
||||
console.log('✅ Manual verification email request sent');
|
||||
console.log(' Check if you received 1 or 2 emails');
|
||||
} catch (error) {
|
||||
console.error('❌ Manual verification failed:', error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Registration failed:', error);
|
||||
if (error.response) {
|
||||
console.error(' Response:', error.response.data);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n📌 Check your email logs/inbox for:');
|
||||
console.log(` - Email to: ${testEmail}`);
|
||||
console.log(' - Should receive verification email(s)');
|
||||
console.log('\n📌 Also check:');
|
||||
console.log(' - PocketBase Admin → Logs');
|
||||
console.log(' - PocketBase Admin → Settings → Mail settings');
|
||||
console.log(' - Application URL is set correctly');
|
||||
}
|
||||
|
||||
// Run test
|
||||
testRegistration();
|
||||
76
apps/uload/scripts/test-url-variants.js
Normal file
76
apps/uload/scripts/test-url-variants.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
// Test verschiedene URL-Varianten
|
||||
const urls = [
|
||||
'http://pocketbase-xs0ccokk8s0goko4w40gwc0w.91.99.221.179.sslip.io', // ohne trailing slash
|
||||
'http://pocketbase-xs0ccokk8s0goko4w40gwc0w.91.99.221.179.sslip.io/' // mit trailing slash
|
||||
];
|
||||
|
||||
console.log('Testing different URL configurations...\n');
|
||||
|
||||
async function testUrl(url) {
|
||||
console.log(`Testing: ${url}`);
|
||||
console.log('-'.repeat(60));
|
||||
|
||||
try {
|
||||
const pb = new PocketBase(url);
|
||||
|
||||
// Test 1: Health check
|
||||
const healthUrl = `${url}/api/health`;
|
||||
const healthResponse = await fetch(healthUrl);
|
||||
console.log(`✓ Health check status: ${healthResponse.status}`);
|
||||
|
||||
// Test 2: Registration
|
||||
const testEmail = `test${Date.now()}@example.com`;
|
||||
const testUsername = `user${Date.now()}`;
|
||||
|
||||
const userData = {
|
||||
email: testEmail,
|
||||
password: 'TestPass123!',
|
||||
passwordConfirm: 'TestPass123!',
|
||||
username: testUsername,
|
||||
name: 'Test User',
|
||||
emailVisibility: true
|
||||
};
|
||||
|
||||
console.log(` Attempting registration with: ${testEmail}`);
|
||||
|
||||
try {
|
||||
const result = await pb.collection('users').create(userData);
|
||||
console.log(`✓ Registration successful! User ID: ${result.id}`);
|
||||
|
||||
// Clean up
|
||||
try {
|
||||
await pb.collection('users').delete(result.id);
|
||||
console.log('✓ Test user cleaned up');
|
||||
} catch (e) {
|
||||
console.log('⚠ Could not clean up test user');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`✗ Registration failed: ${err.message}`);
|
||||
if (err.response?.data) {
|
||||
console.error(' Error details:', JSON.stringify(err.response.data, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✓ URL configuration works!\n');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`✗ URL configuration failed: ${error.message}\n`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Test all URLs
|
||||
async function testAll() {
|
||||
for (const url of urls) {
|
||||
const success = await testUrl(url);
|
||||
if (success) {
|
||||
console.log(`\n🎉 WORKING URL: ${url}`);
|
||||
console.log('Use this exact URL in your environment variables (without quotes)');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testAll();
|
||||
Loading…
Add table
Add a link
Reference in a new issue