Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# JWT Secret for authentication (CHANGE THIS TO A RANDOM STRING IN PRODUCTION!)
JWT_SECRET=change-this-to-a-secure-random-string-at-least-32-characters-long

# SQLite database is automatically created in data/skytracker.db
# No additional database configuration needed
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,11 @@ logs
.env.*
!.env.example

# Database
data/
*.db
*.db-shm
*.db-wal

bun.lockb
package-lock.json
99 changes: 99 additions & 0 deletions AUTH_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Authentication Setup

This application now uses a custom JWT-based authentication system instead of Clerk.

## Environment Variables

Add the following to your `.env` file:

```env
# JWT Secret for token signing (use a long random string in production)
JWT_SECRET=your-secure-random-secret-key-here

# Database (SQLite - no configuration needed, auto-created in data/ directory)
```

## Database

The application uses SQLite with better-sqlite3 and Drizzle ORM. The database file is automatically created at `data/skytracker.db` on first run.

### Schema

- **users**: User accounts with email/password authentication
- **user_settings**: User preferences and privacy settings
- **notifications**: User notifications
- **partial_skylanders**: Skylander figure data
- **daily_metadata**: Daily random skylander selection
- **user_wishlist**: User wishlist items
- **user_figures**: User collection items
- **user_watching**: Items user is watching
- **auth_sessions**: JWT token sessions (for future token revocation)

## Authentication Flow

### Registration
- POST `/api/auth/register`
- Required: email, username, password
- Optional: firstName
- Returns: user object and JWT token

### Login
- POST `/api/auth/login`
- Required: email, password
- Returns: user object and JWT token

### Token Verification
- GET `/api/auth/me`
- Headers: `ST-Auth-Token: <jwt-token>`
- Returns: current user object

## Frontend Usage

The `useAuth` composable provides authentication state and methods:

```vue
<script setup>
const { user, isSignedIn, login, logout, register } = useAuth();

// Login
const handleLogin = async () => {
const result = await login(email.value, password.value);
if (result.success) {
// Login successful
}
};

// Register
const handleRegister = async () => {
const result = await register(email.value, username.value, password.value);
if (result.success) {
// Registration successful
}
};

// Logout
const handleLogout = () => {
logout(); // Clears token and redirects to home
};
</script>
```

## Protected Routes

All API routes under `/api/v1` that require authentication use the `ST-Auth-Token` header. The middleware automatically validates the token and attaches the user object to the request context.

## Security Notes

1. **JWT_SECRET**: Use a long, random string in production (at least 32 characters)
2. **Password Hashing**: Passwords are hashed using bcrypt with 10 salt rounds
3. **Token Expiry**: JWT tokens expire after 7 days
4. **HTTPS**: Always use HTTPS in production to protect tokens in transit

## Migration from Clerk

If you have existing Clerk users, you'll need to:
1. Export user data from Clerk
2. Create a migration script to import users into the new SQLite database
3. Send password reset emails to all users

Note: Existing MongoDB data needs to be migrated to SQLite before the application will work properly.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ A Skylander collector's most useful multitool

Track your collection, wishlist your favorites, and watch prices on needs.
Skytracker is a website for Skylanders collectors to manage their collection, track prices of desired figures, and compare their collection with other avid collectors.

## Recent Major Updates

**Database & Authentication Migration (Latest)**
- ✅ Migrated from MongoDB to SQLite for simplified deployment
- ✅ Replaced Clerk with custom email/password authentication
- ✅ JWT-based token system for secure API access
- 📖 See [AUTH_SETUP.md](AUTH_SETUP.md) for setup instructions

## Features
- Track your Skylanders collection
- Manage your wishlist
- Watch prices on figures you want
- Compare collections with other collectors
- Browse all Skylanders figures by game, category, or element

Screenshots (taken 11/10/24):

Expand All @@ -28,3 +43,14 @@ Collection/wishlist/watchlist (they have the same general layout):

Profile:
![profile](https://github.com/user-attachments/assets/eb0b12e8-70aa-488e-bac4-440dab6e9b7b)

## Setup

1. Clone the repository
2. Copy `.env.example` to `.env` and update values (especially JWT_SECRET)
3. Install dependencies: `npm install`
4. Run development server: `npm run dev`
5. Build for production: `npm run build`

The SQLite database will be automatically created on first run.

83 changes: 83 additions & 0 deletions components/LoginModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script setup>
const { login } = useAuth();
const emit = defineEmits(['close', 'switchToRegister']);

const email = ref('');
const password = ref('');
const error = ref('');
const loading = ref(false);

const handleLogin = async () => {
error.value = '';
loading.value = true;

const result = await login(email.value, password.value);

loading.value = false;

if (result.success) {
emit('close');
} else {
error.value = result.error || 'Login failed';
}
};
</script>

<template>
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 max-w-md w-full mx-4">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold">Sign In</h2>
<button @click="emit('close')" class="text-gray-500 hover:text-gray-700">
<Icon name="ic:baseline-close" class="w-6 h-6" />
</button>
</div>

<form @submit.prevent="handleLogin">
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Email</label>
<input
v-model="email"
type="email"
required
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="your@email.com"
/>
</div>

<div class="mb-6">
<label class="block text-sm font-medium mb-2">Password</label>
<input
v-model="password"
type="password"
required
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="••••••••"
/>
</div>

<div v-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded-lg">
{{ error }}
</div>

<button
type="submit"
:disabled="loading"
class="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{{ loading ? 'Signing in...' : 'Sign In' }}
</button>

<div class="mt-4 text-center">
<button
type="button"
@click="emit('switchToRegister')"
class="text-blue-600 hover:underline"
>
Don't have an account? Sign up
</button>
</div>
</form>
</div>
</div>
</template>
107 changes: 107 additions & 0 deletions components/RegisterModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<script setup>
const { register } = useAuth();
const emit = defineEmits(['close', 'switchToLogin']);

const email = ref('');
const username = ref('');
const password = ref('');
const firstName = ref('');
const error = ref('');
const loading = ref(false);

const handleRegister = async () => {
error.value = '';
loading.value = true;

const result = await register(email.value, username.value, password.value, firstName.value);

loading.value = false;

if (result.success) {
emit('close');
} else {
error.value = result.error || 'Registration failed';
}
};
</script>

<template>
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 max-w-md w-full mx-4">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold">Sign Up</h2>
<button @click="emit('close')" class="text-gray-500 hover:text-gray-700">
<Icon name="ic:baseline-close" class="w-6 h-6" />
</button>
</div>

<form @submit.prevent="handleRegister">
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Email</label>
<input
v-model="email"
type="email"
required
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="your@email.com"
/>
</div>

<div class="mb-4">
<label class="block text-sm font-medium mb-2">Username</label>
<input
v-model="username"
type="text"
required
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="username"
/>
</div>

<div class="mb-4">
<label class="block text-sm font-medium mb-2">First Name (Optional)</label>
<input
v-model="firstName"
type="text"
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="John"
/>
</div>

<div class="mb-6">
<label class="block text-sm font-medium mb-2">Password</label>
<input
v-model="password"
type="password"
required
minlength="6"
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="••••••••"
/>
</div>

<div v-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded-lg">
{{ error }}
</div>

<button
type="submit"
:disabled="loading"
class="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{{ loading ? 'Creating account...' : 'Sign Up' }}
</button>

<div class="mt-4 text-center">
<button
type="button"
@click="emit('switchToLogin')"
class="text-blue-600 hover:underline"
>
Already have an account? Sign in
</button>
</div>
</form>
</div>
</div>
</template>
Loading