require(“dotenv”).config(); const express = require(“express”); const session = require(“express-session”); const passport = require(“passport”); const GoogleStrategy = require(“passport-google-oauth20”).Strategy; const Database = require(“better-sqlite3”); const crypto = require(“crypto”); const app = express(); const PORT = Number(process.env.PORT || 3000); const BASE_URL = (process.env.PUBLIC_BASE_URL || “http://localhost:3000”).replace(/\/+$/, “”); const required = [ “SESSION_SECRET”, “GOOGLE_CLIENT_ID”, “GOOGLE_CLIENT_SECRET”, “GOOGLE_CALLBACK_URL”, “PUBLIC_BASE_URL” ]; const missing = required.filter(key => !process.env[key]); if (missing.length) { console.error(“Missing .env settings:”, missing.join(“, “)); process.exit(1); } if (process.env.SESSION_SECRET.length < 32) { console.error("SESSION_SECRET must be at least 32 characters."); process.exit(1); } const db = new Database("link89xx.sqlite"); db.pragma("journal_mode = WAL"); db.exec(` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS links ( code TEXT PRIMARY KEY, url TEXT NOT NULL, user_id TEXT NOT NULL, clicks INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL ); `); app.disable("x-powered-by"); app.use(express.json({ limit: "10kb" })); app.use(express.urlencoded({ extended: false, limit: "10kb" })); app.use(session({ name: "link89xx.sid", secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", maxAge: 86400000 } })); app.use(passport.initialize()); app.use(passport.session()); passport.serializeUser((user, done) => done(null, user.id)); passport.deserializeUser((id, done) => { try { done(null, db.prepare( “SELECT * FROM users WHERE id = ?” ).get(id) || false); } catch (error) { done(error); } }); passport.use(new GoogleStrategy({ clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, callbackURL: process.env.GOOGLE_CALLBACK_URL }, (accessToken, refreshToken, profile, done) => { try { const email = (profile.emails || []).find( item => item.value )?.value; const verified = profile._json && profile._json.email_verified; if (!email || !(verified === true || verified === “true”)) { return done(null, false); } db.prepare(` INSERT INTO users (id, name, email) VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = excluded.name, email = excluded.email `).run(profile.id, profile.displayName || “User”, email); done(null, db.prepare( “SELECT * FROM users WHERE id = ?” ).get(profile.id)); } catch (error) { done(error); } })); function requireLogin(req, res, next) { if (req.isAuthenticated()) return next(); if (req.path.startsWith(“/api/”)) { return res.status(401).json({ error: “Please sign in with Google first.” }); } res.redirect(“/”); } function isValidUrl(value) { try { const parsed = new URL(value); return [“http:”, “https:”].includes(parsed.protocol); } catch { return false; } } function escapeHtml(value) { return String(value).replace(/[&<>“‘]/g, char => ({ “&”: “&”, “<": "<", ">“: “>”, ‘”‘: “"”, “‘”: “'” })[char]); } // Google registration and login app.get(“/auth/google”, passport.authenticate(“google”, { scope: [“profile”, “email”], state: true })); app.get(“/auth/google/callback”, (req, res, next) => { passport.authenticate(“google”, (error, user) => { if (error) return next(error); if (!user) return res.redirect(“/?error=google_login”); req.logIn(user, loginError => { if (loginError) return next(loginError); res.redirect(“/”); }); })(req, res, next); }); // Logout app.post(“/logout”, requireLogin, (req, res, next) => { req.logout(error => { if (error) return next(error); req.session.destroy(sessionError => { if (sessionError) return next(sessionError); res.clearCookie(“link89xx.sid”); res.redirect(“/”); }); }); }); // Main website app.get(“/”, (req, res) => { const user = req.user || null; const loginError = req.query.error === “google_login” ? ‘

Google sign-in failed. Please try again.

‘ : “”; let content = “”; if (user) { content = `

Welcome, ${escapeHtml(user.name)}

${escapeHtml(user.email)}

Your links

`; } else { content = ` G Continue with Google

New users register automatically with Google.

`; } const dashboardScript = user ? ` ` : “”; res.type(“html”).send(` Link89xx URL Shortener

Link89xx

Make long URLs short and shareable.

${loginError} ${content}
${dashboardScript} `); }); // List links belonging to the signed-in user app.get(“/api/links”, requireLogin, (req, res) => { try { const rows = db.prepare(` SELECT code, url, clicks FROM links WHERE user_id = ? ORDER BY created_at DESC LIMIT 100 `).all(req.user.id); res.json({ links: rows.map(row => ({ shortUrl: BASE_URL + “/” + row.code, url: row.url, clicks: row.clicks })) }); } catch (error) { console.error(error); res.status(500).json({error: “Unable to load links.”}); } }); // Create a short link app.post(“/api/shorten”, requireLogin, (req, res) => { const url = String(req.body.url || “”).trim(); if (url.length > 2048 || !isValidUrl(url)) { return res.status(400).json({ error: “Enter a valid HTTP or HTTPS URL.” }); } try { const insert = db.prepare(` INSERT INTO links (code, url, user_id, clicks, created_at) VALUES (?, ?, ?, 0, ?) `); let code = null; for (let attempt = 0; attempt < 5; attempt++) { const candidate = crypto.randomBytes(5).toString("base64url"); try { insert.run( candidate, url, req.user.id, new Date().toISOString() ); code = candidate; break; } catch (error) { if (!String(error.code || "").includes("CONSTRAINT")) { throw error; } } } if (!code) throw new Error("Could not allocate short code."); res.json({shortUrl: BASE_URL + "/" + code}); } catch (error) { console.error(error); res.status(500).json({error: "Unable to save the short link."}); } }); // Redirect short links app.get("/:code", (req, res, next) => { const code = req.params.code; if ([“favicon.ico”, “auth”, “api”, “logout”].includes(code)) { return next(); } try { const row = db.prepare( “SELECT url FROM links WHERE code = ?” ).get(code); if (!row) { return res.status(404).send(“Short link not found.”); } db.prepare( “UPDATE links SET clicks = clicks + 1 WHERE code = ?” ).run(code); res.redirect(302, row.url); } catch (error) { console.error(error); res.status(500).send(“Internal server error.”); } }); app.use((error, req, res, next) => { console.error(error); if (res.headersSent) return next(error); if (req.path.startsWith(“/api/”)) { return res.status(500).json({ error: “Internal server error.” }); } res.status(500).send(“Internal server error.”); }); app.listen(PORT, “0.0.0.0”, () => { console.log(“Link89xx running on port ” + PORT); console.log(“Public URL: ” + BASE_URL); });

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *