5 changed files with 3758 additions and 0 deletions
File diff suppressed because it is too large
@ -0,0 +1,33 @@ |
|||
{ |
|||
"name": "origin-dex-api", |
|||
"version": "1.0.0", |
|||
"main": "dist/server.js", |
|||
"scripts": { |
|||
"test": "echo \"Error: no test specified\" && exit 1", |
|||
"start": "node dist/server.js", |
|||
"dev": "ts-node-dev src/server.ts", |
|||
"build": "tsc" |
|||
}, |
|||
"keywords": [], |
|||
"author": "", |
|||
"license": "ISC", |
|||
"description": "", |
|||
"dependencies": { |
|||
"@types/bcrypt": "^5.0.2", |
|||
"@types/jsonwebtoken": "^9.0.7", |
|||
"bcrypt": "^5.1.1", |
|||
"cors": "^2.8.5", |
|||
"dotenv": "^16.4.5", |
|||
"express": "^4.21.1", |
|||
"jsonwebtoken": "^9.0.2", |
|||
"sqlite": "^5.0.1", |
|||
"sqlite3": "^5.1.6" |
|||
}, |
|||
"devDependencies": { |
|||
"@types/cors": "^2.8.17", |
|||
"@types/express": "^5.0.0", |
|||
"@types/node": "^22.8.6", |
|||
"ts-node-dev": "^2.0.0", |
|||
"typescript": "^5.6.3" |
|||
} |
|||
} |
|||
@ -0,0 +1,423 @@ |
|||
import dotenv from 'dotenv'; |
|||
// Add this line before other imports
|
|||
dotenv.config(); |
|||
import express from 'express'; |
|||
import { Request, Response, NextFunction } from 'express'; |
|||
import cors from 'cors'; |
|||
import sqlite3 from 'sqlite3'; |
|||
import { open } from 'sqlite'; |
|||
import jwt from 'jsonwebtoken'; |
|||
import bcrypt from 'bcrypt'; |
|||
import fs from 'fs/promises'; |
|||
import path from 'path'; |
|||
|
|||
const app = express(); |
|||
const port = process.env.PORT || 5000; |
|||
const JWT_SECRET = process.env.JWT_SECRET; |
|||
if (!JWT_SECRET) { |
|||
throw new Error('JWT_SECRET must be defined'); |
|||
} |
|||
|
|||
interface AuthRequest extends Request { |
|||
user?: any; |
|||
} |
|||
|
|||
// Middleware
|
|||
app.use(cors()); |
|||
app.use(express.json()); |
|||
|
|||
const userDbPromise = open({ |
|||
filename: './user_data.db', // This will be created in the api folder
|
|||
driver: sqlite3.Database |
|||
}); |
|||
|
|||
// Initialize users table
|
|||
async function initializeDb() { |
|||
const db = await userDbPromise; |
|||
await db.exec(` |
|||
CREATE TABLE IF NOT EXISTS users ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
username TEXT UNIQUE NOT NULL, |
|||
password TEXT NOT NULL, |
|||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP |
|||
); |
|||
|
|||
CREATE TABLE IF NOT EXISTS caught_pokemon ( |
|||
user_id INTEGER, |
|||
pfic TEXT, |
|||
caught_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
|||
PRIMARY KEY (user_id, pfic), |
|||
FOREIGN KEY (user_id) REFERENCES users(id) |
|||
); |
|||
`);
|
|||
} |
|||
|
|||
initializeDb().catch(console.error); |
|||
|
|||
// Update the auth middleware
|
|||
const authenticateToken = ( |
|||
req: AuthRequest, |
|||
res: Response, |
|||
next: NextFunction |
|||
): void => { |
|||
const authHeader = req.headers['authorization']; |
|||
const token = authHeader && authHeader.split(' ')[1]; |
|||
|
|||
if (!token) { |
|||
res.status(401).json({ error: 'Authentication required' }); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
const user = jwt.verify(token, JWT_SECRET as jwt.Secret); |
|||
req.user = user; |
|||
next(); |
|||
} catch (err) { |
|||
res.status(403).json({ error: 'Invalid token' }); |
|||
return; |
|||
} |
|||
}; |
|||
|
|||
// Update the route handlers
|
|||
app.post('/api/auth/register', (req: Request, res: Response) => { |
|||
const { username, password } = req.body; |
|||
|
|||
userDbPromise.then(async (db) => { |
|||
try { |
|||
const existingUser = await db.get('SELECT id FROM users WHERE username = ?', username); |
|||
if (existingUser) { |
|||
return res.status(400).json({ error: 'Username already exists' }); |
|||
} |
|||
|
|||
const hashedPassword = await bcrypt.hash(password, 10); |
|||
await db.run( |
|||
'INSERT INTO users (username, password) VALUES (?, ?)', |
|||
[username, hashedPassword] |
|||
); |
|||
|
|||
res.status(201).json({ message: 'User registered successfully' }); |
|||
} catch (err) { |
|||
console.error('Registration error:', err); |
|||
res.status(500).json({ error: 'Internal server error' }); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
app.post('/api/auth/login', (req: Request, res: Response) => { |
|||
const { username, password } = req.body; |
|||
|
|||
userDbPromise.then(async (db) => { |
|||
try { |
|||
const user = await db.get('SELECT * FROM users WHERE username = ?', username); |
|||
if (!user) { |
|||
return res.status(401).json({ error: 'Invalid credentials' }); |
|||
} |
|||
|
|||
const validPassword = await bcrypt.compare(password, user.password); |
|||
if (!validPassword) { |
|||
return res.status(401).json({ error: 'Invalid credentials' }); |
|||
} |
|||
|
|||
const token = jwt.sign( |
|||
{ id: user.id, username: user.username }, |
|||
JWT_SECRET as jwt.Secret, |
|||
{ expiresIn: '24h' } |
|||
); |
|||
|
|||
res.json({ |
|||
id: user.id, |
|||
username: user.username, |
|||
token |
|||
}); |
|||
} catch (err) { |
|||
console.error('Login error:', err); |
|||
res.status(500).json({ error: 'Internal server error' }); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
// Database connection
|
|||
const dbPromise = open({ |
|||
filename: '../pokemon_forms.db', // Adjust path to your database file
|
|||
driver: sqlite3.Database |
|||
}); |
|||
|
|||
function processPokemonData(pokemonData: any[]): any[][] { |
|||
const pokemonList: any[][] = []; |
|||
let current_group: any[] = []; |
|||
let current_dex_number = 0; |
|||
let current_generation = 0; |
|||
let pokemon_forms: any[] = []; |
|||
|
|||
for (const pokemon of pokemonData) { |
|||
if (pokemon.national_dex !== current_dex_number) { |
|||
if (pokemon_forms.length > 0) { |
|||
for (const form of pokemon_forms) { |
|||
current_group.push(form); |
|||
if (current_group.length === 30) { |
|||
pokemonList.push([...current_group]); |
|||
current_group = []; |
|||
} |
|||
} |
|||
pokemon_forms = []; |
|||
} |
|||
current_dex_number = pokemon.national_dex; |
|||
|
|||
if (!pokemon.form_name) { |
|||
if (current_generation === null || pokemon.generation !== current_generation) { |
|||
if (current_group.length > 0) { |
|||
while (current_group.length < 30) { |
|||
current_group.push(null); |
|||
} |
|||
pokemonList.push([...current_group]); |
|||
current_group = []; |
|||
} |
|||
current_generation = pokemon.generation || 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
pokemon_forms.push(pokemon); |
|||
} |
|||
|
|||
// Handle remaining pokemon forms
|
|||
for (const form of pokemon_forms) { |
|||
current_group.push(form); |
|||
if (current_group.length === 30) { |
|||
pokemonList.push([...current_group]); |
|||
current_group = []; |
|||
} |
|||
} |
|||
|
|||
// Handle the last group
|
|||
if (current_group.length > 0) { |
|||
while (current_group.length < 30) { |
|||
current_group.push(null); |
|||
} |
|||
pokemonList.push([...current_group]); |
|||
} |
|||
|
|||
return pokemonList; |
|||
} |
|||
|
|||
// Routes
|
|||
app.get('/api/pokemon', async (req, res) => { |
|||
try { |
|||
const db = await dbPromise; |
|||
const pokemon = await db.all(` |
|||
SELECT |
|||
pf.national_dex, pf.name, pf.form_name, pf.PFIC, pf.generation, |
|||
ps.storable_in_home, m.icon_path, m.name as mark_name |
|||
FROM pokemon_forms pf |
|||
JOIN pokemon_storage ps ON pf.PFIC = ps.PFIC |
|||
LEFT JOIN form_marks fm ON pf.PFIC = fm.pfic |
|||
LEFT JOIN marks m ON fm.mark_id = m.id |
|||
WHERE ps.storable_in_home = 1 |
|||
ORDER BY pf.PFIC |
|||
`);
|
|||
|
|||
const processedData = processPokemonData(pokemon); |
|||
res.json(processedData); |
|||
} catch (err) { |
|||
console.error('Error fetching pokemon:', err); |
|||
res.status(500).json({ error: 'Internal server error' }); |
|||
} |
|||
}); |
|||
|
|||
app.get('/api/pokemon/:pfic/details', async (req, res) => { |
|||
try { |
|||
const db = await dbPromise; |
|||
const { pfic } = req.params; |
|||
|
|||
const details = await db.get(` |
|||
SELECT pf.name, pf.form_name, pf.national_dex, pf.generation, |
|||
ps.storable_in_home, pf.is_baby_form |
|||
FROM pokemon_forms pf |
|||
LEFT JOIN pokemon_storage ps ON pf.PFIC = ps.PFIC |
|||
WHERE pf.PFIC = ? |
|||
`, pfic);
|
|||
|
|||
const encounters = await db.all(` |
|||
SELECT g.name as game_name, e.location, e.day, e.time, |
|||
e.dual_slot, e.static_encounter_count, e.static_encounter, |
|||
e.extra_text, e.stars, e.rods, e.fishing |
|||
FROM encounters e |
|||
JOIN games g ON e.game_id = g.id |
|||
WHERE e.pfic = ? |
|||
ORDER BY g.name, e.location |
|||
`, pfic);
|
|||
|
|||
res.json({ ...details, encounters }); |
|||
} catch (err) { |
|||
console.error('Error fetching pokemon details:', err); |
|||
res.status(500).json({ error: 'Internal server error' }); |
|||
} |
|||
}); |
|||
|
|||
app.get('/api/plan', authenticateToken, async (req: AuthRequest, res: Response) => { |
|||
try { |
|||
// Read the efficiency plan file
|
|||
const planData = await fs.readFile( |
|||
path.join(__dirname, '../../efficiency_plan.json'), |
|||
'utf-8' |
|||
); |
|||
const efficiencyPlan = JSON.parse(planData); |
|||
|
|||
// Get the Pokemon database connection
|
|||
const db = await open({ |
|||
filename: '../pokemon_forms.db', |
|||
driver: sqlite3.Database |
|||
}); |
|||
|
|||
// Get user's caught Pokemon
|
|||
const userDb = await userDbPromise; |
|||
const caughtPokemon = await userDb.all( |
|||
'SELECT pfic FROM caught_pokemon WHERE user_id = ?', |
|||
[req.user.id] |
|||
); |
|||
const caughtPfics = new Set(caughtPokemon.map(p => p.pfic)); |
|||
|
|||
// Helper function to get evolution methods
|
|||
async function getEvolutionMethods(fromPfic: string, toPfic: string) { |
|||
// Try direct evolution first
|
|||
const direct = await db.get(` |
|||
SELECT method, to_pfic |
|||
FROM evolution_chains |
|||
WHERE from_pfic = ? AND to_pfic = ? |
|||
`, [fromPfic, toPfic]);
|
|||
|
|||
if (direct) { |
|||
return [direct.method]; |
|||
} |
|||
|
|||
// Try indirect evolution path
|
|||
const methods = await db.all(` |
|||
WITH RECURSIVE evolution_path AS ( |
|||
SELECT from_pfic, to_pfic, method, 1 as depth |
|||
FROM evolution_chains |
|||
WHERE from_pfic = ? |
|||
|
|||
UNION ALL |
|||
|
|||
SELECT e.from_pfic, e.to_pfic, e.method, ep.depth + 1 |
|||
FROM evolution_chains e |
|||
JOIN evolution_path ep ON e.from_pfic = ep.to_pfic |
|||
WHERE ep.depth < 3 |
|||
) |
|||
SELECT method |
|||
FROM evolution_path |
|||
WHERE to_pfic = ? |
|||
ORDER BY depth; |
|||
`, [fromPfic, toPfic]);
|
|||
|
|||
if (methods && methods.length > 0) { |
|||
return methods.map(m => m.method); |
|||
} |
|||
|
|||
return ['Evolution']; |
|||
} |
|||
|
|||
const debug_pfic = "0010-01-000-0"; |
|||
// Enhance the plan with evolution methods and account for caught Pokemon
|
|||
for (const game of efficiencyPlan) { |
|||
for (const pokemon of game.pokemon) { |
|||
// Set initial catch count
|
|||
pokemon.catch_count = 1; |
|||
if (pokemon.pfic === debug_pfic) { |
|||
console.log(`pokemon: ${pokemon.name} - ${pokemon.catch_count}`); |
|||
} |
|||
|
|||
// Add evolution targets to catch count
|
|||
if (pokemon.evolve_to) { |
|||
pokemon.catch_count += pokemon.evolve_to.length; |
|||
if (pokemon.pfic === debug_pfic) { |
|||
console.log(`pokemon: ${pokemon.name} - ${pokemon.catch_count}`); |
|||
} |
|||
|
|||
// Add evolution methods
|
|||
for (const evolution of pokemon.evolve_to) { |
|||
const methods = await getEvolutionMethods(pokemon.pfic, evolution.pfic); |
|||
evolution.method = methods.join(' → '); |
|||
} |
|||
} |
|||
|
|||
// Reduce catch count for already caught Pokemon
|
|||
if (caughtPfics.has(pokemon.pfic)) { |
|||
pokemon.catch_count = Math.max(0, pokemon.catch_count - 1); |
|||
if (pokemon.pfic === debug_pfic) { |
|||
console.log(`B pokemon: ${pokemon.name} - ${pokemon.catch_count}`); |
|||
} |
|||
} |
|||
|
|||
// Check evolution targets
|
|||
if (pokemon.evolve_to) { |
|||
for (const evolution of pokemon.evolve_to) { |
|||
if (caughtPfics.has(evolution.pfic)) { |
|||
pokemon.catch_count = Math.max(0, pokemon.catch_count - 1); |
|||
if (pokemon.pfic === debug_pfic) { |
|||
console.log(`C pokemon: ${pokemon.name} - ${pokemon.catch_count} (${evolution.pfic})`); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
await db.close(); |
|||
res.json(efficiencyPlan); |
|||
|
|||
} catch (err) { |
|||
console.error('Error loading efficiency plan:', err); |
|||
res.status(500).json({ error: 'Internal server error' }); |
|||
} |
|||
}); |
|||
|
|||
// Update the caught Pokemon routes
|
|||
app.get('/api/pokemon/caught', authenticateToken, (req: AuthRequest, res: Response) => { |
|||
void userDbPromise.then(async (db) => { |
|||
try { |
|||
const caught = await db.all( |
|||
'SELECT pfic FROM caught_pokemon WHERE user_id = ?', |
|||
req.user.id |
|||
); |
|||
res.json(caught.map(c => c.pfic)); |
|||
} catch (err) { |
|||
console.error('Error fetching caught pokemon:', err); |
|||
res.status(500).json({ error: 'Internal server error' }); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
app.post('/api/pokemon/caught/:pfic', authenticateToken, (req: AuthRequest, res: Response) => { |
|||
const { pfic } = req.params; |
|||
void userDbPromise.then(async (db) => { |
|||
try { |
|||
const existing = await db.get( |
|||
'SELECT 1 FROM caught_pokemon WHERE user_id = ? AND pfic = ?', |
|||
[req.user.id, pfic] |
|||
); |
|||
|
|||
if (existing) { |
|||
await db.run( |
|||
'DELETE FROM caught_pokemon WHERE user_id = ? AND pfic = ?', |
|||
[req.user.id, pfic] |
|||
); |
|||
res.json({ status: 'released' }); |
|||
} else { |
|||
await db.run( |
|||
'INSERT INTO caught_pokemon (user_id, pfic) VALUES (?, ?)', |
|||
[req.user.id, pfic] |
|||
); |
|||
res.json({ status: 'caught' }); |
|||
} |
|||
console.log(`Caught ${pfic}`); |
|||
} catch (err) { |
|||
console.error('Error updating caught status:', err); |
|||
res.status(500).json({ error: 'Internal server error' }); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
app.listen(port, () => { |
|||
console.log(`Server running on port ${port}`); |
|||
}); |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"target": "es2020", |
|||
"module": "commonjs", |
|||
"outDir": "./dist", |
|||
"rootDir": "./src", |
|||
"strict": true, |
|||
"esModuleInterop": true, |
|||
"skipLibCheck": true, |
|||
"forceConsistentCasingInFileNames": true |
|||
}, |
|||
"include": ["src/**/*"], |
|||
"exclude": ["node_modules"] |
|||
} |
|||
Loading…
Reference in new issue