decentralized crowd-sourced service status tracker on ATProto
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});