const express = require("express"); const session = require("express-session"); const { Issuer } = require("openid-client"); const { createTemplateRenderer } = require("./utils/templates.js"); const { Client } = require("pg"); const { generateOtp } = require("mobilepass"); require("dotenv").config(); const PORT = process.env.PORT || 3000; const app = express(); app.use(express.json()); app.use( session({ secret: process.env.COOKIE_SECRET, saveUninitialized: false, cookie: { maxAge: 10 * 60 * 1000 }, }) ); const renderHtml = createTemplateRenderer({ useCache: process.env.NODE_ENV === "production", }); (async () => { const issuer = await Issuer.discover(process.env.OIDC_WELL_KNOWN_URL); const client = new issuer.Client({ client_id: process.env.CLIENT_ID, client_secret: process.env.CLIENT_SECRET, response_types: ["code"], }); const pg = new Client(); await pg.connect(); await pg.query(` CREATE TABLE IF NOT EXISTS counters ( id SERIAL PRIMARY KEY, counter INTEGER NOT NULL ) `); app.get("/", async (req, res) => { if (req.session.user) { const counter = await pg.query( "SELECT counter FROM counters WHERE id = 1" ).then((result) => { if (result.rows.length > 0) { return result.rows[0].counter; } else { return 0; } }); res.send( renderHtml("index.html", { USERNAME: req.session.user.sub, OTP: await generateOtp(process.env.ACTIVATION_CODE, counter), COUNTER: counter, }) ); } else { res.send(renderHtml("login.html")); } }); const stateMap = {}; app.post("/authorize", (req, res) => { const { redirectUri } = req.body; if (!redirectUri) { return res.send( renderHtml("login-fail.html", { ERROR: "redirectUri missing or invalid", }) ); } const state = Math.random().toString(36).substring(2); stateMap[state] = { redirectUri }; const url = client.authorizationUrl({ scope: "openid profile", redirect_uri: redirectUri, state, }); // 回傳 JSON 給前端,讓前端負責導向 res.json({ redirect: url }); }); app.get("/callback", async (req, res) => { try { const state = req.query.state; const { redirectUri } = stateMap[state]; const params = client.callbackParams(req); const tokenSet = await client.callback(redirectUri, params, { state }); const userinfo = await client.userinfo(tokenSet.access_token); req.session.user = userinfo; res.redirect("/"); } catch (error) { res.send(renderHtml("login-fail.html", { ERROR: error })); } }); app.put("/counter", (req, res) => { if (req.session.user) { const { counter } = req.body; try { pg.query( ` INSERT INTO counters (id, counter) VALUES (1, $1) ON CONFLICT (id) DO UPDATE SET counter = $1 `, [counter] ); res.json({ success: true }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } } else { res.status(401).json({ success: false, error: "Unauthorized" }); } }); app.post("/logout", (req, res) => { req.session.destroy(() => { res.redirect("/"); }); }); process.on("SIGINT", async () => { await pg.end(); console.log("Database connection closed."); process.exit(0); }); app.listen(PORT, () => { console.log(`listening at http://localhost:${PORT}`); }); })();