decentralized crowd-sourced service status tracker on ATProto
at main 4.3 kB view raw
1// server.js – now with live API 2const express = require("express"); 3const session = require("express-session"); 4const path = require("path"); 5const fs = require("fs"); 6 7const app = express(); 8 9app.set("view engine", "ejs"); 10app.set("views", path.join(__dirname, "views")); 11app.use(express.urlencoded({ extended: true })); 12app.use(express.static("public")); 13app.use( 14 session({ 15 secret: "temporary-dev-secret-123", 16 resave: false, 17 saveUninitialized: false, 18 cookie: { secure: false }, 19 }), 20); 21 22// OAuth config (hard-coded for now; will add .env it later) 23const CLIENT_ID = 24 process.env.CLIENT_ID || "https://atstatus.net/oauth-client-metadata.json"; 25const REDIRECT_URI = "https://atstatus.net/oauth/callback"; 26const SCOPES = "atproto transition:generic"; 27 28// Login route 29app.get("/login", (req, res) => { 30 const state = Math.random().toString(36).substring(7); // Simple random state 31 req.session.oauthState = state; 32 33 const authUrl = new URL("https://bsky.social/oauth/authorize"); 34 authUrl.searchParams.append("client_id", CLIENT_ID); 35 authUrl.searchParams.append("redirect_uri", REDIRECT_URI); 36 authUrl.searchParams.append("response_type", "code"); 37 authUrl.searchParams.append("scope", SCOPES); 38 authUrl.searchParams.append("state", state); 39 40 res.redirect(authUrl.toString()); 41}); 42 43// Load services list 44const SERVICES = JSON.parse(fs.readFileSync("./data/services.json", "utf8")); 45 46// Fake in-memory report store (replace with real ATProto query later) 47let reports = {}; // { "cloudflare.com": { up: 5, down: 12, degraded: 3, lastHour: [...] } } 48app.get("/", (req, res) => { 49 res.render( 50 "home", 51 { services: SERVICES, session: req.session }, 52 (err, html) => { 53 res.render("layout", { body: html, session: req.session }); 54 }, 55 ); 56}); 57 58// API: current status of all services 59app.get("/api/status", (req, res) => { 60 const now = Date.now(); 61 const oneHourAgo = now - 60 * 60 * 1000; 62 63 const status = {}; 64 for (const svc of SERVICES) { 65 const domain = svc.domain; 66 const r = reports[domain] || { up: 0, down: 0, degraded: 0, lastHour: [] }; 67 const recent = r.lastHour.filter((t) => t > oneHourAgo); 68 const down = recent.filter((s) => s === "down").length; 69 const degraded = recent.filter((s) => s === "degraded").length; 70 const total = recent.length || 1; 71 72 let current = "up"; 73 if (down / total > 0.3) current = "down"; 74 else if (degraded / total > 0.3 || down > 0) current = "degraded"; 75 76 status[domain] = { 77 current, 78 reports: recent.length, 79 }; 80 } 81 res.json(status); 82}); 83 84app.get("/oauth/callback", async (req, res) => { 85 if (!req.query.code || req.query.state !== req.session.oauthState) { 86 return res.status(400).send("Invalid state or missing code"); 87 } 88 89 try { 90 // Exchange code for tokens 91 const tokenResponse = await fetch("https://bsky.social/oauth/token", { 92 method: "POST", 93 headers: { "Content-Type": "application/x-www-form-urlencoded" }, 94 body: new URLSearchParams({ 95 grant_type: "authorization_code", 96 code: req.query.code, 97 redirect_uri: REDIRECT_URI, 98 client_id: CLIENT_ID, 99 }), 100 }); 101 102 if (!tokenResponse.ok) 103 throw new Error(`Token error: ${await tokenResponse.text()}`); 104 105 const tokens = await tokenResponse.json(); 106 console.log("Tokens received:", { 107 did: tokens.did, 108 hasAccess: !!tokens.access_token, 109 }); 110 111 // Log in with the access token 112 const agent = new BskyAgent({ service: "https://bsky.social" }); 113 await agent.login({ 114 identifier: tokens.did, 115 password: tokens.access_token, 116 }); 117 118 // Save to session 119 req.session.did = agent.did; 120 req.session.handle = agent.session?.data?.handle || tokens.did; 121 req.session.accessJwt = tokens.access_token; 122 req.session.refreshJwt = tokens.refresh_token; 123 124 console.log("Logged in as @" + req.session.handle); 125 res.redirect("/"); 126 } catch (err) { 127 console.error("OAuth callback failed:", err); 128 res.status(500).send("Login failed — check logs"); 129 } 130}); 131 132app.get("/logout", (req, res) => { 133 req.session.destroy(() => res.redirect("/")); 134}); 135 136const PORT = process.env.PORT || 3000; 137app.listen(PORT, () => { 138 console.log(`atstatus.net → http://localhost:${PORT}`); 139});