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:
132
src/backend/services/auth.service.js
Normal file
132
src/backend/services/auth.service.js
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user