From 928ce0ddc3512048549867b73ad33e9ef9008532 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Oct 2025 15:53:54 +0000
Subject: [PATCH 1/4] Initial plan
From 6f9c066acab9f9f9886a07a92bea0f752d8d6da2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Oct 2025 16:14:26 +0000
Subject: [PATCH 2/4] Complete database and auth migration from MongoDB/Clerk
to SQLite/Custom JWT
Co-authored-by: brandonapt <63559800+brandonapt@users.noreply.github.com>
---
.gitignore | 6 +
components/LoginModal.vue | 83 ++++
components/RegisterModal.vue | 107 ++++
components/navbar.vue | 64 ++-
components/tiles/shortcuts.vue | 43 +-
composables/useAuth.ts | 130 +++++
nuxt.config.ts | 1 -
package.json | 12 +-
pages/my/settings/index.vue | 31 +-
plugins/auth.client.ts | 9 +
schemas/dailyMetadata.ts | 15 -
schemas/notification.ts | 11 -
schemas/partialSkylander.ts | 22 -
schemas/user.ts | 55 ---
scripts/scrape.ts | 2 +-
server/api/auth/login.post.ts | 70 +++
server/api/auth/me.get.ts | 44 ++
server/api/auth/register.post.ts | 55 +++
server/api/v1/collections/fetch.ts | 2 +-
server/api/v1/collections/modify/[id].ts | 2 +-
server/api/v1/collections/user/[id].ts | 2 +-
server/api/v1/me/bio.ts | 6 +-
server/api/v1/me/metadata.ts | 10 +-
server/api/v1/messages/fetch.ts | 2 +-
server/api/v1/metadata/today.ts | 2 +-
server/api/v1/partials/[id].ts | 2 +-
server/api/v1/partials/all.ts | 2 +-
server/api/v1/partials/category/[category].ts | 2 +-
server/api/v1/partials/game/[game].ts | 2 +-
server/api/v1/partials/page/[page].ts | 2 +-
server/api/v1/searching/[term].ts | 2 +-
server/api/v1/settings/me/fetch.ts | 2 +-
server/api/v1/settings/me/update.ts | 2 +-
server/api/v1/skylanders/[id].ts | 2 +-
server/api/v1/users/[username]/fetch.ts | 23 +-
server/api/v1/watching/fetch.ts | 2 +-
server/api/v1/watching/modify/[id].ts | 2 +-
server/api/v1/watching/user/[id].ts | 2 +-
server/api/v1/wishlist/fetch.ts | 2 +-
server/api/v1/wishlist/modify/[id].ts | 2 +-
server/api/v1/wishlist/user/[id].ts | 2 +-
server/db/index.ts | 121 +++++
server/db/schema.ts | 90 ++++
server/middleware/auth.ts | 25 +-
server/utils/auth.ts | 53 ++
server/utils/database.ts | 455 ++++++++++++++++++
utils/database.ts | 296 ------------
47 files changed, 1372 insertions(+), 507 deletions(-)
create mode 100644 components/LoginModal.vue
create mode 100644 components/RegisterModal.vue
create mode 100644 composables/useAuth.ts
create mode 100644 plugins/auth.client.ts
delete mode 100644 schemas/dailyMetadata.ts
delete mode 100644 schemas/notification.ts
delete mode 100644 schemas/partialSkylander.ts
delete mode 100644 schemas/user.ts
create mode 100644 server/api/auth/login.post.ts
create mode 100644 server/api/auth/me.get.ts
create mode 100644 server/api/auth/register.post.ts
create mode 100644 server/db/index.ts
create mode 100644 server/db/schema.ts
create mode 100644 server/utils/auth.ts
create mode 100644 server/utils/database.ts
delete mode 100644 utils/database.ts
diff --git a/.gitignore b/.gitignore
index f29a6c4..d37907e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,5 +23,11 @@ logs
.env.*
!.env.example
+# Database
+data/
+*.db
+*.db-shm
+*.db-wal
+
bun.lockb
package-lock.json
\ No newline at end of file
diff --git a/components/LoginModal.vue b/components/LoginModal.vue
new file mode 100644
index 0000000..7f56a94
--- /dev/null
+++ b/components/LoginModal.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
Sign In
+
+
+
+
+
+
+
diff --git a/components/RegisterModal.vue b/components/RegisterModal.vue
new file mode 100644
index 0000000..487c79c
--- /dev/null
+++ b/components/RegisterModal.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
Sign Up
+
+
+
+
+
+
+
diff --git a/components/navbar.vue b/components/navbar.vue
index fc32042..519af40 100644
--- a/components/navbar.vue
+++ b/components/navbar.vue
@@ -2,8 +2,10 @@
const router = useRouter();
const colorMode = useColorMode();
const hovering = ref(false);
-const { isLoaded, isSignedIn, user } = useUser();
-const clerk = useClerk();
+const { isLoaded, isSignedIn, user, logout } = useAuth();
+
+const showLoginModal = ref(false);
+const showRegisterModal = ref(false);
const items = ["logo", "account"];
@@ -28,6 +30,26 @@ function hover() {
hovering.value = !hovering.value;
console.log(hovering.value);
}
+
+function openLoginModal() {
+ showLoginModal.value = true;
+ showRegisterModal.value = false;
+}
+
+function openRegisterModal() {
+ showRegisterModal.value = true;
+ showLoginModal.value = false;
+}
+
+function closeModals() {
+ showLoginModal.value = false;
+ showRegisterModal.value = false;
+}
+
+function handleLogout() {
+ logout();
+ hovering.value = false;
+}
@@ -51,15 +73,17 @@ function hover() {
@click="push('/')"
/>
-
+ >
+ Sign In
+
-
@@ -161,11 +185,11 @@ function hover() {
size="sm"
class="w-36"
truncate=""
- @click="push('/@' + user.username)"
- :label="'@' + user.username"
+ @click="push('/@' + user?.username)"
+ :label="'@' + user?.username"
>
-
@@ -245,5 +269,17 @@ function hover() {
+
+
+
+
diff --git a/components/tiles/shortcuts.vue b/components/tiles/shortcuts.vue
index 0082a61..719edb7 100644
--- a/components/tiles/shortcuts.vue
+++ b/components/tiles/shortcuts.vue
@@ -1,9 +1,19 @@
@@ -28,22 +38,21 @@ function push(link) {
-
-
+
+
\ No newline at end of file
diff --git a/composables/useAuth.ts b/composables/useAuth.ts
new file mode 100644
index 0000000..5d41768
--- /dev/null
+++ b/composables/useAuth.ts
@@ -0,0 +1,130 @@
+export const useAuth = () => {
+ const user = useState('auth-user', () => null);
+ const token = useState('auth-token', () => null);
+ const isLoaded = useState('auth-loaded', () => false);
+ const isSignedIn = computed(() => !!user.value && !!token.value);
+
+ // Initialize auth state from localStorage on client
+ const initAuth = () => {
+ if (process.client) {
+ const storedToken = localStorage.getItem('auth-token');
+ const storedUser = localStorage.getItem('auth-user');
+
+ if (storedToken && storedUser) {
+ token.value = storedToken;
+ user.value = JSON.parse(storedUser);
+ }
+
+ isLoaded.value = true;
+ }
+ };
+
+ // Login function
+ const login = async (email: string, password: string) => {
+ try {
+ const response = await $fetch('/api/auth/login', {
+ method: 'POST',
+ body: { email, password },
+ });
+
+ if (response.token && response.user) {
+ token.value = response.token;
+ user.value = response.user;
+
+ if (process.client) {
+ localStorage.setItem('auth-token', response.token);
+ localStorage.setItem('auth-user', JSON.stringify(response.user));
+ }
+
+ return { success: true };
+ }
+
+ return { success: false, error: 'Invalid response from server' };
+ } catch (error: any) {
+ return { success: false, error: error.data?.message || 'Login failed' };
+ }
+ };
+
+ // Register function
+ const register = async (email: string, username: string, password: string, firstName?: string) => {
+ try {
+ const response = await $fetch('/api/auth/register', {
+ method: 'POST',
+ body: { email, username, password, firstName },
+ });
+
+ if (response.token && response.user) {
+ token.value = response.token;
+ user.value = response.user;
+
+ if (process.client) {
+ localStorage.setItem('auth-token', response.token);
+ localStorage.setItem('auth-user', JSON.stringify(response.user));
+ }
+
+ return { success: true };
+ }
+
+ return { success: false, error: 'Invalid response from server' };
+ } catch (error: any) {
+ return { success: false, error: error.data?.message || 'Registration failed' };
+ }
+ };
+
+ // Logout function
+ const logout = () => {
+ token.value = null;
+ user.value = null;
+
+ if (process.client) {
+ localStorage.removeItem('auth-token');
+ localStorage.removeItem('auth-user');
+ }
+
+ // Redirect to home page
+ navigateTo('/');
+ };
+
+ // Verify token and get current user
+ const verifyAuth = async () => {
+ if (!token.value) {
+ isLoaded.value = true;
+ return false;
+ }
+
+ try {
+ const response = await $fetch('/api/auth/me', {
+ headers: {
+ 'ST-Auth-Token': token.value,
+ },
+ });
+
+ if (response.user) {
+ user.value = response.user;
+ if (process.client) {
+ localStorage.setItem('auth-user', JSON.stringify(response.user));
+ }
+ isLoaded.value = true;
+ return true;
+ }
+ } catch (error) {
+ // Token is invalid, clear auth state
+ logout();
+ }
+
+ isLoaded.value = true;
+ return false;
+ };
+
+ return {
+ user,
+ token,
+ isLoaded,
+ isSignedIn,
+ initAuth,
+ login,
+ register,
+ logout,
+ verifyAuth,
+ };
+};
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 8e32760..1c57d1a 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -5,7 +5,6 @@ export default defineNuxtConfig({
modules: [
"@nuxt/ui",
"@nuxt/icon",
- "vue-clerk/nuxt",
"@nuxt/image",
'@formkit/auto-animate/nuxt',
"@nuxtjs/mdc",
diff --git a/package.json b/package.json
index 23e9bc2..16e5e1a 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,6 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
- "@clerk/backend": "^1.13.5",
"@formkit/auto-animate": "^0.8.2",
"@iconify-json/material-symbols": "^1.2.1",
"@nuxt/icon": "^1.5.2",
@@ -18,21 +17,26 @@
"@nuxt/ui": "^2.18.6",
"@nuxtjs/mdc": "^0.9.2",
"@types/node-fetch": "^2.6.11",
+ "bcryptjs": "^3.0.2",
+ "better-sqlite3": "^12.4.1",
"chart.js": "^4.4.4",
"cheerio": "^1.0.0",
"dotenv": "^16.4.5",
+ "drizzle-orm": "^0.44.7",
"fs": "^0.0.1-security",
- "mongoose": "^8.7.0",
+ "jsonwebtoken": "^9.0.2",
"node-fetch": "^2.6.7",
"nuxt": "^3.13.0",
"nuxt-typed-router": "^3.7.0",
"nuxt-umami": "^3.0.2",
"vue": "latest",
"vue-chartjs": "^5.3.1",
- "vue-clerk": "^0.8.1",
"vue-router": "latest"
},
"devDependencies": {
- "@iconify-json/ic": "^1.2.1"
+ "@iconify-json/ic": "^1.2.1",
+ "@types/bcryptjs": "^2.4.6",
+ "@types/jsonwebtoken": "^9.0.10",
+ "drizzle-kit": "^0.31.6"
}
}
diff --git a/pages/my/settings/index.vue b/pages/my/settings/index.vue
index aaf9ce4..8d200ab 100644
--- a/pages/my/settings/index.vue
+++ b/pages/my/settings/index.vue
@@ -3,12 +3,10 @@ useHead({ title: "skytracker - settings" });
const settingsMetadata = ref();
const tabData = ref([]);
-const { user } = useUser();
+const { user, token, logout } = useAuth();
const page = ref("website");
-const clerk = useClerk();
const toast = useToast();
const userSettings = ref();
-const { getToken } = useAuth();
const route = useRoute();
const bio = ref("");
const privacySettings = ref({});
@@ -25,7 +23,7 @@ const goto = (path) => router.push(path);
async function fetchMetadata() {
const response = await fetch("/api/v1/settings/fetch");
settingsMetadata.value = await response.json();
- const me = await makeAuthenticatedRequest(`/api/v1/me/metadata`, await getToken.value());
+ const me = await makeAuthenticatedRequest(`/api/v1/me/metadata`, token.value);
bio.value = me.bio;
const arr = [];
console.log(user.value);
@@ -58,14 +56,14 @@ async function fetchMetadata() {
label: "Log out",
icon: "material-symbols:logout",
click: () => {
- clerk.signOut();
+ logout();
},
},
]);
const userSettingRes = await makeAuthenticatedRequest(
`/api/v1/settings/me/fetch`,
- await getToken.value()
+ token.value
);
userSettings.value = userSettingRes;
@@ -100,7 +98,7 @@ onMounted(() => {
async function updateSetting(key, value) {
const res = await makeAuthenticatedPostRequest(
`/api/v1/settings/me/update`,
- await getToken.value(),
+ token.value,
{
setting: key,
value: value,
@@ -116,7 +114,7 @@ async function updateSetting(key, value) {
async function updateBio() {
const res = await makeAuthenticatedPostRequest(
`/api/v1/me/bio`,
- await getToken.value(),
+ token.value,
{
bio: bio.value,
}
@@ -138,7 +136,11 @@ async function updateBio() {
-
+
+
Profile Settings
+
Manage your profile information
+
+
@@ -251,14 +253,3 @@ async function updateBio() {
-
-
diff --git a/plugins/auth.client.ts b/plugins/auth.client.ts
new file mode 100644
index 0000000..ec79508
--- /dev/null
+++ b/plugins/auth.client.ts
@@ -0,0 +1,9 @@
+export default defineNuxtPlugin(async () => {
+ const { initAuth, verifyAuth } = useAuth();
+
+ // Initialize auth from localStorage
+ initAuth();
+
+ // Verify token if present
+ await verifyAuth();
+});
diff --git a/schemas/dailyMetadata.ts b/schemas/dailyMetadata.ts
deleted file mode 100644
index 37fc7bb..0000000
--- a/schemas/dailyMetadata.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import mongoose from 'mongoose';
-
-const dailyMetadataSchema = new mongoose.Schema({
- date: {
- type: Date,
- required: true,
- unique: true,
- default: new Date()
- },
- skylander: String
-});
-
-const DailyMetadata = mongoose.model('DailyMetadata', dailyMetadataSchema);
-
-export default DailyMetadata;
\ No newline at end of file
diff --git a/schemas/notification.ts b/schemas/notification.ts
deleted file mode 100644
index 468c085..0000000
--- a/schemas/notification.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import mongoose from "mongoose";
-
-const notificationSchema = new mongoose.Schema({
- message: String,
- date: Date,
- read: Boolean,
-}, { versionKey: false });
-
-const Notification = mongoose.model("Notification", notificationSchema);
-
-export default Notification;
\ No newline at end of file
diff --git a/schemas/partialSkylander.ts b/schemas/partialSkylander.ts
deleted file mode 100644
index 164f549..0000000
--- a/schemas/partialSkylander.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import mongoose from "mongoose";
-
-const skylanderSchema = new mongoose.Schema({
- name: String,
- link: String,
- image: String,
- category: String,
- game: String,
- element: String,
- releasedWith: String,
- series: String,
- price: String,
- links: {
- ebay: String,
- amazon: String,
- scl: String,
- }
-}, { versionKey: false });
-
-const PartialSkylander = mongoose.model("PartialSkylander", skylanderSchema);
-
-export default PartialSkylander;
\ No newline at end of file
diff --git a/schemas/user.ts b/schemas/user.ts
deleted file mode 100644
index dbcf3f9..0000000
--- a/schemas/user.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import mongoose from "mongoose";
-
-const Notification = new mongoose.Schema({
- title: String,
- message: String,
- date: {
- type: Date,
- default: Date.now
- }
-}, { versionKey: false, id: true });
-
-const Settings = new mongoose.Schema({
- collectionVisibility: {
- type: Boolean,
- default: true
- },
- wishlistVisibility: {
- type: Boolean,
- default: true
- },
- watchingVisibility: {
- type: Boolean,
- default: true
- },
- language: {
- type: String,
- default: "english"
- }
-}, { versionKey: false, id: false });
-
-const userSchema = new mongoose.Schema({
- id: {
- type: String,
- required: true,
- unique: true
- },
- wishlist: [String],
- figures: [String],
- watching: [String],
- notifications: [Notification],
- settings: {
- type: Settings,
- default: {
- collectionVisibility: true,
- wishlistVisibility: true,
- watchingVisibility: true,
- language: "english",
- trackers: true
- }
- }
-}, { versionKey: false, id: false });
-
-const User = mongoose.model("User", userSchema);
-
-export default User;
\ No newline at end of file
diff --git a/scripts/scrape.ts b/scripts/scrape.ts
index 4a0ef61..e99791e 100644
--- a/scripts/scrape.ts
+++ b/scripts/scrape.ts
@@ -4,7 +4,7 @@
import fetch from "node-fetch";
import * as cheerio from "cheerio";
import { mkdirSync, readdirSync, writeFileSync } from "fs";
-import { savePartialSkylander, wipePartialSkylanders } from "../utils/database";
+import { savePartialSkylander, wipePartialSkylanders } from "../server/utils/database";
const links = [
"https://skylanderscharacterlist.com/spyros-adventure-figures/",
diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts
new file mode 100644
index 0000000..d298364
--- /dev/null
+++ b/server/api/auth/login.post.ts
@@ -0,0 +1,70 @@
+import { verifyPassword, generateToken } from '~/server/utils/auth';
+import { getUserByEmail, updateUserLastActive } from '~/server/utils/database';
+import { db } from '~/server/db';
+import { users } from '~/server/db/schema';
+import { eq } from 'drizzle-orm';
+
+export default defineEventHandler(async (event) => {
+ const body = await readBody(event);
+ const { email, password } = body;
+
+ // Validate input
+ if (!email || !password) {
+ throw createError({
+ status: 400,
+ message: 'Email and password are required',
+ });
+ }
+
+ // Get user by email
+ const user = await getUserByEmail(email);
+ if (!user) {
+ throw createError({
+ status: 401,
+ message: 'Invalid email or password',
+ });
+ }
+
+ // Get password hash from database
+ const userWithPassword = await db.select()
+ .from(users)
+ .where(eq(users.id, user.id))
+ .limit(1);
+
+ if (userWithPassword.length === 0) {
+ throw createError({
+ status: 401,
+ message: 'Invalid email or password',
+ });
+ }
+
+ // Verify password
+ const isValidPassword = await verifyPassword(password, userWithPassword[0].passwordHash);
+ if (!isValidPassword) {
+ throw createError({
+ status: 401,
+ message: 'Invalid email or password',
+ });
+ }
+
+ // Update last active
+ await updateUserLastActive(user.id);
+
+ // Generate token
+ const token = generateToken({
+ userId: user.id,
+ email: user.email,
+ username: user.username,
+ });
+
+ return {
+ user: {
+ id: user.id,
+ email: user.email,
+ username: user.username,
+ firstName: user.firstName,
+ imageUrl: user.imageUrl,
+ },
+ token,
+ };
+});
diff --git a/server/api/auth/me.get.ts b/server/api/auth/me.get.ts
new file mode 100644
index 0000000..6e1a895
--- /dev/null
+++ b/server/api/auth/me.get.ts
@@ -0,0 +1,44 @@
+import { verifyToken } from '~/server/utils/auth';
+import { fetchUser } from '~/server/utils/database';
+
+export default defineEventHandler(async (event) => {
+ const authHeader = getHeader(event, 'ST-Auth-Token');
+
+ if (!authHeader) {
+ throw createError({
+ status: 401,
+ message: 'Missing authentication token',
+ });
+ }
+
+ const payload = verifyToken(authHeader);
+ if (!payload) {
+ throw createError({
+ status: 401,
+ message: 'Invalid or expired token',
+ });
+ }
+
+ const user = await fetchUser(payload.userId);
+ if (!user) {
+ throw createError({
+ status: 404,
+ message: 'User not found',
+ });
+ }
+
+ return {
+ user: {
+ id: user.id,
+ email: user.email,
+ username: user.username,
+ firstName: user.firstName,
+ lastName: user.lastName,
+ imageUrl: user.imageUrl,
+ bio: user.bio,
+ banned: user.banned,
+ createdAt: user.createdAt,
+ lastActiveAt: user.lastActiveAt,
+ },
+ };
+});
diff --git a/server/api/auth/register.post.ts b/server/api/auth/register.post.ts
new file mode 100644
index 0000000..786e635
--- /dev/null
+++ b/server/api/auth/register.post.ts
@@ -0,0 +1,55 @@
+import { hashPassword, generateToken } from '~/server/utils/auth';
+import { createUser, getUserByEmail, getUserByUsername } from '~/server/utils/database';
+
+export default defineEventHandler(async (event) => {
+ const body = await readBody(event);
+ const { email, username, password, firstName } = body;
+
+ // Validate input
+ if (!email || !username || !password) {
+ throw createError({
+ status: 400,
+ message: 'Email, username, and password are required',
+ });
+ }
+
+ // Check if email already exists
+ const existingEmail = await getUserByEmail(email);
+ if (existingEmail) {
+ throw createError({
+ status: 409,
+ message: 'Email already in use',
+ });
+ }
+
+ // Check if username already exists
+ const existingUsername = await getUserByUsername(username);
+ if (existingUsername) {
+ throw createError({
+ status: 409,
+ message: 'Username already in use',
+ });
+ }
+
+ // Hash password and create user
+ const passwordHash = await hashPassword(password);
+ const user = await createUser(email, username, passwordHash, firstName);
+
+ // Generate token
+ const token = generateToken({
+ userId: user.id,
+ email: user.email,
+ username: user.username,
+ });
+
+ return {
+ user: {
+ id: user.id,
+ email: user.email,
+ username: user.username,
+ firstName: user.firstName,
+ imageUrl: user.imageUrl,
+ },
+ token,
+ };
+});
diff --git a/server/api/v1/collections/fetch.ts b/server/api/v1/collections/fetch.ts
index 1c1d332..c3e7b65 100644
--- a/server/api/v1/collections/fetch.ts
+++ b/server/api/v1/collections/fetch.ts
@@ -1,4 +1,4 @@
-import { fetchCollection } from "~/utils/database";
+import { fetchCollection } from "~/server/utils/database";
export default eventHandler(async (event) => {
const userid = event.context.user.id;
diff --git a/server/api/v1/collections/modify/[id].ts b/server/api/v1/collections/modify/[id].ts
index 18d2901..cf5f564 100644
--- a/server/api/v1/collections/modify/[id].ts
+++ b/server/api/v1/collections/modify/[id].ts
@@ -1,4 +1,4 @@
-import { toggleCollectionSkylander } from "~/utils/database";
+import { toggleCollectionSkylander } from "~/server/utils/database";
export default eventHandler(async (event) => {
const userid = event.context.user.id;
diff --git a/server/api/v1/collections/user/[id].ts b/server/api/v1/collections/user/[id].ts
index 9ed0ff1..a51d10d 100644
--- a/server/api/v1/collections/user/[id].ts
+++ b/server/api/v1/collections/user/[id].ts
@@ -1,4 +1,4 @@
-import { fetchCollection } from "~/utils/database";
+import { fetchCollection } from "~/server/utils/database";
export default eventHandler(async (event) => {
const userid = getRouterParam(event, "id");
diff --git a/server/api/v1/me/bio.ts b/server/api/v1/me/bio.ts
index dc298b9..aff2dfc 100644
--- a/server/api/v1/me/bio.ts
+++ b/server/api/v1/me/bio.ts
@@ -1,8 +1,10 @@
-import { clerkClient } from "~/utils/database";
+import { updateUserBio } from "~/server/utils/database";
export default eventHandler(async (event) => {
const body = await readBody(event);
const bio = await JSON.parse(body).bio;
- const result = await clerkClient.users.updateUserMetadata(event.context.user.id, { publicMetadata: { bio: bio } });
+
+ await updateUserBio(event.context.user.id, bio);
+
return { bio: bio };
});
\ No newline at end of file
diff --git a/server/api/v1/me/metadata.ts b/server/api/v1/me/metadata.ts
index 5938dc8..658a383 100644
--- a/server/api/v1/me/metadata.ts
+++ b/server/api/v1/me/metadata.ts
@@ -1,6 +1,8 @@
-import { clerkClient } from "~/utils/database"
-
export default defineEventHandler(async (event) => {
- const user = await clerkClient.users.getUser(event.context.user.id)
- return user.publicMetadata
+ const user = event.context.user;
+
+ // Return user metadata (bio and other public metadata)
+ return {
+ bio: user.bio || '',
+ };
});
\ No newline at end of file
diff --git a/server/api/v1/messages/fetch.ts b/server/api/v1/messages/fetch.ts
index c0dfeb7..63f1e57 100644
--- a/server/api/v1/messages/fetch.ts
+++ b/server/api/v1/messages/fetch.ts
@@ -1,4 +1,4 @@
-import { fetchMessages } from "~/utils/database";
+import { fetchMessages } from "~/server/utils/database";
export default eventHandler(async (event) => {
const userid = event.context.user.id;
diff --git a/server/api/v1/metadata/today.ts b/server/api/v1/metadata/today.ts
index 2319757..5ca2b98 100644
--- a/server/api/v1/metadata/today.ts
+++ b/server/api/v1/metadata/today.ts
@@ -1,4 +1,4 @@
-import { getDailyMetadata } from "~/utils/database";
+import { getDailyMetadata } from "~/server/utils/database";
export default defineCachedEventHandler(async (event) => {
const metadata = await getDailyMetadata(new Date());
diff --git a/server/api/v1/partials/[id].ts b/server/api/v1/partials/[id].ts
index a03071f..2b7fcaa 100644
--- a/server/api/v1/partials/[id].ts
+++ b/server/api/v1/partials/[id].ts
@@ -1,4 +1,4 @@
-import { fetchPartialSkylander } from "~/utils/database";
+import { fetchPartialSkylander } from "~/server/utils/database";
export default defineEventHandler(async (event) => {
const name = getRouterParam(event, "id");
diff --git a/server/api/v1/partials/all.ts b/server/api/v1/partials/all.ts
index ffcf764..c86497f 100644
--- a/server/api/v1/partials/all.ts
+++ b/server/api/v1/partials/all.ts
@@ -1,4 +1,4 @@
-import { fetchPartialSkylanders } from "~/utils/database"
+import { fetchPartialSkylanders } from "~/server/utils/database"
export default defineEventHandler(async (event) => {
const skylanders = await fetchPartialSkylanders();
diff --git a/server/api/v1/partials/category/[category].ts b/server/api/v1/partials/category/[category].ts
index 876f1f0..6409860 100644
--- a/server/api/v1/partials/category/[category].ts
+++ b/server/api/v1/partials/category/[category].ts
@@ -1,4 +1,4 @@
-import { fetchPartialSkylandersByCategory } from "~/utils/database";
+import { fetchPartialSkylandersByCategory } from "~/server/utils/database";
export default defineEventHandler(async (event) => {
let category = getRouterParam(event, "category");
diff --git a/server/api/v1/partials/game/[game].ts b/server/api/v1/partials/game/[game].ts
index 5db4826..98aff3d 100644
--- a/server/api/v1/partials/game/[game].ts
+++ b/server/api/v1/partials/game/[game].ts
@@ -1,4 +1,4 @@
-import { fetchPartialSkylandersByGame } from "~/utils/database";
+import { fetchPartialSkylandersByGame } from "~/server/utils/database";
export default defineEventHandler(async (event) => {
const game = getRouterParam(event, "game");
diff --git a/server/api/v1/partials/page/[page].ts b/server/api/v1/partials/page/[page].ts
index 880a4db..ce171b4 100644
--- a/server/api/v1/partials/page/[page].ts
+++ b/server/api/v1/partials/page/[page].ts
@@ -1,4 +1,4 @@
-import { getPageOfPartials } from "~/utils/database";
+import { getPageOfPartials } from "~/server/utils/database";
export default defineEventHandler(async (event) => {
const page = getRouterParam(event, 'page');
diff --git a/server/api/v1/searching/[term].ts b/server/api/v1/searching/[term].ts
index 372edcf..6b7a53b 100644
--- a/server/api/v1/searching/[term].ts
+++ b/server/api/v1/searching/[term].ts
@@ -1,4 +1,4 @@
-import { search } from "~/utils/database";
+import { search } from "~/server/utils/database";
import { filterString } from "~/utils/string";
export default defineEventHandler(async (event) => {
diff --git a/server/api/v1/settings/me/fetch.ts b/server/api/v1/settings/me/fetch.ts
index a28712f..4f86e53 100644
--- a/server/api/v1/settings/me/fetch.ts
+++ b/server/api/v1/settings/me/fetch.ts
@@ -1,4 +1,4 @@
-import { fetchSettings } from "~/utils/database";
+import { fetchSettings } from "~/server/utils/database";
export default defineEventHandler(async (event) => {
const id = event.context.user.id;
diff --git a/server/api/v1/settings/me/update.ts b/server/api/v1/settings/me/update.ts
index 99dd3bf..4f67950 100644
--- a/server/api/v1/settings/me/update.ts
+++ b/server/api/v1/settings/me/update.ts
@@ -1,4 +1,4 @@
-import { modifySetting } from "~/utils/database"
+import { modifySetting } from "~/server/utils/database"
import { turnStringNice } from "~/utils/string"
export default eventHandler(async (event) => {
diff --git a/server/api/v1/skylanders/[id].ts b/server/api/v1/skylanders/[id].ts
index 34daead..540ceb5 100644
--- a/server/api/v1/skylanders/[id].ts
+++ b/server/api/v1/skylanders/[id].ts
@@ -1,4 +1,4 @@
-import { fetchPartialSkylander } from "~/utils/database";
+import { fetchPartialSkylander } from "~/server/utils/database";
import { getSkylanderData } from "~/utils/scraper";
export default defineCachedEventHandler(async (event) => {
diff --git a/server/api/v1/users/[username]/fetch.ts b/server/api/v1/users/[username]/fetch.ts
index 752bed5..9bf6ba0 100644
--- a/server/api/v1/users/[username]/fetch.ts
+++ b/server/api/v1/users/[username]/fetch.ts
@@ -1,4 +1,4 @@
-import { clerkClient } from "~/utils/database";
+import { getUserByUsername } from "~/server/utils/database";
export default eventHandler(async (event) => {
const username = getRouterParam(event, "username");
@@ -10,20 +10,25 @@ export default eventHandler(async (event) => {
});
}
+ const user = await getUserByUsername(username);
- const result = await clerkClient.users.getUserList({
- username: [username],
- })
-
- if (result.data.length === 0) {
+ if (!user) {
throw createError({
status: 404,
message: "User not found",
});
}
- const user = result.data[0];
-
- return { username: user.username, id: user.id, banned: user.banned, firstName: user.firstName, publicMetadata: user.publicMetadata, lastOnline: user.lastActiveAt, createdAt: user.createdAt, image: user.imageUrl, hasImage: user.hasImage };
+ return {
+ username: user.username,
+ id: user.id,
+ banned: user.banned,
+ firstName: user.firstName,
+ publicMetadata: { bio: user.bio || '' },
+ lastOnline: user.lastActiveAt,
+ createdAt: user.createdAt,
+ image: user.imageUrl,
+ hasImage: !!user.imageUrl
+ };
});
\ No newline at end of file
diff --git a/server/api/v1/watching/fetch.ts b/server/api/v1/watching/fetch.ts
index 7820d45..50f95ab 100644
--- a/server/api/v1/watching/fetch.ts
+++ b/server/api/v1/watching/fetch.ts
@@ -1,4 +1,4 @@
-import { fetchWatchingSkylanders } from "~/utils/database";
+import { fetchWatchingSkylanders } from "~/server/utils/database";
export default eventHandler(async (event) => {
const userid = event.context.user.id;
diff --git a/server/api/v1/watching/modify/[id].ts b/server/api/v1/watching/modify/[id].ts
index 1358c01..f4eb285 100644
--- a/server/api/v1/watching/modify/[id].ts
+++ b/server/api/v1/watching/modify/[id].ts
@@ -1,4 +1,4 @@
-import { toggleWatchingSkylander } from "~/utils/database";
+import { toggleWatchingSkylander } from "~/server/utils/database";
export default eventHandler(async (event) => {
const userid = event.context.user.id;
diff --git a/server/api/v1/watching/user/[id].ts b/server/api/v1/watching/user/[id].ts
index 627f749..dda5e76 100644
--- a/server/api/v1/watching/user/[id].ts
+++ b/server/api/v1/watching/user/[id].ts
@@ -1,4 +1,4 @@
-import { fetchWatchingSkylanders } from "~/utils/database";
+import { fetchWatchingSkylanders } from "~/server/utils/database";
export default eventHandler(async (event) => {
const userid = getRouterParam(event, "id");
diff --git a/server/api/v1/wishlist/fetch.ts b/server/api/v1/wishlist/fetch.ts
index 477ec51..0f35edd 100644
--- a/server/api/v1/wishlist/fetch.ts
+++ b/server/api/v1/wishlist/fetch.ts
@@ -1,4 +1,4 @@
-import { fetchWishlist } from "~/utils/database";
+import { fetchWishlist } from "~/server/utils/database";
export default eventHandler(async (event) => {
const userid = event.context.user.id;
diff --git a/server/api/v1/wishlist/modify/[id].ts b/server/api/v1/wishlist/modify/[id].ts
index b14cdd6..f85888f 100644
--- a/server/api/v1/wishlist/modify/[id].ts
+++ b/server/api/v1/wishlist/modify/[id].ts
@@ -1,4 +1,4 @@
-import { toggleWatchingSkylander, toggleWishlist } from "~/utils/database";
+import { toggleWishlist } from "~/server/utils/database";
export default eventHandler(async (event) => {
const userid = event.context.user.id;
diff --git a/server/api/v1/wishlist/user/[id].ts b/server/api/v1/wishlist/user/[id].ts
index c8ec47b..f50d8b8 100644
--- a/server/api/v1/wishlist/user/[id].ts
+++ b/server/api/v1/wishlist/user/[id].ts
@@ -1,4 +1,4 @@
-import { fetchWishlist } from "~/utils/database";
+import { fetchWishlist } from "~/server/utils/database";
export default eventHandler(async (event) => {
const userid = getRouterParam(event, "id");
diff --git a/server/db/index.ts b/server/db/index.ts
new file mode 100644
index 0000000..2b4edbd
--- /dev/null
+++ b/server/db/index.ts
@@ -0,0 +1,121 @@
+import Database from 'better-sqlite3';
+import { drizzle } from 'drizzle-orm/better-sqlite3';
+import * as schema from './schema';
+import path from 'path';
+import fs from 'fs';
+
+// Ensure the data directory exists
+const dataDir = path.join(process.cwd(), 'data');
+if (!fs.existsSync(dataDir)) {
+ fs.mkdirSync(dataDir, { recursive: true });
+}
+
+const dbPath = path.join(dataDir, 'skytracker.db');
+
+// Create SQLite database connection
+const sqlite = new Database(dbPath);
+sqlite.pragma('journal_mode = WAL');
+
+// Create Drizzle instance
+export const db = drizzle(sqlite, { schema });
+
+// Initialize database tables
+export function initializeDatabase() {
+ // Create tables if they don't exist
+ sqlite.exec(`
+ CREATE TABLE IF NOT EXISTS users (
+ id TEXT PRIMARY KEY,
+ email TEXT NOT NULL UNIQUE,
+ username TEXT NOT NULL UNIQUE,
+ password_hash TEXT NOT NULL,
+ first_name TEXT,
+ last_name TEXT,
+ image_url TEXT,
+ bio TEXT,
+ banned INTEGER DEFAULT 0,
+ created_at INTEGER NOT NULL,
+ last_active_at INTEGER
+ );
+
+ CREATE TABLE IF NOT EXISTS user_settings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ collection_visibility INTEGER DEFAULT 1,
+ wishlist_visibility INTEGER DEFAULT 1,
+ watching_visibility INTEGER DEFAULT 1,
+ language TEXT DEFAULT 'english'
+ );
+
+ CREATE TABLE IF NOT EXISTS notifications (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ title TEXT,
+ message TEXT,
+ date INTEGER,
+ read INTEGER DEFAULT 0
+ );
+
+ CREATE TABLE IF NOT EXISTS partial_skylanders (
+ id TEXT PRIMARY KEY,
+ name TEXT,
+ link TEXT,
+ image TEXT,
+ category TEXT,
+ game TEXT,
+ element TEXT,
+ released_with TEXT,
+ series TEXT,
+ price TEXT,
+ ebay_link TEXT,
+ amazon_link TEXT,
+ scl_link TEXT
+ );
+
+ CREATE TABLE IF NOT EXISTS daily_metadata (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ date INTEGER NOT NULL UNIQUE,
+ skylander_id TEXT REFERENCES partial_skylanders(id)
+ );
+
+ CREATE TABLE IF NOT EXISTS user_wishlist (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ skylander_id TEXT NOT NULL REFERENCES partial_skylanders(id) ON DELETE CASCADE
+ );
+
+ CREATE TABLE IF NOT EXISTS user_figures (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ skylander_id TEXT NOT NULL REFERENCES partial_skylanders(id) ON DELETE CASCADE
+ );
+
+ CREATE TABLE IF NOT EXISTS user_watching (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ skylander_id TEXT NOT NULL REFERENCES partial_skylanders(id) ON DELETE CASCADE
+ );
+
+ CREATE TABLE IF NOT EXISTS auth_sessions (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ token TEXT NOT NULL UNIQUE,
+ expires_at INTEGER NOT NULL,
+ created_at INTEGER
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
+ CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
+ CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id);
+ CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id);
+ CREATE INDEX IF NOT EXISTS idx_user_wishlist_user_id ON user_wishlist(user_id);
+ CREATE INDEX IF NOT EXISTS idx_user_figures_user_id ON user_figures(user_id);
+ CREATE INDEX IF NOT EXISTS idx_user_watching_user_id ON user_watching(user_id);
+ CREATE INDEX IF NOT EXISTS idx_partial_skylanders_name ON partial_skylanders(name);
+ CREATE INDEX IF NOT EXISTS idx_auth_sessions_token ON auth_sessions(token);
+ `);
+}
+
+// Initialize on import
+initializeDatabase();
+
+export default db;
diff --git a/server/db/schema.ts b/server/db/schema.ts
new file mode 100644
index 0000000..69cce58
--- /dev/null
+++ b/server/db/schema.ts
@@ -0,0 +1,90 @@
+import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core';
+
+// Users table with authentication
+export const users = sqliteTable('users', {
+ id: text('id').primaryKey(),
+ email: text('email').notNull().unique(),
+ username: text('username').notNull().unique(),
+ passwordHash: text('password_hash').notNull(),
+ firstName: text('first_name'),
+ lastName: text('last_name'),
+ imageUrl: text('image_url'),
+ bio: text('bio'),
+ banned: integer('banned', { mode: 'boolean' }).default(false),
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
+ lastActiveAt: integer('last_active_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
+});
+
+// User settings (separate table for cleaner structure)
+export const userSettings = sqliteTable('user_settings', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
+ collectionVisibility: integer('collection_visibility', { mode: 'boolean' }).default(true),
+ wishlistVisibility: integer('wishlist_visibility', { mode: 'boolean' }).default(true),
+ watchingVisibility: integer('watching_visibility', { mode: 'boolean' }).default(true),
+ language: text('language').default('english'),
+});
+
+// User notifications
+export const notifications = sqliteTable('notifications', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
+ title: text('title'),
+ message: text('message'),
+ date: integer('date', { mode: 'timestamp' }).$defaultFn(() => new Date()),
+ read: integer('read', { mode: 'boolean' }).default(false),
+});
+
+// Skylanders (partial data)
+export const partialSkylanders = sqliteTable('partial_skylanders', {
+ id: text('id').primaryKey(),
+ name: text('name'),
+ link: text('link'),
+ image: text('image'),
+ category: text('category'),
+ game: text('game'),
+ element: text('element'),
+ releasedWith: text('released_with'),
+ series: text('series'),
+ price: text('price'),
+ ebayLink: text('ebay_link'),
+ amazonLink: text('amazon_link'),
+ sclLink: text('scl_link'),
+});
+
+// Daily metadata
+export const dailyMetadata = sqliteTable('daily_metadata', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ date: integer('date', { mode: 'timestamp' }).notNull().unique(),
+ skylanderId: text('skylander_id').references(() => partialSkylanders.id),
+});
+
+// User wishlist (many-to-many)
+export const userWishlist = sqliteTable('user_wishlist', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
+ skylanderId: text('skylander_id').notNull().references(() => partialSkylanders.id, { onDelete: 'cascade' }),
+});
+
+// User collection/figures (many-to-many)
+export const userFigures = sqliteTable('user_figures', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
+ skylanderId: text('skylander_id').notNull().references(() => partialSkylanders.id, { onDelete: 'cascade' }),
+});
+
+// User watching (many-to-many)
+export const userWatching = sqliteTable('user_watching', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
+ skylanderId: text('skylander_id').notNull().references(() => partialSkylanders.id, { onDelete: 'cascade' }),
+});
+
+// Auth sessions for JWT tokens (optional, for token revocation)
+export const authSessions = sqliteTable('auth_sessions', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
+ token: text('token').notNull().unique(),
+ expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
+ createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
+});
diff --git a/server/middleware/auth.ts b/server/middleware/auth.ts
index 4cca4b6..9b316e7 100644
--- a/server/middleware/auth.ts
+++ b/server/middleware/auth.ts
@@ -1,5 +1,5 @@
-import { verifyToken } from "@clerk/backend";
-import { clerkClient } from "~/utils/database";
+import { verifyToken } from "~/server/utils/auth";
+import { fetchUser } from "~/server/utils/database";
const authenticatedRoutes = [
"/api/v1/users/me",
@@ -42,25 +42,17 @@ export default defineEventHandler(async (event) => {
})
}
- const tokenInfo = await verifyToken(authHeader, {
- secretKey: process.env.NUXT_CLERK_SECRET_KEY,
- }).catch((err) => {
+ const tokenPayload = verifyToken(authHeader)
+ if (!tokenPayload) {
throw createError({
status: 401,
- message: err
- })
- })
-
- event.context.token = tokenInfo
-
- if (!tokenInfo?.sub) {
- throw createError({
- status: 401,
- message: "Invalid token"
+ message: "Invalid or expired token"
})
}
- const user = await clerkClient.users.getUser(tokenInfo.sub)
+ event.context.token = tokenPayload
+
+ const user = await fetchUser(tokenPayload.userId)
if (!user) {
throw createError({
@@ -69,6 +61,5 @@ export default defineEventHandler(async (event) => {
})
}
-
event.context.user = user
});
\ No newline at end of file
diff --git a/server/utils/auth.ts b/server/utils/auth.ts
new file mode 100644
index 0000000..87e722c
--- /dev/null
+++ b/server/utils/auth.ts
@@ -0,0 +1,53 @@
+import bcrypt from 'bcryptjs';
+import jwt from 'jsonwebtoken';
+import { randomBytes } from 'crypto';
+
+const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-this-in-production';
+const TOKEN_EXPIRY = '7d'; // 7 days
+
+export interface JWTPayload {
+ userId: string;
+ email: string;
+ username: string;
+}
+
+/**
+ * Hash a password using bcrypt
+ */
+export async function hashPassword(password: string): Promise {
+ const salt = await bcrypt.genSalt(10);
+ return bcrypt.hash(password, salt);
+}
+
+/**
+ * Verify a password against a hash
+ */
+export async function verifyPassword(password: string, hash: string): Promise {
+ return bcrypt.compare(password, hash);
+}
+
+/**
+ * Generate a JWT token for a user
+ */
+export function generateToken(payload: JWTPayload): string {
+ return jwt.sign(payload, JWT_SECRET, { expiresIn: TOKEN_EXPIRY });
+}
+
+/**
+ * Verify and decode a JWT token
+ */
+export function verifyToken(token: string): JWTPayload | null {
+ try {
+ const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
+ return decoded;
+ } catch (error) {
+ return null;
+ }
+}
+
+/**
+ * Generate a unique user ID
+ */
+export function generateUserId(): string {
+ return 'user_' + randomBytes(16).toString('hex');
+}
diff --git a/server/utils/database.ts b/server/utils/database.ts
new file mode 100644
index 0000000..09f798d
--- /dev/null
+++ b/server/utils/database.ts
@@ -0,0 +1,455 @@
+import { eq, and, like, or, sql, inArray } from 'drizzle-orm';
+import { db } from '../db';
+import * as schema from '../db/schema';
+import { generateUserId } from './auth';
+
+export interface PartialSkylander {
+ id: string;
+ name: string | null;
+ link: string | null;
+ image: string | null;
+ category: string | null;
+ game: string | null;
+ element: string | null;
+ releasedWith: string | null;
+ series: string | null;
+ price: string | null;
+ links?: {
+ ebay: string | null;
+ amazon: string | null;
+ scl: string | null;
+ };
+}
+
+export interface User {
+ id: string;
+ email: string;
+ username: string;
+ firstName: string | null;
+ lastName: string | null;
+ imageUrl: string | null;
+ bio: string | null;
+ banned: boolean | null;
+ createdAt: Date;
+ lastActiveAt: Date | null;
+}
+
+export interface Settings {
+ collectionVisibility: boolean | null;
+ wishlistVisibility: boolean | null;
+ watchingVisibility: boolean | null;
+ language: string | null;
+}
+
+export interface DailyMetadata {
+ id: number;
+ date: Date;
+ skylanderId: string | null;
+}
+
+// Skylander operations
+export async function fetchPartialSkylanders(): Promise {
+ const skylanders = await db.select().from(schema.partialSkylanders);
+ return skylanders.map(mapSkylanderWithLinks);
+}
+
+export async function savePartialSkylander(data: PartialSkylander): Promise {
+ let category = data.category?.toLowerCase() || '';
+ category = category.replaceAll(' ', '-');
+
+ await db.insert(schema.partialSkylanders).values({
+ id: data.id,
+ name: data.name,
+ link: data.link,
+ image: data.image,
+ category: category,
+ game: data.game,
+ element: data.element,
+ releasedWith: data.releasedWith,
+ series: data.series,
+ price: data.price,
+ ebayLink: data.links?.ebay || null,
+ amazonLink: data.links?.amazon || null,
+ sclLink: data.links?.scl || null,
+ });
+}
+
+export async function wipePartialSkylanders(): Promise {
+ await db.delete(schema.partialSkylanders);
+}
+
+export async function fetchPartialSkylander(id: string): Promise {
+ const result = await db.select()
+ .from(schema.partialSkylanders)
+ .where(eq(schema.partialSkylanders.id, id))
+ .limit(1);
+
+ return result.length > 0 ? mapSkylanderWithLinks(result[0]) : null;
+}
+
+export async function fetchPartialSkylandersByCategory(category: string): Promise {
+ const skylanders = await db.select()
+ .from(schema.partialSkylanders)
+ .where(eq(schema.partialSkylanders.category, category));
+
+ return skylanders.map(mapSkylanderWithLinks);
+}
+
+export async function fetchPartialSkylandersByGame(game: string): Promise {
+ const skylanders = await db.select()
+ .from(schema.partialSkylanders)
+ .where(eq(schema.partialSkylanders.game, game));
+
+ return skylanders.map(mapSkylanderWithLinks);
+}
+
+export async function getPageOfPartials(page: number, filter?: any): Promise {
+ let query = db.select().from(schema.partialSkylanders);
+
+ if (filter) {
+ if (filter.game) {
+ query = query.where(eq(schema.partialSkylanders.game, filter.game)) as any;
+ } else if (filter.category) {
+ query = query.where(eq(schema.partialSkylanders.category, filter.category)) as any;
+ } else if (filter.element) {
+ query = query.where(eq(schema.partialSkylanders.element, filter.element)) as any;
+ }
+ const skylanders = await query;
+ return skylanders.map(mapSkylanderWithLinks);
+ }
+
+ const skylanders = await query.limit(25).offset((page - 1) * 25);
+ return skylanders.map(mapSkylanderWithLinks);
+}
+
+export async function search(term: string, related: boolean = false): Promise {
+ const searchTerm = term.replaceAll('%20', ' ');
+ const words = searchTerm.split(' ');
+
+ if (words.length > 1 && related) {
+ const resultsAll: PartialSkylander[] = [];
+ for (const word of words) {
+ const results = await search(word, true);
+ resultsAll.push(...results);
+ }
+ return resultsAll;
+ }
+
+ const skylanders = await db.select()
+ .from(schema.partialSkylanders)
+ .where(like(schema.partialSkylanders.name, `%${searchTerm}%`));
+
+ return skylanders.map(mapSkylanderWithLinks);
+}
+
+// Daily metadata operations
+export async function getDailyMetadata(date: Date): Promise {
+ const dateNoTime = new Date(date);
+ dateNoTime.setHours(0, 0, 0, 0);
+
+ const result = await db.select()
+ .from(schema.dailyMetadata)
+ .where(eq(schema.dailyMetadata.date, dateNoTime))
+ .limit(1);
+
+ if (result.length === 0) {
+ const skylander = await getRandomSkylander();
+ if (!skylander) {
+ throw new Error('No skylanders found');
+ }
+
+ const inserted = await db.insert(schema.dailyMetadata)
+ .values({
+ date: dateNoTime,
+ skylanderId: skylander.id,
+ })
+ .returning();
+
+ return inserted[0];
+ }
+
+ return result[0];
+}
+
+async function getRandomSkylander(): Promise {
+ const skylanders = await fetchPartialSkylanders();
+ if (skylanders.length === 0) return null;
+ const randomIndex = Math.floor(Math.random() * skylanders.length);
+ return skylanders[randomIndex];
+}
+
+// User operations
+export async function fetchUser(id: string): Promise {
+ const result = await db.select()
+ .from(schema.users)
+ .where(eq(schema.users.id, id))
+ .limit(1);
+
+ if (result.length === 0) {
+ return null;
+ }
+
+ return result[0];
+}
+
+export async function createUser(email: string, username: string, passwordHash: string, firstName?: string): Promise {
+ const userId = generateUserId();
+ const now = new Date();
+
+ const user = await db.insert(schema.users)
+ .values({
+ id: userId,
+ email,
+ username,
+ passwordHash,
+ firstName: firstName || null,
+ createdAt: now,
+ lastActiveAt: now,
+ })
+ .returning();
+
+ // Create default settings for user
+ await db.insert(schema.userSettings)
+ .values({
+ userId,
+ collectionVisibility: true,
+ wishlistVisibility: true,
+ watchingVisibility: true,
+ language: 'english',
+ });
+
+ return user[0];
+}
+
+export async function getUserByEmail(email: string): Promise {
+ const result = await db.select()
+ .from(schema.users)
+ .where(eq(schema.users.email, email))
+ .limit(1);
+
+ return result.length > 0 ? result[0] : null;
+}
+
+export async function getUserByUsername(username: string): Promise {
+ const result = await db.select()
+ .from(schema.users)
+ .where(eq(schema.users.username, username))
+ .limit(1);
+
+ return result.length > 0 ? result[0] : null;
+}
+
+export async function updateUserBio(userId: string, bio: string): Promise {
+ await db.update(schema.users)
+ .set({ bio })
+ .where(eq(schema.users.id, userId));
+}
+
+export async function updateUserLastActive(userId: string): Promise {
+ await db.update(schema.users)
+ .set({ lastActiveAt: new Date() })
+ .where(eq(schema.users.id, userId));
+}
+
+// Watching operations
+export async function fetchWatchingSkylanders(userId: string, mine: boolean): Promise {
+ const user = await fetchUser(userId);
+ if (!user) return [];
+
+ if (!mine) {
+ const settings = await fetchSettings(userId);
+ if (!settings.watchingVisibility) return [];
+ }
+
+ const watching = await db.select({
+ skylander: schema.partialSkylanders,
+ })
+ .from(schema.userWatching)
+ .innerJoin(schema.partialSkylanders, eq(schema.userWatching.skylanderId, schema.partialSkylanders.id))
+ .where(eq(schema.userWatching.userId, userId));
+
+ return watching.map(w => mapSkylanderWithLinks(w.skylander));
+}
+
+export async function toggleWatchingSkylander(userId: string, skylanderId: string): Promise {
+ const existing = await db.select()
+ .from(schema.userWatching)
+ .where(and(
+ eq(schema.userWatching.userId, userId),
+ eq(schema.userWatching.skylanderId, skylanderId)
+ ))
+ .limit(1);
+
+ if (existing.length === 0) {
+ await db.insert(schema.userWatching).values({ userId, skylanderId });
+ return true;
+ } else {
+ await db.delete(schema.userWatching)
+ .where(and(
+ eq(schema.userWatching.userId, userId),
+ eq(schema.userWatching.skylanderId, skylanderId)
+ ));
+ return false;
+ }
+}
+
+// Collection operations
+export async function fetchCollection(userId: string, mine: boolean): Promise {
+ const user = await fetchUser(userId);
+ if (!user) return [];
+
+ if (!mine) {
+ const settings = await fetchSettings(userId);
+ if (!settings.collectionVisibility) return [];
+ }
+
+ const figures = await db.select({
+ skylander: schema.partialSkylanders,
+ })
+ .from(schema.userFigures)
+ .innerJoin(schema.partialSkylanders, eq(schema.userFigures.skylanderId, schema.partialSkylanders.id))
+ .where(eq(schema.userFigures.userId, userId));
+
+ return figures.map(f => mapSkylanderWithLinks(f.skylander));
+}
+
+export async function toggleCollectionSkylander(userId: string, skylanderId: string): Promise {
+ const existing = await db.select()
+ .from(schema.userFigures)
+ .where(and(
+ eq(schema.userFigures.userId, userId),
+ eq(schema.userFigures.skylanderId, skylanderId)
+ ))
+ .limit(1);
+
+ if (existing.length === 0) {
+ await db.insert(schema.userFigures).values({ userId, skylanderId });
+ return true;
+ } else {
+ await db.delete(schema.userFigures)
+ .where(and(
+ eq(schema.userFigures.userId, userId),
+ eq(schema.userFigures.skylanderId, skylanderId)
+ ));
+ return false;
+ }
+}
+
+// Wishlist operations
+export async function fetchWishlist(userId: string, mine: boolean): Promise {
+ const user = await fetchUser(userId);
+ if (!user) return [];
+
+ if (!mine) {
+ const settings = await fetchSettings(userId);
+ if (!settings.wishlistVisibility) return [];
+ }
+
+ const wishlist = await db.select({
+ skylander: schema.partialSkylanders,
+ })
+ .from(schema.userWishlist)
+ .innerJoin(schema.partialSkylanders, eq(schema.userWishlist.skylanderId, schema.partialSkylanders.id))
+ .where(eq(schema.userWishlist.userId, userId));
+
+ return wishlist.map(w => mapSkylanderWithLinks(w.skylander));
+}
+
+export async function toggleWishlist(userId: string, skylanderId: string): Promise {
+ const existing = await db.select()
+ .from(schema.userWishlist)
+ .where(and(
+ eq(schema.userWishlist.userId, userId),
+ eq(schema.userWishlist.skylanderId, skylanderId)
+ ))
+ .limit(1);
+
+ if (existing.length === 0) {
+ await db.insert(schema.userWishlist).values({ userId, skylanderId });
+ return true;
+ } else {
+ await db.delete(schema.userWishlist)
+ .where(and(
+ eq(schema.userWishlist.userId, userId),
+ eq(schema.userWishlist.skylanderId, skylanderId)
+ ));
+ return false;
+ }
+}
+
+// Notification/messages operations
+export async function fetchMessages(userId: string) {
+ const messages = await db.select()
+ .from(schema.notifications)
+ .where(eq(schema.notifications.userId, userId));
+
+ return messages;
+}
+
+// Settings operations
+export async function fetchSettings(userId: string): Promise {
+ const result = await db.select()
+ .from(schema.userSettings)
+ .where(eq(schema.userSettings.userId, userId))
+ .limit(1);
+
+ if (result.length === 0) {
+ // Create default settings if they don't exist
+ await db.insert(schema.userSettings).values({
+ userId,
+ collectionVisibility: true,
+ wishlistVisibility: true,
+ watchingVisibility: true,
+ language: 'english',
+ });
+
+ return {
+ collectionVisibility: true,
+ wishlistVisibility: true,
+ watchingVisibility: true,
+ language: 'english',
+ };
+ }
+
+ return result[0];
+}
+
+export async function modifySetting(userId: string, setting: string, value: any): Promise {
+ const settings = await db.select()
+ .from(schema.userSettings)
+ .where(eq(schema.userSettings.userId, userId))
+ .limit(1);
+
+ if (settings.length === 0) {
+ // Create settings if they don't exist
+ await db.insert(schema.userSettings).values({
+ userId,
+ [setting]: value,
+ });
+ } else {
+ await db.update(schema.userSettings)
+ .set({ [setting]: value })
+ .where(eq(schema.userSettings.userId, userId));
+ }
+}
+
+// Helper function to map skylander with links
+function mapSkylanderWithLinks(skylander: any): PartialSkylander {
+ return {
+ id: skylander.id,
+ name: skylander.name,
+ link: skylander.link,
+ image: skylander.image,
+ category: skylander.category,
+ game: skylander.game,
+ element: skylander.element,
+ releasedWith: skylander.releasedWith || skylander.released_with,
+ series: skylander.series,
+ price: skylander.price,
+ links: {
+ ebay: skylander.ebayLink || skylander.ebay_link,
+ amazon: skylander.amazonLink || skylander.amazon_link,
+ scl: skylander.sclLink || skylander.scl_link,
+ },
+ };
+}
diff --git a/utils/database.ts b/utils/database.ts
deleted file mode 100644
index d7b4fd6..0000000
--- a/utils/database.ts
+++ /dev/null
@@ -1,296 +0,0 @@
-import mongoose from "mongoose";
-import DailyMetadata from "~/schemas/dailyMetadata";
-import PartialSkylander from "~/schemas/partialSkylander";
-import { config } from "dotenv";
-import User from "~/schemas/user";
-import { createClerkClient } from "@clerk/backend";
-
-export const clerkClient = createClerkClient({ secretKey: process.env.NUXT_CLERK_SECRET_KEY });
-
-config();
-
-mongoose.connect(process.env.MONGODB_URI || "mongodb://localhost:27017/skylanders");
-
-export async function fetchPartialSkylanders(): Promise {
- const skylanders: PartialSkylander[] = await PartialSkylander.find();
- return skylanders;
-}
-
-export async function savePartialSkylander(
- data: PartialSkylander
-): Promise {
- data.category = data.category.toLowerCase();
- data.category = data.category.replaceAll(" ", "-");
- const skylander = new PartialSkylander(data);
- await skylander.save();
-}
-
-export async function wipePartialSkylanders(): Promise {
- await PartialSkylander.deleteMany({});
-}
-
-export async function fetchPartialSkylander(
- id: string
-): Promise {
- const skylander: PartialSkylander | null = await PartialSkylander.findOne({
- _id: id,
- });
-
- return skylander;
-}
-
-export async function fetchPartialSkylandersByCategory(
- category: Category
-): Promise {
- const skylanders: PartialSkylander[] = await PartialSkylander.find({
- category,
- });
- return skylanders;
-}
-
-export async function fetchPartialSkylandersByGame(
- game: Game
-): Promise {
- const skylanders: PartialSkylander[] = await PartialSkylander.find({ game });
- return skylanders;
-}
-
-async function getRandomSkylander(): Promise {
- const skylanders: PartialSkylander[] = await fetchPartialSkylanders();
- const randomIndex = Math.floor(Math.random() * skylanders.length);
- return skylanders[randomIndex];
-}
-
-export async function getDailyMetadata(date: Date): Promise {
- const dateNoTime = new Date(date);
- dateNoTime.setHours(0, 0, 0, 0);
-
- const metadata: DailyMetadata | null = await DailyMetadata.findOne({
- date: dateNoTime,
- });
-
-
- if (metadata === null) {
- const skylander = await getRandomSkylander();
- if (!skylander) {
- throw new Error("No skylanders found");
- }
-
- const newMetadata = new DailyMetadata({ date: dateNoTime, skylander: skylander._id });
- await newMetadata.save();
-
- // @ts-ignore
- return newMetadata as DailyMetadata;
- }
-
- return metadata;
-}
-
-// this function SUCKS!!
-export async function getPageOfPartials(page: number, filter?: Object): Promise {
- if (filter) {
- // @ts-ignore
- if (filter["game"]) {
- // @ts-ignore
- const skylanders: PartialSkylander[] = await PartialSkylander.find({ game: filter["game"] })
- return skylanders;
- // @ts-ignore
- } else if (filter["category"]) {
- // @ts-ignore
- const skylanders: PartialSkylander[] = await PartialSkylander.find({ category: filter["category"] })
- return skylanders;
- // @ts-ignore
- } else if (filter["element"]) {
- // @ts-ignore
- const skylanders: PartialSkylander[] = await PartialSkylander.find({ element: filter["element"] })
- return skylanders;
- }
- }
-
- // @ts-ignore
- const skylanders: PartialSkylander[] = await PartialSkylander.find().skip((page - 1) * 25).limit(25);
- return skylanders;
-}
-
-export async function search(term: String, related: Boolean): Promise {
- const words = term.replaceAll("%20", " ").split(" ");
- if (words.length > 1 && related) {
- const resultsAll: PartialSkylander[] = [];
- for (const word of words) {
- const results = await search(word, true);
- resultsAll.push(...results);
- }
- return resultsAll;
- }
-
- const skylanders: PartialSkylander[] = await PartialSkylander.find({
- name: { $regex: term, $options: "i" },
- });
- return skylanders;
-}
-
-export async function fetchUser(id: string): Promise {
- const user: User | null = await User.findOne({ id });
- if (!user) {
- console.log(id)
- const clerkUser = await clerkClient.users.getUser(id)
- if (!clerkUser) {
- return null;
- };
- const newUser = new User({ id, wishlist: [], figures: [], watching: [], notifications: [] });
- await newUser.save();
-
- // @ts-ignore
- return newUser as User;
- }
-
- return user;
-}
-
-export async function fetchWatchingSkylanders(userId: string, mine: boolean): Promise {
- const user = await fetchUser(userId);
- if (!user) {
- return [];
- }
-
- if (!mine && user.settings.watchingVisibility === false) {
- return [];
- }
-
-
- const skylanders: PartialSkylander[] = await PartialSkylander.find({
- _id: { $in: user.watching },
- });
- return skylanders;
-}
-
-export async function toggleWatchingSkylander(userId: string, skylanderId: string): Promise {
- const user = await fetchUser(userId);
- if (!user) {
- throw new Error("User not found");
- }
-
- let result
-
- const index = user.watching.indexOf(skylanderId);
- if (index === -1) {
- user.watching.push(skylanderId);
- result = true;
-
- } else {
- user.watching.splice(index, 1);
- result = false;
- }
-
- // @ts-ignore
- await user.save();
- return result;
-}
-
-export async function toggleCollectionSkylander(userId: string, skylanderId: string): Promise {
- const user = await fetchUser(userId);
- if (!user) {
- throw new Error("User not found");
- }
-
- let result
-
- const index = user.figures.indexOf(skylanderId);
- if (index === -1) {
- user.figures.push(skylanderId);
- result = true;
-
- } else {
- user.figures.splice(index, 1);
- result = false;
- }
-
- // @ts-ignore
- await user.save();
- return result;
-}
-
-export async function fetchMessages(userId: string) {
- const user = await fetchUser(userId);
- if (!user) {
- return [];
- }
-
- return user.notifications;
-}
-
-export async function fetchCollection(userId: string, mine: boolean): Promise {
- const user = await fetchUser(userId);
- if (!user) {
- return [];
- }
-
- if (!mine && user.settings.collectionVisibility === false) {
- return [];
- }
-
- const skylanders: PartialSkylander[] = await PartialSkylander.find({
- _id: { $in: user.figures },
- });
- return skylanders;
-}
-
-export async function toggleWishlist(userId: string, skylanderId: string): Promise {
- const user = await fetchUser(userId);
- if (!user) {
- throw new Error("User not found");
- }
-
- let result
-
- const index = user.wishlist.indexOf(skylanderId);
- if (index === -1) {
- user.wishlist.push(skylanderId);
- result = true;
-
- } else {
- user.wishlist.splice(index, 1);
- result = false;
- }
-
- // @ts-ignore
- await user.save();
- return result;
-}
-
-export async function fetchWishlist(userId: string, mine: boolean): Promise {
- const user = await fetchUser(userId);
- if (!user) {
- return [];
- }
-
- if (!mine && user.settings.wishlistVisibility === false) {
- return [];
- }
-
- const skylanders: PartialSkylander[] = await PartialSkylander.find({
- _id: { $in: user.wishlist },
- });
- return skylanders;
-}
-
-export async function fetchSettings(userId: string): Promise {
- const user = await fetchUser(userId);
- if (!user) {
- throw new Error("User not found");
- }
-
- return user.settings;
-}
-
-export async function modifySetting(userId: string, setting: string, value: any): Promise {
- const user = await fetchUser(userId);
- if (!user) {
- throw new Error("User not found");
- }
-
- // @ts-ignore
- user.settings[setting] = value;
- // @ts-ignore
- await user.save();
-}
\ No newline at end of file
From 8846ea62cafa3fb54ccd02035aba212a5092f6d5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Oct 2025 16:16:47 +0000
Subject: [PATCH 3/4] Add documentation for new auth system and setup
instructions
Co-authored-by: brandonapt <63559800+brandonapt@users.noreply.github.com>
---
.env.example | 5 ++
AUTH_SETUP.md | 99 ++++++++++++++++++++++++++++++
README.md | 26 ++++++++
scripts/migrate-mongo-to-sqlite.js | 34 ++++++++++
4 files changed, 164 insertions(+)
create mode 100644 .env.example
create mode 100644 AUTH_SETUP.md
create mode 100644 scripts/migrate-mongo-to-sqlite.js
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..2bc24f5
--- /dev/null
+++ b/.env.example
@@ -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
diff --git a/AUTH_SETUP.md b/AUTH_SETUP.md
new file mode 100644
index 0000000..a9fdbcf
--- /dev/null
+++ b/AUTH_SETUP.md
@@ -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: `
+- Returns: current user object
+
+## Frontend Usage
+
+The `useAuth` composable provides authentication state and methods:
+
+```vue
+
+```
+
+## 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.
diff --git a/README.md b/README.md
index 739a047..df542db 100644
--- a/README.md
+++ b/README.md
@@ -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):
@@ -28,3 +43,14 @@ Collection/wishlist/watchlist (they have the same general layout):
Profile:

+
+## 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.
+
diff --git a/scripts/migrate-mongo-to-sqlite.js b/scripts/migrate-mongo-to-sqlite.js
new file mode 100644
index 0000000..608fe44
--- /dev/null
+++ b/scripts/migrate-mongo-to-sqlite.js
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+
+/**
+ * MongoDB to SQLite Migration Script
+ *
+ * This script helps migrate data from MongoDB to SQLite.
+ *
+ * Usage:
+ * 1. Ensure MongoDB is still accessible
+ * 2. Update MONGODB_URI in this file
+ * 3. Run: node scripts/migrate-mongo-to-sqlite.js
+ */
+
+console.log('MongoDB to SQLite Migration Script');
+console.log('===================================\n');
+
+console.log('⚠️ IMPORTANT NOTES:');
+console.log('1. This script requires the old MongoDB dependencies to be installed');
+console.log('2. Make sure your MongoDB instance is accessible');
+console.log('3. Backup your data before running this migration');
+console.log('4. User passwords cannot be migrated (Clerk handled auth before)');
+console.log('5. Users will need to use the new registration flow\n');
+
+console.log('📋 Migration Steps:');
+console.log('1. Export data from MongoDB (skylanders, metadata, etc.)');
+console.log('2. Import skylanders data into SQLite');
+console.log('3. Import daily metadata into SQLite');
+console.log('4. Users must register again with email/password\n');
+
+console.log('💡 To migrate Skylanders data:');
+console.log(' - The scrape script (scripts/scrape.ts) can populate the database');
+console.log(' - Or manually export from MongoDB and import into SQLite\n');
+
+console.log('For custom migration needs, modify this script or contact the development team.');
From 25c28de2fbd2084b9423ae6ee5f4026a452ab43b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Oct 2025 16:19:42 +0000
Subject: [PATCH 4/4] Improve migration guide clarity based on code review
feedback
Co-authored-by: brandonapt <63559800+brandonapt@users.noreply.github.com>
---
scripts/migrate-mongo-to-sqlite.js | 62 ++++++++++++++++++------------
1 file changed, 37 insertions(+), 25 deletions(-)
diff --git a/scripts/migrate-mongo-to-sqlite.js b/scripts/migrate-mongo-to-sqlite.js
index 608fe44..a7fa132 100644
--- a/scripts/migrate-mongo-to-sqlite.js
+++ b/scripts/migrate-mongo-to-sqlite.js
@@ -1,34 +1,46 @@
#!/usr/bin/env node
/**
- * MongoDB to SQLite Migration Script
+ * MongoDB to SQLite Migration Guide
*
- * This script helps migrate data from MongoDB to SQLite.
+ * This file provides guidance on migrating data from MongoDB to SQLite.
+ * For actual migration, you'll need to customize based on your specific data.
*
- * Usage:
- * 1. Ensure MongoDB is still accessible
- * 2. Update MONGODB_URI in this file
- * 3. Run: node scripts/migrate-mongo-to-sqlite.js
+ * IMPORTANT: This is a guide, not an automated migration script.
*/
-console.log('MongoDB to SQLite Migration Script');
-console.log('===================================\n');
+console.log('MongoDB to SQLite Migration Guide');
+console.log('==================================\n');
console.log('⚠️ IMPORTANT NOTES:');
-console.log('1. This script requires the old MongoDB dependencies to be installed');
-console.log('2. Make sure your MongoDB instance is accessible');
-console.log('3. Backup your data before running this migration');
-console.log('4. User passwords cannot be migrated (Clerk handled auth before)');
-console.log('5. Users will need to use the new registration flow\n');
-
-console.log('📋 Migration Steps:');
-console.log('1. Export data from MongoDB (skylanders, metadata, etc.)');
-console.log('2. Import skylanders data into SQLite');
-console.log('3. Import daily metadata into SQLite');
-console.log('4. Users must register again with email/password\n');
-
-console.log('💡 To migrate Skylanders data:');
-console.log(' - The scrape script (scripts/scrape.ts) can populate the database');
-console.log(' - Or manually export from MongoDB and import into SQLite\n');
-
-console.log('For custom migration needs, modify this script or contact the development team.');
+console.log('1. This is a migration GUIDE, not an automated script');
+console.log('2. Users cannot be migrated (Clerk handled auth before)');
+console.log('3. Skylanders data can be repopulated using the scrape script');
+console.log('4. All users will need to re-register with email/password\n');
+
+console.log('📋 Recommended Migration Steps:\n');
+
+console.log('1. SKYLANDERS DATA:');
+console.log(' Option A: Run the scrape script to repopulate:');
+console.log(' $ npm run dev');
+console.log(' $ node scripts/scrape.ts');
+console.log(' Option B: Export from MongoDB and manually import\n');
+
+console.log('2. USER DATA:');
+console.log(' - User accounts CANNOT be migrated (Clerk handled passwords)');
+console.log(' - All users must re-register with email/password');
+console.log(' - Consider sending notification to existing users\n');
+
+console.log('3. DAILY METADATA:');
+console.log(' - Will be automatically generated on first access');
+console.log(' - No migration needed\n');
+
+console.log('4. SETTINGS & PRIVACY:');
+console.log(' - Default settings applied for new users');
+console.log(' - Users can reconfigure after registration\n');
+
+console.log('For custom migration needs, you can:');
+console.log('- Export MongoDB collections as JSON');
+console.log('- Write custom import scripts using server/utils/database.ts functions');
+console.log('- Contact the development team for assistance\n');
+