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

View File

@@ -0,0 +1,132 @@
import bcrypt from 'bcrypt';
import { eq } from 'drizzle-orm';
import { db } from '../config/database.js';
import { users } from '../db/schema/index.js';
const SALT_ROUNDS = 10;
/**
* Hash a password using bcrypt
* @param {string} password - Plain text password
* @returns {Promise<string>} Hashed password
*/
export async function hashPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS);
}
/**
* Verify a password against a hash
* @param {string} password - Plain text password
* @param {string} hash - Hashed password
* @returns {Promise<boolean>} True if password matches
*/
export async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
/**
* Register a new user
* @param {Object} userData - User registration data
* @param {string} userData.email - User email
* @param {string} userData.password - Plain text password
* @param {string} userData.callsign - Ham radio callsign
* @returns {Promise<Object>} Created user object (without password)
* @throws {Error} If email already exists
*/
export async function registerUser({ email, password, callsign }) {
// Check if user already exists
const existingUser = await db
.select()
.from(users)
.where(eq(users.email, email))
.get();
if (existingUser) {
throw new Error('Email already registered');
}
// Hash password
const passwordHash = await hashPassword(password);
// Create user
const newUser = await db
.insert(users)
.values({
email,
passwordHash,
callsign: callsign.toUpperCase(),
})
.returning();
// Return user without password hash
const { passwordHash: _, ...userWithoutPassword } = newUser[0];
return userWithoutPassword;
}
/**
* Authenticate user with email and password
* @param {string} email - User email
* @param {string} password - Plain text password
* @returns {Promise<Object>} User object (without password) if authenticated
* @throws {Error} If credentials are invalid
*/
export async function authenticateUser(email, password) {
// Find user by email
const user = await db
.select()
.from(users)
.where(eq(users.email, email))
.get();
if (!user) {
throw new Error('Invalid email or password');
}
// Verify password
const isValid = await verifyPassword(password, user.passwordHash);
if (!isValid) {
throw new Error('Invalid email or password');
}
// Return user without password hash
const { passwordHash: _, ...userWithoutPassword } = user;
return userWithoutPassword;
}
/**
* Get user by ID
* @param {number} userId - User ID
* @returns {Promise<Object|null>} User object (without password) or null
*/
export async function getUserById(userId) {
const user = await db
.select()
.from(users)
.where(eq(users.id, userId))
.get();
if (!user) return null;
const { passwordHash: _, ...userWithoutPassword } = user;
return userWithoutPassword;
}
/**
* Update user's LoTW credentials (encrypted)
* @param {number} userId - User ID
* @param {string} lotwUsername - LoTW username
* @param {string} lotwPassword - LoTW password (will be encrypted)
* @returns {Promise<void>}
*/
export async function updateLoTWCredentials(userId, lotwUsername, lotwPassword) {
// Simple encryption for storage (in production, use a proper encryption library)
// For now, we'll store as-is but marked for encryption
await db
.update(users)
.set({
lotwUsername,
lotwPassword, // TODO: Encrypt before storing
updatedAt: new Date(),
})
.where(eq(users.id, userId));
}