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
Loading 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);
});
Leave a Reply