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:
216
src/backend/index.js
Normal file
216
src/backend/index.js
Normal 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;
|
||||
Reference in New Issue
Block a user