diff --git a/README.md b/README.md index f3b9c36..8b94f5c 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,22 @@ -# Enstrayed API -This repository contains the code for my personal web API written in JavaScript using the Express framework. - -## Documentation -This file contains documentation relevant for development and deployment, but not necessarily usage. Information for all endpoints is available [on my website](https://enstrayed.com/posts/20240409-API-Documentation.html). - -## Issues -If you would like to report a bug or security issue, please open a GitHub issue. If you are the operator of a service this application accesses, use the contact information provided during registration with your service to contact me directly. +## Note for Visitors +* This README mainly contains information for operation but not usage. API documentation is available [here](https://enstrayed.com/posts/20240409-API-Documentation.html). +* Have feedback or experiencing a problem with an endpoint? Please [open a GitHub issue](https://github.com/Enstrayed/enstrayedapi/issues/new). +* Security problem? [Open a ticket here](https://helpdesk.enstrayed.com/open.php) with the topic set as 'Responsible Disclosure'. +* This code is unlicensed but I don't really care if you use parts of it (I don't know why you would though). ## Configuration -The configuration is downloaded from CouchDB on startup, however two environment variables must be set to specify the URL of the CouchDB server and the credentials for accessing it: -| Variable | Required? | Purpose | -|--------------|----------------------|-----------------------------------------------------------------------------------------------------| -| `API_PORT` | No, defaults to 8081 | Sets the port the server will listen on | -| `API_DBHOST` | Yes | Complete URL of the CouchDB instance, including port and protocol | -| `API_DBCRED` | Yes | Credentials to access the CouchDB instance, in Basic Authentication format e.g. `username:password` | +TODO: Rewrite -
Configuration Example - -* `frontpage.directory`: Directory of frontpage, will be served at root with modifications. -* `mailjet.apiKey`: Mailjet API Key. -* `mailjet.senderAddress`: Email address that emails will be received from, must be verified in Mailjet admin panel. -* `nowplaying.*.apiKey`: API key of respective service. -* `nowplaying.*.target`: User that should be queried to retrieve playback information. +
Configuration Template ```json -{ - "frontpage": { - "directory": "" - }, - "mailjet": { - "apiKey": "", - "senderAddress": "" - }, - - "nowplaying": { - "lastfm": { - "apiKey": "", - "target": "" - }, - "jellyfin": { - "apiKey": "", - "host": "", - "target": "" - }, - "cider": { - "apiKeys": [], - "hosts": [] - } - } - -} ```
## Docker -In production, this application is designed to be run in Docker, and the container built by pulling the latest commit from the main branch. As such, deploying this application is just a matter of creating a directory and copying the Dockerfile: - -> [!IMPORTANT] -> Please review the Configuration section of this document for important information. By default, the `config.json` file is expected to be mounted into the container at `/app/config.json`. +TODO: Rewrite & add Komodo TOML files ```dockerfile FROM node:22 @@ -75,19 +32,7 @@ ENTRYPOINT [ "node", "index.js" ]
Docker Compose File ```yaml ---- -services: - enstrayedapi: - build: - context: . - image: enstrayedapi - container_name: enstrayedapi - restart: unless-stopped - volumes: - - ./config.json:/app/config.json + ``` -
- -## License -If for whatever reason you want to, you are free to adapt this code for your own projects or as reference. However, this software is provided as-is with no warranty or agreement to support it. \ No newline at end of file +
\ No newline at end of file diff --git a/index.js b/index.js index e0b2a20..15b9e52 100644 --- a/index.js +++ b/index.js @@ -1,31 +1,32 @@ import * as fs from 'fs' import { execSync } from 'child_process' +import postgres from 'postgres' import express, { json } from 'express' +import cookieParser from 'cookie-parser' + const app = express() -if (!process.env.API_DBHOST || !process.env.API_DBCRED) { - console.log("FATAL: API_DBHOST and API_DBCRED must be set") +if (!process.env.DATABASE_URI) { + console.log("FATAL: DATABASE_URI must be set") process.exit(1) } -const globalConfig = await fetch(`${process.env.API_DBHOST}/config/${process.env.API_DBCRED.split(":")[0]}`,{ - headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`} -}).then(response => { - if (response.status !== 200) { - console.log(`FATAL: Failed to download configuration: ${response.status} ${response.statusText}`) - process.exit(1) - } else { - return response.json() - } +const db = postgres(process.env.DATABASE_URI) + +const globalConfig = await db`select content from config where id = ${process.env.CONFIG_OVERRIDE ?? 'production'}`.then(response => {return response[0]["content"]}).catch(error => { + console.log(`FATAL: Error occured in downloading configuration: ${error}`) + process.exit(1) }) + const globalVersion = execSync(`git show --oneline -s`).toString().split(" ")[0] // Returns ISO 8601 Date & 24hr time for UTC-7/PDT const startTime = new Date(new Date().getTime() - 25200000).toISOString().slice(0,19).replace('T',' ') -export { app, fs, globalConfig, globalVersion } +export { app, fs, db, globalConfig, globalVersion } app.use(json()) // Allows receiving JSON bodies // see important note: https://expressjs.com/en/api.html#express.json +app.use(cookieParser()) // Allows receiving cookies process.on('SIGTERM', function() { console.log("Received SIGTERM, exiting...") diff --git a/liberals/auth.js b/liberals/auth.js index 171db72..066316c 100644 --- a/liberals/auth.js +++ b/liberals/auth.js @@ -1,32 +1,54 @@ -import { globalConfig } from "../index.js" +import { globalConfig, db } from "../index.js" /** - * Checks if a token exists in the sessions file (authentication) and if it has the correct permissions (authorization) + * (DEPRECATED) Checks if a token exists in the sessions file (authentication) and if it has the correct permissions (authorization) * @param {string} token Token as received by client * @param {string} scope Scope the token will need to have in order to succeed - * @returns True for successful authentication and authorization, false if either fail + * @returns {boolean} True for successful authentication and authorization, false if either fail */ async function checkToken(token,scope) { - return await fetch(`${process.env.API_DBHOST}/auth/sessions`, { - headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`} - }).then(fetchRes => { - - return fetchRes.json().then(dbRes => { - - if (dbRes.sessions[token] == undefined) { // If the token is not on the sessions list then reject - return false - } else if (dbRes.sessions[token].scopes.includes(scope)) { // If the token is on the seesions list and includes the scope then accept - return true - } else { // Otherwise reject - return false - } - - }) - - }).catch(error => { - console.log(`ERROR: auth.js: Fetch failed: ${error}`) - return false + return await db`select s.token, s.scopes, s.expires, u.username from sessions s join users u on s.owner = u.id where s.token = ${token}`.then(response => { + if (response.length === 0) { + return false + } else if (response[0]?.scopes.split(",").includes(scope)) { + return true + } else { + return false + } }) } -export {checkToken} \ No newline at end of file +/** + * Checks for tokens provided in request and validates them against the sessions table including the desired scope + * @param {object} request Express request object + * @param {string} scope Desired scope for action + * @typedef {object} Object containing the result and the username of the token owner + * @property {boolean} result Boolean result of if the check passed + * @property {string} owner Username of the token owner + * @property {number} ownerId Database ID of the token owner + */ + +async function checkTokenNew(request, scope) { + + if (!request.cookies["APIToken"] && !request.get("Authorization")) { + return { result: false, owner: "", ownerId: "" } + } else { + return await db`select s.*, u.username from sessions s join users u on s.owner = u.id where s.token = ${request.get("Authorization") ?? request.cookies["APIToken"]}`.then(response => { + if (response.length === 0) { + return { result: false, owner: response[0]?.username, ownerId: response[0]?.owner } + } else if (response[0]?.scopes.split(",").includes(scope)) { + return { result: true, owner: response[0]?.username, ownerId: response[0]?.owner } + } else { + return { result: false, owner: response[0]?.username, ownerId: response[0]?.owner } + } + }).catch(dbErr => { + return { result: false, owner: "", ownerId: "" } + }) + } + + + + +} + +export {checkToken, checkTokenNew} \ No newline at end of file diff --git a/liberals/libnowplaying.js b/liberals/libnowplaying.js index 0bfb6a2..3876e18 100644 --- a/liberals/libnowplaying.js +++ b/liberals/libnowplaying.js @@ -27,7 +27,7 @@ async function queryLastfm() { "artUrl": response.recenttracks.track[0].image[3]["#text"], "link": response.recenttracks.track[0].url }, - "html": `Album Art

I'm listening to

${response.recenttracks.track[0].name} by ${response.recenttracks.track[0].artist["#text"]}

from ${response.recenttracks.track[0].album["#text"]}

View on Last.fm
` + "html": `Album Art
I'm listening to ${response.recenttracks.track[0].name} by ${response.recenttracks.track[0].artist["#text"]} from ${response.recenttracks.track[0].album["#text"]} View on Last.fm
` } } } @@ -60,7 +60,7 @@ async function queryJellyfin() { "artUrl": `${globalConfig.nowplaying.jellyfin.hostPublic}/Items/${response[x].NowPlayingItem.Id}/Images/Primary`, "link": `https://www.last.fm/music/${response[x].NowPlayingItem.Artists[0].replaceAll(" ","+")}/_/${response[x].NowPlayingItem.Name.replaceAll(" ","+")}` }, - "html": `Album Art

I'm listening to

${response[x].NowPlayingItem.Name} by ${response[x].NowPlayingItem.Artists[0]}

from ${response[x].NowPlayingItem.Album ?? `${response[x].NowPlayingItem.Name} (Single)`}

View on Last.fm
` + "html": `Album Art
I'm listening to ${response[x].NowPlayingItem.Name} by ${response[x].NowPlayingItem.Artists[0]} from ${response[x].NowPlayingItem.Album ?? `${response[x].NowPlayingItem.Name} (Single)`} View on Last.fm
` } } diff --git a/liberals/logging.js b/liberals/logging.js index 3586855..ebbe529 100644 --- a/liberals/logging.js +++ b/liberals/logging.js @@ -4,9 +4,10 @@ * @param {object} request Parent request object * @param {number} code Status code to log, should be same as sent to client * @param {string} extra Optional extra details to add to log, ideal for caught errors + * @param {object} authresponse Optionally include result of auth response to include owner information for a token */ -function logRequest(response,request,code,extra) { - console.log(`${request.get("cf-connecting-ip") ?? request.ip} ${request.get("Authorization") ?? ""} ${request.method} ${request.path} returned ${code} ${extra ?? ""}`) +function logRequest(response,request,code,extra,authresponse) { + console.log(`${request.get("cf-connecting-ip") ?? request.ip} ${authresponse?.owner ?? ""}/${request.get("Authorization") ?? ""} ${request.method} ${request.path} returned ${code} ${extra ?? ""}`) } export { logRequest } \ No newline at end of file diff --git a/liberals/misc.js b/liberals/misc.js new file mode 100644 index 0000000..5fafd90 --- /dev/null +++ b/liberals/misc.js @@ -0,0 +1,55 @@ +function randomStringBase16(length) { + let characters = "0123456789abcdef" + let remaining = length + let returnstring = "" + while (remaining > 0) { + returnstring = returnstring + characters.charAt(Math.floor(Math.random() * characters.length)) + remaining = remaining - 1 + } + return returnstring +} + +function randomStringBase62(length) { + let characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHJIJKLMNOPQRSTUVWXYZ" + let remaining = length + let returnstring = "" + while (remaining > 0) { + returnstring = returnstring + characters.charAt(Math.floor(Math.random() * characters.length)) + remaining = remaining - 1 + } + return returnstring +} + +function getHumanReadableUserAgent(useragent) { + let formattedua = useragent.replace(/[\/()]/g," ").split(" ") + let os = "" + let browser = "" + + if (formattedua.includes("Windows")) { + os += "Windows" + } else if (formattedua.includes("Macintosh")) { + os += "macOS" + } else if (formattedua.includes("iPhone")) { + os += "iOS" + } else if (formattedua.includes("Android")) { + os += "Android" + } else if (formattedua.includes("Linux")) { + os += "Linux" + } else { + os += "Other" + } + + if (formattedua.includes("Firefox")) { + browser += "Firefox" + } else if (formattedua.includes("Chrome")) { + browser += "Chrome" + } else if (formattedua.includes("Safari")) { + browser += "Safari" + } else { + browser += "Other" + } + + return `${os} ${browser}` +} + +export { randomStringBase16, randomStringBase62, getHumanReadableUserAgent } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c53f465..9e3a581 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "1.0.0", "license": "UNLICENSED", "dependencies": { + "cookie-parser": "^1.4.7", "express": "^4.18.2", "marked": "^14.1.3", - "nodemailer": "^6.9.15" + "nodemailer": "^6.9.15", + "postgres": "^3.4.5" }, "devDependencies": { "@types/bun": "^1.0.12", @@ -157,6 +159,28 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -470,9 +494,9 @@ } }, "node_modules/marked": { - "version": "14.1.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.3.tgz", - "integrity": "sha512-ZibJqTULGlt9g5k4VMARAktMAjXoVnnr+Y3aCqW1oDftcV4BA3UmrBifzXoZyenHRk75csiPu9iwsTj4VNBT0g==", + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz", + "integrity": "sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -600,6 +624,19 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/postgres": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.5.tgz", + "integrity": "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/package.json b/package.json index 8f9de25..2daec5a 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,10 @@ { "dependencies": { + "cookie-parser": "^1.4.7", "express": "^4.18.2", "marked": "^14.1.3", - "nodemailer": "^6.9.15" + "nodemailer": "^6.9.15", + "postgres": "^3.4.5" }, "name": "enstrayedapi", "version": "1.0.0", diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..98ae8c1 --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,128 @@ +import { app, db, globalConfig } from "../index.js" // Get globals from index +import { checkTokenNew } from "../liberals/auth.js" +import { logRequest } from "../liberals/logging.js" +import { randomStringBase62, getHumanReadableUserAgent } from "../liberals/misc.js" + +app.get("/api/auth/whoami", (rreq,rres) => { + if (!rreq.cookies["APIToken"] && !rreq.get("Authorization")) { + rres.send({ "loggedIn": false, "username": "", "scopes": "" }) + } else { + db`select s.scopes, u.username from sessions s join users u on s.owner = u.id where s.token = ${rreq.cookies["APIToken"] ?? rreq.get("Authorization")}`.then(dbRes => { + if (dbRes.length > 0 && dbRes.length < 2) { + rres.send({ "loggedIn": true, "username": dbRes[0]?.username, "scopes": dbRes[0]?.scopes.split(",") }) + } else { + rres.send({ "loggedIn": false, "username": "", "scopes": "" }) + } + }).catch(dbErr => { + logRequest(rres,rreq,500,dbErr) + rres.status(500).send({ "loggedIn": false, "username": "", "scopes": "" }) + }) + } +}) + +app.get("/api/auth/login", (rreq,rres) => { + + if (rreq.query.state === "redirect") { + if (!rreq.query.destination) { + rres.redirect(`${globalConfig.oidc.authorizeUrl}?client_id=${globalConfig.oidc.clientId}&response_type=code&scope=openid enstrayedapi&redirect_uri=${rreq.protocol}://${rreq.get("Host")}/api/auth/callback&state=none`) + } else { + let newState = `redirect_${btoa(rreq.query.destination).replace("/","-")}` + rres.redirect(`${globalConfig.oidc.authorizeUrl}?client_id=${globalConfig.oidc.clientId}&response_type=code&scope=openid enstrayedapi&redirect_uri=${rreq.protocol}://${rreq.get("Host")}/api/auth/callback&state=${newState}`) + } + } else if (rreq.query.state === "display" || rreq.query.state === "close") { + rres.redirect(`${globalConfig.oidc.authorizeUrl}?client_id=${globalConfig.oidc.clientId}&response_type=code&scope=openid enstrayedapi&redirect_uri=${rreq.protocol}://${rreq.get("Host")}/api/auth/callback&state=${rreq.query.state}`) + } else { + rres.redirect(`${globalConfig.oidc.authorizeUrl}?client_id=${globalConfig.oidc.clientId}&response_type=code&scope=openid enstrayedapi&redirect_uri=${rreq.protocol}://${rreq.get("Host")}/api/auth/callback&state=none`) + } + +}) + +app.get("/api/auth/logout", (rreq,rres) => { + if (rreq.cookies["APIToken"] || rreq.get("Authorization")) { + db`delete from sessions where token = ${rreq.cookies["APIToken"] ?? rreq.get("Authorization")}`.then(dbRes => { + if (dbRes.count > 0) { + rres.send("Success") + } else { + rres.status(400).send("Token does not exist.") + } + }).catch(dbErr => { + logRequest(rres,rreq,500,dbErr) + rres.status(500).send("Exception occured while invalidating token, details: "+dbErr) + }) + } else { + rres.status(400).send("Missing token or authorization header, you may not be logged in.") + } +}) + +app.get("/api/auth/callback", (rreq,rres) => { + fetch(globalConfig.oidc.tokenUrl, { // Call token endpoint at IdP using code provdided during callback + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded"}, + body: `grant_type=authorization_code&code=${rreq.query.code}&redirect_uri=${rreq.protocol}://${rreq.get("Host")}/api/auth/callback&client_id=${globalConfig.oidc.clientId}&client_secret=${globalConfig.oidc.clientSecret}` + }).then(fetchRes1 => { + fetchRes1.json().then(fetchRes1 => { // Convert response to JSON then continue + if (fetchRes1.error) { // Fetch to token endpoint succeded but resulted in error, usually because the provided code is invalid + localError500(`Callback-Token-${fetchRes1.error}`) + } else { // Assumed success + fetch(globalConfig.oidc.userinfoUrl, { // Call userinfo endpoint at IdP using token provided during previous step + headers: { "Authorization": `Bearer ${fetchRes1.access_token}`} + }).then(fetchRes2 => { + if (fetchRes2.ok === false) { // Fetch to userinfo endpoint succeded but resulted in error (usually 401) + localError500(`Callback-Userinfo-${fetchRes2.status}`) + } else { + fetchRes2.json().then(fetchRes2 => { + let newToken = randomStringBase62(64) + let newExpiration = Date.now() + 86400 + let newComment = `Login token for ${getHumanReadableUserAgent(rreq.get("User-Agent"))} on ${rreq.get("cf-connecting-ip") ?? rreq.ip}` + + db`insert into sessions (token,owner,scopes,expires,comment) values (${newToken},(select id from users where oidc_username = ${fetchRes2.username}),${fetchRes2.enstrayedapi_scopes},${newExpiration},${newComment});`.then(dbRes1 => { + if (rreq.query.state.split("_")[0] === "redirect") { + let newDestination = atob(rreq.query.state.split("_")[1].replace("-","/")) + rres.setHeader("Set-Cookie", `APIToken=${newToken}; Domain=${rreq.hostname}; Expires=${new Date(newExpiration).toUTCString()}; Path=/`).redirect(newDestination) + } else if (rreq.query.state === "display") { + // Change this to not write the token to a cookie + rres.setHeader("Set-Cookie", `APIToken=${newToken}; Domain=${rreq.hostname}; Expires=${new Date(newExpiration).toUTCString()}; Path=/`).send(`Success! Your token is ${newToken}`) + } else if (rreq.query.state === "close") { + rres.setHeader("Set-Cookie", `APIToken=${newToken}; Domain=${rreq.hostname}; Expires=${new Date(newExpiration).toUTCString()}; Path=/`).send(` Success! You may now close this window.`) + } else { + rres.setHeader("Set-Cookie", `APIToken=${newToken}; Domain=${rreq.hostname}; Expires=${new Date(newExpiration).toUTCString()}; Path=/`).send(`Success! No state was provided, so you can close this window.`) + } + }).catch(dbErr => { + localError500(`Callback-Write-${dbErr}`) + }) + + + }) + } + }).catch(fetchErr2 => { // Fetch to userinfo endpoint failed for some other reason + localError500(`Callback-Fetch2-${fetchErr2}`) + }) + } + }) + }).catch(fetchErr1 => { // Fetch to token endpoint failed for some other reason + localError500(`Callback-Fetch-${fetchErr1}`) + }) + + function localError500(code) { + logRequest(rres,rreq,500,code) + rres.status(500).send(`An error occured during login, a token was not created.

500 ${code}`) + } +}) + +app.post("/api/auth/token", (rreq,rres) => { + rres.send("Non functional endpoint") +}) + +app.delete("/api/auth/token", (rreq,rres) => { + rres.send("Non functional endpoint") +}) + +app.get("/api/auth/tokenlist", (rreq,rres) => { + rres.send("Non functional endpoint") +}) + +app.get("/api/auth/nuke", (rreq,rres) => { + rres.send("Non functional endpoint") +}) + +export { app } \ No newline at end of file diff --git a/routes/email.js b/routes/email.js index 4119776..9058096 100644 --- a/routes/email.js +++ b/routes/email.js @@ -1,5 +1,5 @@ import { app, globalConfig } from "../index.js" // Get globals from index -import { checkToken } from "../liberals/auth.js" +import { checkTokenNew } from "../liberals/auth.js" import { logRequest } from "../liberals/logging.js" import * as nodemailer from 'nodemailer' @@ -14,10 +14,10 @@ const transporter = nodemailer.createTransport({ }) app.post("/api/sendemail", (rreq,rres) => { - checkToken(rreq.get("Authorization"),"email").then(authRes => { - if (authRes === false) { + checkTokenNew(rreq,"email").then(authRes => { + if (authRes.result === false) { rres.sendStatus(401) - } else if (authRes === true) { + } else if (authRes.result === true) { if (rreq.body == undefined || rreq.body.recipient == undefined) { // 2024-05-11: Turbo bodge check to make sure request JSON is valid, probably wont work but whatever rres.sendStatus(400) } else { @@ -29,14 +29,14 @@ app.post("/api/sendemail", (rreq,rres) => { text: rreq.body.message ?? "Message Not Set" }).then(transportResponse => { if (transportResponse.response.slice(0,1) === "2") { - logRequest(rres,rreq,200,transportResponse.response) + logRequest(rres,rreq,200,transportResponse.response,authRes) rres.status(200).send(transportResponse.response) } else { - logRequest(rres,rreq,400,transportResponse.response) + logRequest(rres,rreq,400,transportResponse.response,authRes) rres.status(400).send(transportResponse.response) } }).catch(transportError => { - logRequest(rres,rreq,500,transportError) + logRequest(rres,rreq,500,transportError,authRes) rres.sendStatus(500) }) diff --git a/routes/etyd.js b/routes/etyd.js index f185745..dbd3cf1 100644 --- a/routes/etyd.js +++ b/routes/etyd.js @@ -1,130 +1,73 @@ -import { app, globalConfig } from "../index.js" // Get globals from index -import { checkToken } from "../liberals/auth.js" +import { app, db, globalConfig } from "../index.js" // Get globals from index +import { checkToken, checkTokenNew } from "../liberals/auth.js" import { logRequest } from "../liberals/logging.js" app.get("/api/etyd*", (rreq,rres) => { - fetch(`${process.env.API_DBHOST}/etyd${rreq.path.replace("/api/etyd","")}`,{ - headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`} - }).then(dbRes => { - if (dbRes.status == 404) { - rres.sendStatus(404) + let userRequest = rreq.path.replace("/api/etyd/","") + db`select content from etyd where url = ${userRequest}`.then(response => { + if (response.length == 0) { + rres.status(404).send(`etyd.cc: URL "${userRequest}" was not found`) } else { - dbRes.json().then(dbRes => { - try { - rres.redirect(dbRes.content.url) // Node will crash if the Database entry is malformed - } catch (responseError) { - logRequest(rres,rreq,500,responseError) - rres.sendStatus(500) - } - }) + rres.redirect(response[0].content) } - }).catch(fetchError => { - logRequest(rres,rreq,500,fetchError) - rres.sendStatus(500) + }).catch(dbError => { + logRequest(rres,rreq,500,dbError) + rres.status(500).send(`etyd.cc: An internal error occured`) }) }) -// app.delete("/api/etyd*", (rreq,rres) => { +app.delete("/api/etyd*", (rreq,rres) => { -// if (rreq.get("Authorization") === undefined) { -// rres.sendStatus(400) -// } else { -// checkToken(rreq.get("Authorization"),"etyd").then(authRes => { -// if (authRes === false) { -// rres.sendStatus(401) -// } else if (authRes === true) { // Authorization successful + checkTokenNew(rreq,"etyd").then(authRes => { + if (authRes.result === false) { + rres.sendStatus(401) // Token not provided or invalid for this action + } else { + db`delete from etyd where url = ${rreq.path.replace("/api/etyd/","")} and owner = ${authRes.ownerId}`.then(dbRes => { + if (dbRes.count === 1) { + rres.sendStatus(200) + } else if (dbRes.count === 0) { + rres.sendStatus(400) + } else { + logRequest(rres, rreq, 500, `Something bad happened during delete from database`) + rres.sendStatus(500) + } + }).catch(dbErr => { + logRequest(rres, rreq, 500, dbErr) + rres.sendStatus(500) + }) + } + }) -// fetch(`${process.env.API_DBHOST}/etyd${rreq.path.replace("/api/etyd", "")}`,{ -// headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`} -// }).then(dbRes => { +}) -// if (dbRes.status == 404) { -// rres.sendStatus(404) // Entry does not exist -// } else { -// dbRes.json().then(dbRes => { - -// fetch(`${process.env.API_DBHOST}/etyd${rreq.path.replace("/api/etyd", ""),{ -// headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`} -// }}`, { -// method: "DELETE", -// headers: { -// "If-Match": dbRes["_rev"] // Using the If-Match header is easiest for deleting entries in couchdb -// } -// }).then(fetchRes => { -// if (fetchRes.status == 200) { -// // console.log(`${rres.get("cf-connecting-ip")} DELETE ${rreq.path} returned 200 KEY: ${rreq.get("Authorization")}`) -// logRequest(rres,rreq,200) -// rres.sendStatus(200) -// } -// }).catch(fetchError => { -// // console.log(`${rres.get("cf-connecting-ip")} DELETE ${rreq.path} returned 500: ${fetchError}`) -// logRequest(rres,rreq,500,fetchError) -// rres.sendStatus(500) -// }) +app.post("/api/etyd*", (rreq,rres) => { -// }) -// } + checkTokenNew(rreq,"etyd").then(authRes => { + if (authRes.result === false) { + rres.sendStatus(401) // Token not provided or invalid for this action + } else { + if (!rreq.body["url"]) { // Assumption that if the url key isnt present in the body then the request is malformed + rres.sendStatus(400) + } else { + db`insert into etyd (url,content,owner) values (${rreq.path.replace("/api/etyd/","")},${rreq.body["url"]},${authRes.ownerId})`.then(dbRes => { + if (dbRes.count === 1) { + rres.sendStatus(201) + } else { + logRequest(rres,rreq,500,`Database insert did not return expected count but did not error out`) + rres.sendStatus(500) + } + }).catch(dbErr => { + if (dbErr.code == "23505") { // Unique constraint violation, entry already exists + rres.sendStatus(409) + } else { + logRequest(rres,rreq,500,dbErr) + rres.sendStatus(500) + } + }) + } + } + }) -// }).catch(fetchError => { -// logRequest(rres,rreq,500,fetchError) -// rres.sendStatus(500) -// }) - -// } -// }) -// } - -// }) - -// app.post("/api/etyd*", (rreq,rres) => { - -// if (rreq.get("Authorization") === undefined) { -// rres.sendStatus(400) -// } else { -// checkToken(rreq.get("Authorization"),"etyd").then(authRes => { -// if (authRes === false) { -// rres.sendStatus(401) -// } else if (authRes === true) { // Authorization successful - -// if (rreq.body["url"] == undefined) { -// rres.sendStatus(400) -// } else { -// fetch(`${process.env.API_DBHOST}/etyd${rreq.path.replace("/api/etyd", ""),{ -// headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`} -// }}`, { -// method: "PUT", -// body: JSON.stringify({ -// "content": { -// "url": rreq.body["url"] -// } -// }) -// }).then(dbRes => { - -// switch(dbRes.status) { -// case 409: -// rres.sendStatus(409) -// break; - -// case 201: -// rres.status(200).send(rreq.path.replace("/api/etyd", "")) -// break; - -// default: -// logRequest(rres,rreq,500,`CouchDB PUT did not return expected code: ${dbRes.status} ${dbRes.statusText}`) -// rres.sendStatus(500) -// break; -// } - -// }).catch(fetchError => { -// logRequest(rres,rreq,500,fetchError) -// rres.sendStatus(500) -// }) -// } - -// } -// }) -// } - -// }) +}) export {app} // export routes to be imported by index for execution \ No newline at end of file diff --git a/routes/frontpage.js b/routes/frontpage.js index a91402e..4328ba5 100644 --- a/routes/frontpage.js +++ b/routes/frontpage.js @@ -7,22 +7,26 @@ import { marked } from "marked" var timeSinceLastQuery = Date.now()-10000 var cachedResult = "" -app.get("/", (rreq, rres) => { +app.get("/indexbeta", (rreq, rres) => { if (Date.now() < timeSinceLastQuery+10000) { rres.send(cachedResult) } else { - let indexFile = fs.readFileSync(process.cwd()+"/website/templates/indextemplate.html","utf-8") + let indexFile = fs.readFileSync(process.cwd()+"/website/templates/newindextemplate.html","utf-8") cachedResult = indexFile.replace("",parseFiles()).replace("",`API Version ${globalVersion}`) rres.send(cachedResult) } }) +app.get("/", (rreq, rres) => { + rres.sendFile(process.cwd()+"/website/templates/construction.html") +}) + app.get("/static/*", (rreq,rres) => { rres.sendFile(process.cwd()+"/website/static/"+rreq.url.replace("/static/","")) }) app.get("/favicon.ico", (rreq,rres) => { - rres.sendFile(process.cwd()+"/website/static/bs.ico") + rres.sendFile(process.cwd()+"/website/static/snow-leopard.ico") }) app.get("/posts/*", (rreq,rres) => { @@ -39,10 +43,6 @@ app.get("/posts/*", (rreq,rres) => { }) -app.get("/urltoolbox", (rreq,rres) => { - rres.send("Under construction") -}) - function parseFiles() { let files = fs.readdirSync(process.cwd()+"/website/posts") let result = "" diff --git a/routes/helpdesk.js b/routes/helpdesk.js new file mode 100644 index 0000000..d456bb5 --- /dev/null +++ b/routes/helpdesk.js @@ -0,0 +1,19 @@ +import { app } from "../index.js" + +app.get("/helpdesk", (rreq, rres) => { + rres.sendFile(process.cwd()+"/website/helpdesk/templates/landing.html") +}) + +app.get("/helpdesk/articles/*", (rreq, rres) => { + rres.sendFile(process.cwd()+"/website/helpdesk/kbas/"+rreq.url.replace("/helpdesk/articles/","")) +}) + +app.get("/helpdesk/ticket/new", (rreq,rres) => { + rres.sendFile(process.cwd()+"/website/helpdesk/templates/newticket.html") +}) + +app.get("/api/helpdesk/forms/*", (rreq, rres) => { + rres.sendFile(process.cwd()+"/website/helpdesk/forms/"+rreq.url.replace("/api/helpdesk/forms/","")) +}) + +export { app } \ No newline at end of file diff --git a/website/helpdesk/forms/ecls_deleteaccount.json b/website/helpdesk/forms/ecls_deleteaccount.json new file mode 100644 index 0000000..255b9fc --- /dev/null +++ b/website/helpdesk/forms/ecls_deleteaccount.json @@ -0,0 +1,14 @@ +{ + "title": "ECLS: Delete Account", + "form": { + "description": { + "type": "span", + "content": "" + }, + "confirmation": { + "type": "checkbox", + "content": "I have read all of the above and understand this action is irreverisble", + "required": true + } + } +} \ No newline at end of file diff --git a/website/helpdesk/forms/ecls_passwordreset.json b/website/helpdesk/forms/ecls_passwordreset.json new file mode 100644 index 0000000..b894c8d --- /dev/null +++ b/website/helpdesk/forms/ecls_passwordreset.json @@ -0,0 +1,12 @@ +{ + "form": { + "description": { + "type": "span", + "content": "An Email will be sent to the address on your ECLS account with a link to reset your password.

If you cannot access that Email address, or you are locked out of your ECLS account for another reason (e.g. Lost 2FA method), please change the ticket type to 'ECLS: Account Recovery' instead. " + }, + "username": { + "type": "text", + "content": "What is your ECLS username?" + } + } +} \ No newline at end of file diff --git a/website/helpdesk/forms/ecls_personalinfochange.json b/website/helpdesk/forms/ecls_personalinfochange.json new file mode 100644 index 0000000..8cffd59 --- /dev/null +++ b/website/helpdesk/forms/ecls_personalinfochange.json @@ -0,0 +1,21 @@ +{ + "anonymousSubmission": false, + "form": { + "description": { + "type": "span", + "content": "Please enter the information you wish to change. You do not need to enter existing information if it will remain the same.

Important! If you request a username or Email address change, your access to services such as EOES or Jellyfin may be disrupted. You will be contacted in advance if this applies to you." + }, + "newusername": { + "type": "text", + "content": "New Username" + }, + "newdisplayname": { + "type": "text", + "content": "New Display Name" + }, + "newemail": { + "type": "text", + "content": "New Email Address" + } + } +} \ No newline at end of file diff --git a/website/helpdesk/forms/enstrayedcloud_quotachange.json b/website/helpdesk/forms/enstrayedcloud_quotachange.json new file mode 100644 index 0000000..7e03fc1 --- /dev/null +++ b/website/helpdesk/forms/enstrayedcloud_quotachange.json @@ -0,0 +1,14 @@ +{ + "anonymousSubmission": false, + "form": { + "description": { + "type": "span", + "content": "Important: " + }, + "newquota": { + "type": "text", + "content": "New quota (in GB)", + "required": true + } + } +} \ No newline at end of file diff --git a/website/helpdesk/forms/general_inquiry.json b/website/helpdesk/forms/general_inquiry.json new file mode 100644 index 0000000..ae5d662 --- /dev/null +++ b/website/helpdesk/forms/general_inquiry.json @@ -0,0 +1,16 @@ +{ + "form": { + "name": { + "type": "text", + "content": "What is your name?" + }, + "email": { + "type": "text", + "content": "What is a good email address to reach you at?" + }, + "message": { + "type": "bigtext", + "content": "Please enter your message below." + } + } +} \ No newline at end of file diff --git a/website/helpdesk/forms/legalrequest.json b/website/helpdesk/forms/legalrequest.json new file mode 100644 index 0000000..cd405c0 --- /dev/null +++ b/website/helpdesk/forms/legalrequest.json @@ -0,0 +1,5 @@ +{ + "form": { + + } +} \ No newline at end of file diff --git a/website/helpdesk/forms/manifest.json b/website/helpdesk/forms/manifest.json new file mode 100644 index 0000000..9807937 --- /dev/null +++ b/website/helpdesk/forms/manifest.json @@ -0,0 +1,10 @@ +{ + "general_inquiry": "General Inquiry", + "legalrequest": "Legal Request", + "responsibledisclosure": "Responsible Disclosure", + "general_techsupport": "General Technical Support", + "ecls_deleteaccount": "ECLS: Delete Account", + "ecls_passwordreset": "ECLS: Password Reset", + "ecls_personalinfochange": "ECLS: Personal Information Change", + "enstrayedcloud_quotachange": "Enstrayed Cloud: Quota Change" +} \ No newline at end of file diff --git a/website/helpdesk/kbas/example.html b/website/helpdesk/kbas/example.html new file mode 100644 index 0000000..cb8bc22 --- /dev/null +++ b/website/helpdesk/kbas/example.html @@ -0,0 +1,11 @@ + + + + + + Document + + +

example article

+ + \ No newline at end of file diff --git a/website/helpdesk/templates/landing.html b/website/helpdesk/templates/landing.html new file mode 100644 index 0000000..16ba4b7 --- /dev/null +++ b/website/helpdesk/templates/landing.html @@ -0,0 +1,74 @@ + + + + + + Enstrayed Helpdesk + + + + + +
+

Enstrayed Helpdesk

+ Open Ticket + Knowledgebase + +
+ +
+
+
+
+
+

How can I help you?

+
+
+ I'm filing a legal request, such as a DMCA takedown or other legal matter
+ Click here to start a Legal Request +
+
+ I'm responsibly disclosing a problem, such a security vulnerability
+ Click here to start a Responsible Disclosure +
+
+ I need help logging into my ECLS account
+ Click here to reset your ECLS password
+ Click here to start ECLS account recovery +
+
+ I have another question or need to get in contact
+ Click here to start a General Inquiry +
+
+
+
+

Popular Articles

+
+
+ How do I use Security Keys in ECLS?
+ Important ECLS FIDO2/Webauthn/Passkey Information +
+
+ How do I login to Jellyfin?
+ Logging into Jellyfin +
+
+ How do I change my ECLS password?
+ Change ECLS Password +
+
+ How do I use federation features in Enstrayed Cloud?
+ Enstrayed Cloud Federation Features +
+
+
+
+
+ +

Warning

+

This is a warning

+ +
+ + \ No newline at end of file diff --git a/website/helpdesk/templates/newticket.html b/website/helpdesk/templates/newticket.html new file mode 100644 index 0000000..d47a2e4 --- /dev/null +++ b/website/helpdesk/templates/newticket.html @@ -0,0 +1,56 @@ + + + + + + Enstrayed Helpdesk: New Ticket + + + + + + +
+

Enstrayed Helpdesk

+ Main Page + Knowledgebase + +
+ +
+
+
+
+

New Ticket

+ + +
+
+
+ +

Warning

+

This is a warning

+ +
+ + \ No newline at end of file diff --git a/website/posts/20240409-API-Documentation.html b/website/posts/20240409-API-Documentation.html index c030ee6..cee6fdf 100644 --- a/website/posts/20240409-API-Documentation.html +++ b/website/posts/20240409-API-Documentation.html @@ -2,7 +2,7 @@ - + @@ -16,27 +16,47 @@ Return to enstrayed.com

API Documentation

-

This page was last updated 2024-08-20.

+

This page was last updated 2025-04-26.

Source Code & Issue Tracker: github.com/enstrayed/enstrayedapi


+
+

Important Note

+
+ +

Multiple API endpoints are being rewritten/added, especially relating to authentication & authorization, as part of a database change to Postgres. These changes are being made in the new-db branch.

+

/api/nowplaying

nowplaying.js - GET +
+ GET +

Returns whatever I'm listening to via the Last.fm API in JSON. If ?format=html is used in the URL it will return the same but in HTML. If nothing is playing the JSON response will just have "playing": false.

+
+

/api/nowplayingbeta

+ nowplaying.js +
+ GET +
+
+ +

Beta verison of the /nowplaying endpoint. This version will change frequently but presently queries my Jellyfin for what I'm listening to and will return that as JSON. If ?format=html is appended to the URL it will return the same but in HTML. Each line in the HTML response has a class nowPlayingLine[1-4] that can be used to style the text using CSS. See libnowplaying.js:63 for the format of the result.

+

/api/etyd/*

etyd.js - GET - POST - DELETE +
+ GET + POST + DELETE +

Retrieves, creates or deletes entries for the etyd.cc URL shortener. Replace * in the URL for the target of the request.

@@ -53,7 +73,9 @@

/api/sendemail

mailjet.js - POST +
+ POST +

Sends an email to the specified recipient, intended for application & automation use.

@@ -72,7 +94,7 @@

/api/ip

ip.js - GET +
GET

Returns the IP, country and Cloudflare ray of the request in JSON.

@@ -80,9 +102,44 @@

/api/headers

ip.js - GET +
GET

Returns all request headers in JSON.

+ +
+

/api/auth/whoami

+ auth.js:6 +
GET
+
+

Returns JSON with the username of the token owner as well as what scopes the token has access to.

+ +
+ + auth.js:23 +
GET
+
+

Redirects the user to ECLS to login. The state parameter can be used to specify how the login flow will behave. The accepted "states" are:

+ + +
+

/api/auth/logout

+ auth.js:40 +
GET
+
+

Invalidates the token used to access the endpoint.

+ +
+

/api/auth/callback

+ auth.js:57 +
GET
+
+

Internal Use Only. This is the endpoint used by ECLS to finish the login flow. It will write the newly created token to the APIToken cookie as well as performing the action set by state, see login endpoint.

+ + \ No newline at end of file diff --git a/website/static/bs.ico b/website/static/bs.ico deleted file mode 100644 index bfb61b8..0000000 Binary files a/website/static/bs.ico and /dev/null differ diff --git a/website/static/construction.svg b/website/static/construction.svg new file mode 100644 index 0000000..476d46b --- /dev/null +++ b/website/static/construction.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/static/helpdesk/helpdesk.css b/website/static/helpdesk/helpdesk.css new file mode 100644 index 0000000..084a94a --- /dev/null +++ b/website/static/helpdesk/helpdesk.css @@ -0,0 +1,83 @@ +html { + font-family: 'Segoe UI Variable', sans-serif; + + background-color: #282828; +} + +body { + margin: 0; + display: grid; + grid-template-rows: auto 1fr; + grid-template-columns: 1fr; + height: 100vh; +} + +.headerbar { + background-color: #fff; + padding: 0 2em 0 2em; + display: flex; + flex-direction: row; + gap: 2em; + align-items: center; +} + + +.headerbarright { + margin-left: auto; +} + +.maincontent { + display: grid; + place-items: center; +} + +.cardrow { + display: flex; + flex-direction: row; + gap: 2em; +} + +.cardrow > div, .newticketmaincontent { + background-color: #fff; + padding: 2em; +} + +.newticketmaincontent { + min-width: 80ch; + max-width: 80ch; +} + +.cardrow > div > h1 { + margin: 0 0 0.5em 0; +} + +.linkColumn { + display: flex; + flex-direction: column; + gap: 0.5em; +} + +a, a:link { + color: #366FFF; +} + +a > img { + margin-right: 0.2em; +} + +dialog { + padding: 2em; + border: none; +} + +dialog > h2, .newticketmaincontent > h2 { + margin: 0 0 0.5em 0; +} + +dialog > button { + float: right; +} + +dialog::backdrop { + background-color: rgba(0, 0, 0, 0.5); +} \ No newline at end of file diff --git a/website/static/helpdesk/login.js b/website/static/helpdesk/login.js new file mode 100644 index 0000000..71d43a2 --- /dev/null +++ b/website/static/helpdesk/login.js @@ -0,0 +1,54 @@ +var globalLoggedIn = false + +function useGlobalDialog(title,body) { + document.getElementById("globalDialogHeader").innerText = title + document.getElementById("globalDialogText").innerText = body + document.getElementById("globalDialog").showModal() +} + +document.addEventListener("DOMContentLoaded", function () { + fetch(`/api/auth/whoami`).then(fetchRes => { + fetchRes.json().then(jsonRes => { + if (jsonRes.loggedIn) { + globalLoggedIn = true + document.getElementById("loginButton").innerText = `Logout ${jsonRes.username}` + } else { + globalLoggedIn = false + document.getElementById("loginButton").innerText = `Login` + } + }) + }) +}) + +function loginFunction() { + if (globalLoggedIn === true) { + fetch(`/api/auth/logout`).then(fetchRes => { + if (fetchRes.status === 200) { + globalLoggedIn = false + document.getElementById("loginButton").innerText = `Login` + } else { + fetchRes.text().then(textRes => { + useGlobalDialog("Error", `An error occurred during logout: ${textRes}`) + }) + } + }) + } else { + let loginWindow = window.open(`/api/auth/login?state=close`, `_blank`) + let loginWatcher = setInterval(() => { + if (loginWindow.closed) { + fetch(`/api/auth/whoami`).then(fetchRes => { + fetchRes.json().then(jsonRes => { + if (jsonRes.loggedIn) { + globalLoggedIn = true + document.getElementById("loginButton").innerText = `Logout ${jsonRes.username}` + } else { + useGlobalDialog("Error", `You are not logged in. Please try logging in again.`) + } + clearInterval(loginWatcher); + }) + }) + + } + }, 500); + } +} \ No newline at end of file diff --git a/website/static/icons/arrow-right.svg b/website/static/icons/arrow-right.svg new file mode 100644 index 0000000..1094594 --- /dev/null +++ b/website/static/icons/arrow-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/website/static/icons/external.svg b/website/static/icons/external.svg new file mode 100644 index 0000000..afc5079 --- /dev/null +++ b/website/static/icons/external.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/website/static/icons/github.svg b/website/static/icons/github.svg new file mode 100644 index 0000000..90e3f81 --- /dev/null +++ b/website/static/icons/github.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/website/static/icons/link.svg b/website/static/icons/link.svg new file mode 100644 index 0000000..ff2f0dc --- /dev/null +++ b/website/static/icons/link.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/website/static/icons/post.svg b/website/static/icons/post.svg new file mode 100644 index 0000000..e96c37b --- /dev/null +++ b/website/static/icons/post.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/website/static/icons/twitter.svg b/website/static/icons/twitter.svg new file mode 100644 index 0000000..e08ec79 --- /dev/null +++ b/website/static/icons/twitter.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/website/static/newindex.css b/website/static/newindex.css new file mode 100644 index 0000000..ca7755d --- /dev/null +++ b/website/static/newindex.css @@ -0,0 +1,89 @@ +body { + margin: 0; + font-family: 'Segoe UI Variable', sans-serif; + + background-color: #282828; + + + display: grid; + place-items: center; + + +} + +.mainContent { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 2.5em; + + padding: 2em; + background-color: #fdfdfd; + box-shadow: 0 0 1em 0 #202020; + + position: relative; + margin-top: 96px; +} + +.linkColumn { + display: flex; + flex-direction: column; + gap: 0.5em; +} + +#nowplaying { + display: flex; + flex-direction: row; + gap: 1em; + flex-wrap: wrap; +} + +#nowplaying > img { + height: 10em; +} + +#nowplaying > div { + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.2em; + max-width: 14em; +} + +#nowplaying .nowPlayingLine2 { + font-size: 1.25em; +} + +.apiVersion { + margin: 0.8em 0 0.8em; +} + +.marginBottom1em { + margin: 0 0 1em; +} + +.blogPostsList > ul { + list-style-image: url('/static/icons/post.svg'); + list-style-position: inside; + padding: 0; + margin: 0; + gap: 0.5em; +} + +a, a:link { + color: #366FFF; +} + +a:hover { + color: #ff8266; +} + +a > img { + margin-right: 0.2em; +} + +#miauMiau { + position: absolute; + top: -90px; + right: 9%; +} \ No newline at end of file diff --git a/website/static/newposts.css b/website/static/newposts.css new file mode 100644 index 0000000..b7f53c9 --- /dev/null +++ b/website/static/newposts.css @@ -0,0 +1,43 @@ +html { + display: grid; + place-items: center; + background-color: #282828; +} + +body { + margin: 2em 0 0 0; + font-family: 'Segoe UI Variable', sans-serif; + + background-color: #202020; + color: #F1F1F1; + + max-width: 80ch; + padding: 2em; + box-shadow: 0 0 1em 0 #202020; +} + +a, a:link { + color: #FF5A36; +} + +a:hover { + color: #ff8266; +} + +.inlineheader { + display: flex; + align-items: center; + margin-top: 2rem; + gap: 1em; + flex-wrap: wrap; +} + +.inlineheader h2 { + margin: 0; +} + +.inlineheader > div > span { + padding: 0.2rem; + color: white; + background-color: #f06445; +} \ No newline at end of file diff --git a/website/static/orange-globe.svg b/website/static/orange-globe.svg new file mode 100644 index 0000000..67bdf05 --- /dev/null +++ b/website/static/orange-globe.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/website/static/pages/tokenman.html b/website/static/pages/tokenman.html new file mode 100644 index 0000000..b2b8879 --- /dev/null +++ b/website/static/pages/tokenman.html @@ -0,0 +1,60 @@ + + + + + + TokenMan + + + + +

TokenMan

+
+ + + Not Logged In +
+ + \ No newline at end of file diff --git a/website/static/posts.css b/website/static/posts.css index 3df6078..656b127 100644 --- a/website/static/posts.css +++ b/website/static/posts.css @@ -50,9 +50,10 @@ h2 { align-items: center; margin-top: 2rem; gap: 1em; + flex-wrap: wrap; } -.inlineheader > span { +.inlineheader > div > span { padding: 0.2rem; color: white; background-color: #f06445; diff --git a/website/static/snow-leopard.ico b/website/static/snow-leopard.ico new file mode 100644 index 0000000..2320ba5 Binary files /dev/null and b/website/static/snow-leopard.ico differ diff --git a/website/static/snow-leopard.png b/website/static/snow-leopard.png new file mode 100644 index 0000000..0795188 Binary files /dev/null and b/website/static/snow-leopard.png differ diff --git a/website/static/snow-leopard.svg b/website/static/snow-leopard.svg new file mode 100644 index 0000000..344d548 --- /dev/null +++ b/website/static/snow-leopard.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/templates/construction.html b/website/templates/construction.html new file mode 100644 index 0000000..658f7c2 --- /dev/null +++ b/website/templates/construction.html @@ -0,0 +1,42 @@ + + + + + + Under Construction + + + +
+ Construction Icon +

Under Construction

+

+ This page is undergoing a revamp and will be available again soon.
+ All API endpoints are still available. Documentation is available here. +

+
+ + \ No newline at end of file diff --git a/website/templates/markdownposttemplate.html b/website/templates/markdownposttemplate.html index cbcf96e..a34529f 100644 --- a/website/templates/markdownposttemplate.html +++ b/website/templates/markdownposttemplate.html @@ -2,16 +2,18 @@ - + - + <!--SSR_REPLACE_TITLE--> + + diff --git a/website/templates/newindextemplate.html b/website/templates/newindextemplate.html new file mode 100644 index 0000000..eeea358 --- /dev/null +++ b/website/templates/newindextemplate.html @@ -0,0 +1,79 @@ + + + + + + Enstrayed.com + + + + + + + + + + + + + + + +
+ Snow Leopard Icon +
+ + +

Welcome to Enstrayed.com

+
+
+ I received an Email from @enstrayed.com
+ Click here for information about EOES +
+
+ I need help with an Enstrayed.com web service
+ Click here to visit the Helpdesk +
+
+ URL Toolbox +
+
+ API Documentation +
+
+ Downloads +
+
+ +
+
+
+
+ +

@Enstrayed

+
+ Twitter + GitHub +
+ +
+

Notes

+
    + +
+
+
+
+ +
+ + \ No newline at end of file