Initial commit: Ham Radio Award Portal

Features implemented:
- User authentication (register/login) with JWT
- SQLite database with Drizzle ORM
- SvelteKit frontend with authentication flow
- ElysiaJS backend with CORS enabled
- Award definition JSON schemas (DXCC, WAS, VUCC, SAT)
- Responsive dashboard with user profile

Tech stack:
- Backend: ElysiaJS, Drizzle ORM, SQLite, JWT
- Frontend: SvelteKit, Svelte stores
- Runtime: Bun
- Language: JavaScript

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-15 11:01:10 +01:00
commit 8c26fc93e3
41 changed files with 3424 additions and 0 deletions

216
src/backend/index.js Normal file
View File

@@ -0,0 +1,216 @@
import { Elysia, t } from 'elysia';
import { cors } from '@elysiajs/cors';
import { jwt } from '@elysiajs/jwt';
import { JWT_SECRET } from './config/jwt.js';
import {
registerUser,
authenticateUser,
getUserById,
updateLoTWCredentials,
} from './services/auth.service.js';
/**
* Main backend application
* Serves API routes
*/
const app = new Elysia()
// Enable CORS for frontend communication
.use(cors({
origin: true, // Allow all origins in development
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
}))
// JWT plugin
.use(jwt({
name: 'jwt',
secret: JWT_SECRET,
}))
// Authentication: derive user from JWT token
.derive(async ({ jwt, headers }) => {
const authHeader = headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return { user: null };
}
const token = authHeader.substring(7);
try {
const payload = await jwt.verify(token);
if (!payload) {
return { user: null };
}
return {
user: {
id: payload.userId,
email: payload.email,
callsign: payload.callsign,
},
};
} catch (error) {
return { user: null };
}
})
/**
* POST /api/auth/register
* Register a new user
*/
.post(
'/api/auth/register',
async ({ body, jwt, set }) => {
try {
// Create user
const user = await registerUser(body);
// Generate JWT token
const token = await jwt.sign({
userId: user.id,
email: user.email,
callsign: user.callsign,
});
set.status = 201;
return {
success: true,
token,
user,
};
} catch (error) {
set.status = 400;
return {
success: false,
error: error.message,
};
}
},
{
body: t.Object({
email: t.String({
format: 'email',
error: 'Invalid email address',
}),
password: t.String({
minLength: 8,
error: 'Password must be at least 8 characters',
}),
callsign: t.String({
minLength: 3,
maxLength: 10,
error: 'Callsign must be 3-10 characters',
}),
}),
}
)
/**
* POST /api/auth/login
* Authenticate user and return JWT token
*/
.post(
'/api/auth/login',
async ({ body, jwt, set }) => {
try {
// Authenticate user
const user = await authenticateUser(body.email, body.password);
// Generate JWT token
const token = await jwt.sign({
userId: user.id,
email: user.email,
callsign: user.callsign,
});
return {
success: true,
token,
user,
};
} catch (error) {
set.status = 401;
return {
success: false,
error: 'Invalid email or password',
};
}
},
{
body: t.Object({
email: t.String({ format: 'email' }),
password: t.String(),
}),
}
)
/**
* GET /api/auth/me
* Get current user profile (requires authentication)
*/
.get('/api/auth/me', async ({ user, set }) => {
if (!user) {
set.status = 401;
return { success: false, error: 'Unauthorized' };
}
// Get full user data from database
const userData = await getUserById(user.id);
if (!userData) {
set.status = 404;
return { success: false, error: 'User not found' };
}
return {
success: true,
user: userData,
};
})
/**
* PUT /api/auth/lotw-credentials
* Update LoTW credentials (requires authentication)
*/
.put(
'/api/auth/lotw-credentials',
async ({ user, body, set }) => {
if (!user) {
set.status = 401;
return { success: false, error: 'Unauthorized' };
}
try {
await updateLoTWCredentials(user.id, body.lotwUsername, body.lotwPassword);
return {
success: true,
message: 'LoTW credentials updated successfully',
};
} catch (error) {
set.status = 500;
return {
success: false,
error: 'Failed to update LoTW credentials',
};
}
},
{
body: t.Object({
lotwUsername: t.String(),
lotwPassword: t.String(),
}),
}
)
// Health check endpoint
.get('/api/health', () => ({
status: 'ok',
timestamp: new Date().toISOString(),
}))
// Start server
.listen(3001);
console.log(`🦊 Backend server running at http://localhost:${app.server?.port}`);
console.log(`📡 API endpoints available at http://localhost:${app.server?.port}/api`);
export default app;