initial commit
This commit is contained in:
commit
0144534910
5 changed files with 1333 additions and 0 deletions
3
.env.example
Normal file
3
.env.example
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
PORT=
|
||||||
|
DATABASE_URL=
|
||||||
|
SERVER_STATIC_TOKEN=
|
||||||
143
.gitignore
vendored
Normal file
143
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
.output
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Sveltekit cache directory
|
||||||
|
.svelte-kit/
|
||||||
|
|
||||||
|
# vitepress build output
|
||||||
|
**/.vitepress/dist
|
||||||
|
|
||||||
|
# vitepress cache directory
|
||||||
|
**/.vitepress/cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# Firebase cache directory
|
||||||
|
.firebase/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
.pnpm-store
|
||||||
|
|
||||||
|
# yarn v3
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# Vite files
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
.vite/
|
||||||
153
index.js
Normal file
153
index.js
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { Pool } from 'pg';
|
||||||
|
import axios from 'axios';
|
||||||
|
import 'dotenv/config';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
connectionTimeoutMillis: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
pool.on('error', (err) => {
|
||||||
|
console.error('pool error: ', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
const SERVER_STATIC_TOKEN = process.env.SERVER_STATIC_TOKEN;
|
||||||
|
|
||||||
|
|
||||||
|
async function authenticateUser(req, res, next) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
|
||||||
|
return res.status(401).json({ error: 'Missing or malformed Authorization header' });
|
||||||
|
}
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
try {
|
||||||
|
const startAuthTime = Date.now();
|
||||||
|
const response = await axios.get('https://account.spbr.net/api/v2/users/@me/profile', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
timeout: 5000
|
||||||
|
});
|
||||||
|
if (!response.data || !response.data.pid) {
|
||||||
|
|
||||||
|
return res.status(401).json({ error: 'Invalid user token response' });
|
||||||
|
}
|
||||||
|
req.user = response.data;
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(401).json({ error: 'Authentication failed', details: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function authenticateServer(req, res, next) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return res.status(401).json({ error: 'Missing or malformed Authorization header' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
if (token !== SERVER_STATIC_TOKEN) {
|
||||||
|
return res.status(403).json({ error: 'Forbidden: Invalid server token' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
app.get('/api/v1/@me', authenticateUser, async (req, res) => {
|
||||||
|
const userPid = req.user.pid;
|
||||||
|
try {
|
||||||
|
const queryText = `
|
||||||
|
SELECT
|
||||||
|
f.pid AS friend_pid,
|
||||||
|
f.since AS friended_since,
|
||||||
|
p.presence,
|
||||||
|
p.updated_at
|
||||||
|
FROM friendships_of_pid($1) f
|
||||||
|
LEFT JOIN user_presences p ON f.pid = p.pid;
|
||||||
|
`;
|
||||||
|
const queryStartTime = Date.now();
|
||||||
|
const { rows } = await pool.query(queryText, [userPid]);
|
||||||
|
const responseData = {
|
||||||
|
user: req.user,
|
||||||
|
friends: rows.map(row => ({
|
||||||
|
pid: row.friend_pid,
|
||||||
|
since: row.friended_since,
|
||||||
|
presence: row.presence || null,
|
||||||
|
last_updated: row.updated_at || null
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.status(200).json(responseData);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ code: "0402", error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.get('/api/v1/presence/:pid', authenticateUser, async (req, res) => {
|
||||||
|
const targetPid = parseInt(req.params.pid, 10);
|
||||||
|
const userPid = req.user.pid;
|
||||||
|
|
||||||
|
if (isNaN(targetPid)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid target PID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const friendshipCheck = `
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM friendships_of_pid($1) WHERE pid = $2
|
||||||
|
) AS is_friend;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const checkResult = await pool.query(friendshipCheck, [userPid, targetPid]);
|
||||||
|
|
||||||
|
if (!checkResult.rows[0].is_friend) {
|
||||||
|
return res.status(403).json({ error: 'Request denied: target user is not in your friends list' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const presenceQuery = `SELECT presence, updated_at FROM user_presences WHERE pid = $1;`;
|
||||||
|
const presenceResult = await pool.query(presenceQuery, [targetPid]);
|
||||||
|
|
||||||
|
if (presenceResult.rows.length === 0) {
|
||||||
|
return res.status(204).json({ error: 'Presence not found for target PID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
pid: targetPid,
|
||||||
|
presence: presenceResult.rows[0].presence,
|
||||||
|
last_updated: presenceResult.rows[0].updated_at
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/v1/presence', authenticateServer, async (req, res) => {
|
||||||
|
const { pid, presence } = req.body;
|
||||||
|
|
||||||
|
if (!pid || !presence) {
|
||||||
|
return res.status(400).json({ error: 'Missing pid or presence body' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const queryText = `
|
||||||
|
INSERT INTO user_presences (pid, presence, updated_at)
|
||||||
|
VALUES ($1, $2, NOW())
|
||||||
|
ON CONFLICT (pid)
|
||||||
|
DO UPDATE SET presence = EXCLUDED.presence, updated_at = NOW();
|
||||||
|
`;
|
||||||
|
await pool.query(queryText, [pid, JSON.stringify(presence)]);
|
||||||
|
return res.status(200).json({ status: 'success' });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`running on port ${PORT}`);
|
||||||
|
});
|
||||||
26
package.json
Normal file
26
package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "friends-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devEngines": {
|
||||||
|
"packageManager": {
|
||||||
|
"name": "pnpm",
|
||||||
|
"version": "^11.3.0",
|
||||||
|
"onFail": "download"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.19.0",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"pg": "^8.22.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1008
pnpm-lock.yaml
generated
Normal file
1008
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue