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