Compare commits
19 Commits
new-websit
...
new-db
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb289889d2 | ||
|
|
b4ecb63e43 | ||
|
|
0ef0b82765 | ||
|
|
1c3427e63f | ||
|
|
3461e9f618 | ||
|
|
d3f0b29094 | ||
|
|
4be52c7f26 | ||
|
|
080f58baa0 | ||
|
|
a37c6033df | ||
|
|
bd26c63ac7 | ||
|
|
2ad8d04c99 | ||
|
|
c868c9bc09 | ||
|
|
d07d75c994 | ||
|
|
74cf834699 | ||
|
|
9f96d82e53 | ||
|
|
b917800eec | ||
|
|
10be48e848 | ||
|
|
4c0f140e9e | ||
|
|
8a2bd6ecc3 |
75
README.md
@@ -1,65 +1,22 @@
|
|||||||
# Enstrayed API
|
## Note for Visitors
|
||||||
This repository contains the code for my personal web API written in JavaScript using the Express framework.
|
* 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).
|
||||||
## Documentation
|
* Security problem? [Open a ticket here](https://helpdesk.enstrayed.com/open.php) with the topic set as 'Responsible Disclosure'.
|
||||||
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).
|
* This code is unlicensed but I don't really care if you use parts of it (I don't know why you would though).
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
## Configuration
|
## 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:
|
TODO: Rewrite
|
||||||
| 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` |
|
|
||||||
|
|
||||||
<details> <summary>Configuration Example</summary>
|
<details> <summary>Configuration Template</summary>
|
||||||
|
|
||||||
* `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.
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
|
||||||
"frontpage": {
|
|
||||||
"directory": ""
|
|
||||||
},
|
|
||||||
|
|
||||||
"mailjet": {
|
|
||||||
"apiKey": "",
|
|
||||||
"senderAddress": ""
|
|
||||||
},
|
|
||||||
|
|
||||||
"nowplaying": {
|
|
||||||
"lastfm": {
|
|
||||||
"apiKey": "",
|
|
||||||
"target": ""
|
|
||||||
},
|
|
||||||
"jellyfin": {
|
|
||||||
"apiKey": "",
|
|
||||||
"host": "",
|
|
||||||
"target": ""
|
|
||||||
},
|
|
||||||
"cider": {
|
|
||||||
"apiKeys": [],
|
|
||||||
"hosts": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Docker
|
## 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:
|
TODO: Rewrite & add Komodo TOML files
|
||||||
|
|
||||||
> [!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`.
|
|
||||||
|
|
||||||
```dockerfile
|
```dockerfile
|
||||||
FROM node:22
|
FROM node:22
|
||||||
@@ -75,19 +32,7 @@ ENTRYPOINT [ "node", "index.js" ]
|
|||||||
<details> <summary>Docker Compose File</summary>
|
<details> <summary>Docker Compose File</summary>
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
|
||||||
services:
|
|
||||||
enstrayedapi:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
image: enstrayedapi
|
|
||||||
container_name: enstrayedapi
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- ./config.json:/app/config.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## 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.
|
|
||||||
25
index.js
@@ -1,31 +1,32 @@
|
|||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import { execSync } from 'child_process'
|
import { execSync } from 'child_process'
|
||||||
|
import postgres from 'postgres'
|
||||||
import express, { json } from 'express'
|
import express, { json } from 'express'
|
||||||
|
import cookieParser from 'cookie-parser'
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
if (!process.env.API_DBHOST || !process.env.API_DBCRED) {
|
if (!process.env.DATABASE_URI) {
|
||||||
console.log("FATAL: API_DBHOST and API_DBCRED must be set")
|
console.log("FATAL: DATABASE_URI must be set")
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalConfig = await fetch(`${process.env.API_DBHOST}/config/${process.env.API_DBCRED.split(":")[0]}`,{
|
const db = postgres(process.env.DATABASE_URI)
|
||||||
headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`}
|
|
||||||
}).then(response => {
|
const globalConfig = await db`select content from config where id = ${process.env.CONFIG_OVERRIDE ?? 'production'}`.then(response => {return response[0]["content"]}).catch(error => {
|
||||||
if (response.status !== 200) {
|
console.log(`FATAL: Error occured in downloading configuration: ${error}`)
|
||||||
console.log(`FATAL: Failed to download configuration: ${response.status} ${response.statusText}`)
|
process.exit(1)
|
||||||
process.exit(1)
|
|
||||||
} else {
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const globalVersion = execSync(`git show --oneline -s`).toString().split(" ")[0]
|
const globalVersion = execSync(`git show --oneline -s`).toString().split(" ")[0]
|
||||||
// Returns ISO 8601 Date & 24hr time for UTC-7/PDT
|
// Returns ISO 8601 Date & 24hr time for UTC-7/PDT
|
||||||
const startTime = new Date(new Date().getTime() - 25200000).toISOString().slice(0,19).replace('T',' ')
|
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
|
app.use(json()) // Allows receiving JSON bodies
|
||||||
// see important note: https://expressjs.com/en/api.html#express.json
|
// see important note: https://expressjs.com/en/api.html#express.json
|
||||||
|
app.use(cookieParser()) // Allows receiving cookies
|
||||||
|
|
||||||
process.on('SIGTERM', function() {
|
process.on('SIGTERM', function() {
|
||||||
console.log("Received SIGTERM, exiting...")
|
console.log("Received SIGTERM, exiting...")
|
||||||
|
|||||||
@@ -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} token Token as received by client
|
||||||
* @param {string} scope Scope the token will need to have in order to succeed
|
* @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) {
|
async function checkToken(token,scope) {
|
||||||
return await fetch(`${process.env.API_DBHOST}/auth/sessions`, {
|
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 => {
|
||||||
headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`}
|
if (response.length === 0) {
|
||||||
}).then(fetchRes => {
|
return false
|
||||||
|
} else if (response[0]?.scopes.split(",").includes(scope)) {
|
||||||
return fetchRes.json().then(dbRes => {
|
return true
|
||||||
|
} else {
|
||||||
if (dbRes.sessions[token] == undefined) { // If the token is not on the sessions list then reject
|
return false
|
||||||
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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export {checkToken}
|
/**
|
||||||
|
* 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}
|
||||||
@@ -4,9 +4,10 @@
|
|||||||
* @param {object} request Parent request object
|
* @param {object} request Parent request object
|
||||||
* @param {number} code Status code to log, should be same as sent to client
|
* @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 {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) {
|
function logRequest(response,request,code,extra,authresponse) {
|
||||||
console.log(`${request.get("cf-connecting-ip") ?? request.ip} ${request.get("Authorization") ?? ""} ${request.method} ${request.path} returned ${code} ${extra ?? ""}`)
|
console.log(`${request.get("cf-connecting-ip") ?? request.ip} ${authresponse?.owner ?? ""}/${request.get("Authorization") ?? ""} ${request.method} ${request.path} returned ${code} ${extra ?? ""}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { logRequest }
|
export { logRequest }
|
||||||
55
liberals/misc.js
Normal file
@@ -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 }
|
||||||
45
package-lock.json
generated
@@ -9,9 +9,11 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"marked": "^14.1.3",
|
"marked": "^14.1.3",
|
||||||
"nodemailer": "^6.9.15"
|
"nodemailer": "^6.9.15",
|
||||||
|
"postgres": "^3.4.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.0.12",
|
"@types/bun": "^1.0.12",
|
||||||
@@ -157,6 +159,28 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/cookie-signature": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
@@ -470,9 +494,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "14.1.3",
|
"version": "14.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz",
|
||||||
"integrity": "sha512-ZibJqTULGlt9g5k4VMARAktMAjXoVnnr+Y3aCqW1oDftcV4BA3UmrBifzXoZyenHRk75csiPu9iwsTj4VNBT0g==",
|
"integrity": "sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
@@ -600,6 +624,19 @@
|
|||||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"marked": "^14.1.3",
|
"marked": "^14.1.3",
|
||||||
"nodemailer": "^6.9.15"
|
"nodemailer": "^6.9.15",
|
||||||
|
"postgres": "^3.4.5"
|
||||||
},
|
},
|
||||||
"name": "enstrayedapi",
|
"name": "enstrayedapi",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|||||||
128
routes/auth.js
Normal file
@@ -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 <code>${newToken}</code>`)
|
||||||
|
} else if (rreq.query.state === "close") {
|
||||||
|
rres.setHeader("Set-Cookie", `APIToken=${newToken}; Domain=${rreq.hostname}; Expires=${new Date(newExpiration).toUTCString()}; Path=/`).send(`<script>document.addEventListener("DOMContentLoaded", () => {window.close();});</script> 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.<br><br><code>500 ${code}</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 }
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { app, globalConfig } from "../index.js" // Get globals from index
|
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 { logRequest } from "../liberals/logging.js"
|
||||||
import * as nodemailer from 'nodemailer'
|
import * as nodemailer from 'nodemailer'
|
||||||
|
|
||||||
@@ -14,10 +14,10 @@ const transporter = nodemailer.createTransport({
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.post("/api/sendemail", (rreq,rres) => {
|
app.post("/api/sendemail", (rreq,rres) => {
|
||||||
checkToken(rreq.get("Authorization"),"email").then(authRes => {
|
checkTokenNew(rreq,"email").then(authRes => {
|
||||||
if (authRes === false) {
|
if (authRes.result === false) {
|
||||||
rres.sendStatus(401)
|
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
|
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)
|
rres.sendStatus(400)
|
||||||
} else {
|
} else {
|
||||||
@@ -29,14 +29,14 @@ app.post("/api/sendemail", (rreq,rres) => {
|
|||||||
text: rreq.body.message ?? "Message Not Set"
|
text: rreq.body.message ?? "Message Not Set"
|
||||||
}).then(transportResponse => {
|
}).then(transportResponse => {
|
||||||
if (transportResponse.response.slice(0,1) === "2") {
|
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)
|
rres.status(200).send(transportResponse.response)
|
||||||
} else {
|
} else {
|
||||||
logRequest(rres,rreq,400,transportResponse.response)
|
logRequest(rres,rreq,400,transportResponse.response,authRes)
|
||||||
rres.status(400).send(transportResponse.response)
|
rres.status(400).send(transportResponse.response)
|
||||||
}
|
}
|
||||||
}).catch(transportError => {
|
}).catch(transportError => {
|
||||||
logRequest(rres,rreq,500,transportError)
|
logRequest(rres,rreq,500,transportError,authRes)
|
||||||
rres.sendStatus(500)
|
rres.sendStatus(500)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
173
routes/etyd.js
@@ -1,130 +1,73 @@
|
|||||||
import { app, globalConfig } from "../index.js" // Get globals from index
|
import { app, db, globalConfig } from "../index.js" // Get globals from index
|
||||||
import { checkToken } from "../liberals/auth.js"
|
import { checkToken, checkTokenNew } from "../liberals/auth.js"
|
||||||
import { logRequest } from "../liberals/logging.js"
|
import { logRequest } from "../liberals/logging.js"
|
||||||
|
|
||||||
app.get("/api/etyd*", (rreq,rres) => {
|
app.get("/api/etyd*", (rreq,rres) => {
|
||||||
fetch(`${process.env.API_DBHOST}/etyd${rreq.path.replace("/api/etyd","")}`,{
|
let userRequest = rreq.path.replace("/api/etyd/","")
|
||||||
headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`}
|
db`select content from etyd where url = ${userRequest}`.then(response => {
|
||||||
}).then(dbRes => {
|
if (response.length == 0) {
|
||||||
if (dbRes.status == 404) {
|
rres.status(404).send(`etyd.cc: URL "${userRequest}" was not found`)
|
||||||
rres.sendStatus(404)
|
|
||||||
} else {
|
} else {
|
||||||
dbRes.json().then(dbRes => {
|
rres.redirect(response[0].content)
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}).catch(fetchError => {
|
}).catch(dbError => {
|
||||||
logRequest(rres,rreq,500,fetchError)
|
logRequest(rres,rreq,500,dbError)
|
||||||
rres.sendStatus(500)
|
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) {
|
checkTokenNew(rreq,"etyd").then(authRes => {
|
||||||
// rres.sendStatus(400)
|
if (authRes.result === false) {
|
||||||
// } else {
|
rres.sendStatus(401) // Token not provided or invalid for this action
|
||||||
// checkToken(rreq.get("Authorization"),"etyd").then(authRes => {
|
} else {
|
||||||
// if (authRes === false) {
|
db`delete from etyd where url = ${rreq.path.replace("/api/etyd/","")} and owner = ${authRes.ownerId}`.then(dbRes => {
|
||||||
// rres.sendStatus(401)
|
if (dbRes.count === 1) {
|
||||||
// } else if (authRes === true) { // Authorization successful
|
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) {
|
app.post("/api/etyd*", (rreq,rres) => {
|
||||||
// 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)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// })
|
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
|
export {app} // export routes to be imported by index for execution
|
||||||
@@ -7,7 +7,7 @@ import { marked } from "marked"
|
|||||||
var timeSinceLastQuery = Date.now()-10000
|
var timeSinceLastQuery = Date.now()-10000
|
||||||
var cachedResult = ""
|
var cachedResult = ""
|
||||||
|
|
||||||
app.get("/", (rreq, rres) => {
|
app.get("/indexbeta", (rreq, rres) => {
|
||||||
if (Date.now() < timeSinceLastQuery+10000) {
|
if (Date.now() < timeSinceLastQuery+10000) {
|
||||||
rres.send(cachedResult)
|
rres.send(cachedResult)
|
||||||
} else {
|
} else {
|
||||||
@@ -17,6 +17,10 @@ app.get("/", (rreq, rres) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.get("/", (rreq, rres) => {
|
||||||
|
rres.sendFile(process.cwd()+"/website/templates/construction.html")
|
||||||
|
})
|
||||||
|
|
||||||
app.get("/static/*", (rreq,rres) => {
|
app.get("/static/*", (rreq,rres) => {
|
||||||
rres.sendFile(process.cwd()+"/website/static/"+rreq.url.replace("/static/",""))
|
rres.sendFile(process.cwd()+"/website/static/"+rreq.url.replace("/static/",""))
|
||||||
})
|
})
|
||||||
@@ -39,10 +43,6 @@ app.get("/posts/*", (rreq,rres) => {
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get("/urltoolbox", (rreq,rres) => {
|
|
||||||
rres.send("Under construction")
|
|
||||||
})
|
|
||||||
|
|
||||||
function parseFiles() {
|
function parseFiles() {
|
||||||
let files = fs.readdirSync(process.cwd()+"/website/posts")
|
let files = fs.readdirSync(process.cwd()+"/website/posts")
|
||||||
let result = ""
|
let result = ""
|
||||||
|
|||||||
19
routes/helpdesk.js
Normal file
@@ -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 }
|
||||||
14
website/helpdesk/forms/ecls_deleteaccount.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
website/helpdesk/forms/ecls_passwordreset.json
Normal file
@@ -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. <br><br> <b>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. </b>"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "text",
|
||||||
|
"content": "What is your ECLS username?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
website/helpdesk/forms/ecls_personalinfochange.json
Normal file
@@ -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. <br><br> <b>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.</b>"
|
||||||
|
},
|
||||||
|
"newusername": {
|
||||||
|
"type": "text",
|
||||||
|
"content": "New Username"
|
||||||
|
},
|
||||||
|
"newdisplayname": {
|
||||||
|
"type": "text",
|
||||||
|
"content": "New Display Name"
|
||||||
|
},
|
||||||
|
"newemail": {
|
||||||
|
"type": "text",
|
||||||
|
"content": "New Email Address"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
website/helpdesk/forms/enstrayedcloud_quotachange.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"anonymousSubmission": false,
|
||||||
|
"form": {
|
||||||
|
"description": {
|
||||||
|
"type": "span",
|
||||||
|
"content": "Important: <ul> <li>Your Enstrayed Cloud quota does NOT affect your EOES mailbox quota.</li> <li>Your request will be reviewed and may be denied for any reason.</li> </ul>"
|
||||||
|
},
|
||||||
|
"newquota": {
|
||||||
|
"type": "text",
|
||||||
|
"content": "New quota (in GB)",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
website/helpdesk/forms/general_inquiry.json
Normal file
@@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
website/helpdesk/forms/legalrequest.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"form": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
10
website/helpdesk/forms/manifest.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
11
website/helpdesk/kbas/example.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>example article</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
74
website/helpdesk/templates/landing.html
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Enstrayed Helpdesk</title>
|
||||||
|
<link rel="stylesheet" href="/static/helpdesk/helpdesk.css">
|
||||||
|
<script src="/static/helpdesk/login.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="headerbar">
|
||||||
|
<h1>Enstrayed Helpdesk</h1>
|
||||||
|
<a href="/helpdesk/ticket/new">Open Ticket</a>
|
||||||
|
<a href="/helpdesk/articles">Knowledgebase</a>
|
||||||
|
|
||||||
|
<div class="headerbarright">
|
||||||
|
<button id="loginButton" onclick="loginFunction()">Login</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="maincontent">
|
||||||
|
<div class="cardrow">
|
||||||
|
<div>
|
||||||
|
<h1>How can I help you?</h1>
|
||||||
|
<div class="linkColumn">
|
||||||
|
<div>
|
||||||
|
<span>I'm filing a legal request, such as a DMCA takedown or other legal matter</span><br>
|
||||||
|
<a href=""><img src="/static/icons/arrow-right.svg" alt="">Click here to start a Legal Request</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>I'm responsibly disclosing a problem, such a security vulnerability</span><br>
|
||||||
|
<a href=""><img src="/static/icons/arrow-right.svg" alt="">Click here to start a Responsible Disclosure</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>I need help logging into my ECLS account</span><br>
|
||||||
|
<a href=""><img src="/static/icons/external.svg" alt="">Click here to reset your ECLS password</a><br>
|
||||||
|
<a href=""><img src="/static/icons/arrow-right.svg" alt="">Click here to start ECLS account recovery</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>I have another question or need to get in contact</span><br>
|
||||||
|
<a href=""><img src="/static/icons/arrow-right.svg" alt="">Click here to start a General Inquiry</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1>Popular Articles</h1>
|
||||||
|
<div class="linkColumn">
|
||||||
|
<div>
|
||||||
|
<span>How do I use Security Keys in ECLS?</span><br>
|
||||||
|
<a href=""><img src="/static/icons/post.svg" alt="">Important ECLS FIDO2/Webauthn/Passkey Information</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>How do I login to Jellyfin?</span><br>
|
||||||
|
<a href=""><img src="/static/icons/post.svg" alt="">Logging into Jellyfin</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>How do I change my ECLS password?</span><br>
|
||||||
|
<a href=""><img src="/static/icons/post.svg" alt="">Change ECLS Password</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>How do I use federation features in Enstrayed Cloud?</span><br>
|
||||||
|
<a href=""><img src="/static/icons/post.svg" alt="">Enstrayed Cloud Federation Features</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<dialog id="globalDialog">
|
||||||
|
<h2 id="globalDialogHeader">Warning</h2>
|
||||||
|
<p id="globalDialogText">This is a warning</p>
|
||||||
|
<button onclick="document.getElementById('globalDialog').close()">Dismiss</button>
|
||||||
|
</dialog>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
56
website/helpdesk/templates/newticket.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Enstrayed Helpdesk: New Ticket</title>
|
||||||
|
<link rel="stylesheet" href="/static/helpdesk/helpdesk.css">
|
||||||
|
<script src="/static/helpdesk/login.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
document.getElementById('formSelection').value = 'none'
|
||||||
|
document.getElementById('formSelection').addEventListener('change', function() {
|
||||||
|
useGlobalDialog(`Info`, `You selected ${this.value}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
fetch(`/api/helpdesk/forms/manifest.json`).then(fetchRes => {
|
||||||
|
fetchRes.json().then(jsonRes => {
|
||||||
|
for (let x in jsonRes) {
|
||||||
|
let newElement = document.createElement('option')
|
||||||
|
newElement.value = x
|
||||||
|
newElement.textContent = jsonRes[x]
|
||||||
|
document.getElementById('formSelection').appendChild(newElement)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="headerbar">
|
||||||
|
<h1>Enstrayed Helpdesk</h1>
|
||||||
|
<a href="/helpdesk">Main Page</a>
|
||||||
|
<a href="/helpdesk/articles">Knowledgebase</a>
|
||||||
|
|
||||||
|
<div class="headerbarright">
|
||||||
|
<button id="loginButton" onclick="loginFunction()">Login</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="maincontent">
|
||||||
|
<div class="newticketmaincontent">
|
||||||
|
<h2>New Ticket</h2>
|
||||||
|
<label for="formSelection">Please select a form: </label>
|
||||||
|
<select name="Form Selection" id="formSelection">
|
||||||
|
<option value="none" disabled selected>-- Choose From List --</option>
|
||||||
|
</select>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<dialog id="globalDialog">
|
||||||
|
<h2 id="globalDialogHeader">Warning</h2>
|
||||||
|
<p id="globalDialogText">This is a warning</p>
|
||||||
|
<button onclick="document.getElementById('globalDialog').close()">Dismiss</button>
|
||||||
|
</dialog>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -106,5 +106,40 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>Returns all request headers in JSON.</p>
|
<p>Returns all request headers in JSON.</p>
|
||||||
|
|
||||||
|
<div class="inlineheader">
|
||||||
|
<h2>/api/auth/whoami</h2>
|
||||||
|
<a href="https://github.com/Enstrayed/enstrayedapi/blob/new-db/routes/auth.js">auth.js:6</a>
|
||||||
|
<div><span>GET</span></div>
|
||||||
|
</div>
|
||||||
|
<p>Returns JSON with the username of the token owner as well as what scopes the token has access to.</p>
|
||||||
|
|
||||||
|
<div class="inlineheader">
|
||||||
|
<h2 id="jumplink_authlogin">/api/auth/login</h2>
|
||||||
|
<a href="https://github.com/Enstrayed/enstrayedapi/blob/new-db/routes/auth.js">auth.js:23</a>
|
||||||
|
<div><span>GET</span></div>
|
||||||
|
</div>
|
||||||
|
<p>Redirects the user to ECLS to login. The <code>state</code> parameter can be used to specify how the login flow will behave. The accepted "states" are:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>redirect</code> - Redirects the user to a page after logging in. This paramter requires the <code>destination</code> paramter to also be set with the URL the user will be redirected to.</li>
|
||||||
|
<li><code>display</code> - Displays the generated token to the user after login. Currently, this still writes the new token to the <code>APIToken</code> cookie, though this is planned to change.</li>
|
||||||
|
<li><code>close</code> - This will close the page after logging in. This requires the page to be opened with JavaScript otherwise it will not automatically close.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="inlineheader">
|
||||||
|
<h2>/api/auth/logout</h2>
|
||||||
|
<a href="https://github.com/Enstrayed/enstrayedapi/blob/new-db/routes/auth.js">auth.js:40</a>
|
||||||
|
<div><span>GET</span></div>
|
||||||
|
</div>
|
||||||
|
<p>Invalidates the token used to access the endpoint.</p>
|
||||||
|
|
||||||
|
<div class="inlineheader">
|
||||||
|
<h2>/api/auth/callback</h2>
|
||||||
|
<a href="https://github.com/Enstrayed/enstrayedapi/blob/new-db/routes/auth.js">auth.js:57</a>
|
||||||
|
<div><span>GET</span></div>
|
||||||
|
</div>
|
||||||
|
<p><b>Internal Use Only. </b>This is the endpoint used by ECLS to finish the login flow. It will write the newly created token to the <code>APIToken</code> cookie as well as performing the action set by <code>state</code>, see <a href="#jumplink_authlogin">login endpoint</a>.</p>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
152
website/static/construction.svg
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g filter="url(#filter0_ii_18_12057)">
|
||||||
|
<path d="M3 9C3 8.44772 3.44772 8 4 8H28C28.5523 8 29 8.44772 29 9V20C29 20.5523 28.5523 21 28 21H4C3.44772 21 3 20.5523 3 20V9Z" fill="url(#paint0_linear_18_12057)"/>
|
||||||
|
</g>
|
||||||
|
<path d="M3 9C3 8.44772 3.44772 8 4 8H28C28.5523 8 29 8.44772 29 9V20C29 20.5523 28.5523 21 28 21H4C3.44772 21 3 20.5523 3 20V9Z" fill="url(#paint1_linear_18_12057)"/>
|
||||||
|
<path d="M3 9C3 8.44772 3.44772 8 4 8H28C28.5523 8 29 8.44772 29 9V20C29 20.5523 28.5523 21 28 21H4C3.44772 21 3 20.5523 3 20V9Z" fill="url(#paint2_radial_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.4748 20.9999H19.525L29 11.5249V16.4746L24.4748 20.9999Z" fill="url(#paint3_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.4748 20.9999H19.525L29 11.5249V16.4746L24.4748 20.9999Z" fill="url(#paint4_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.4748 20.9999H19.525L29 11.5249V16.4746L24.4748 20.9999Z" fill="url(#paint5_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4748 21H9.52502L22.525 8H27.4748L14.4748 21Z" fill="url(#paint6_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4748 21H9.52502L22.525 8H27.4748L14.4748 21Z" fill="url(#paint7_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4748 21H9.52502L22.525 8H27.4748L14.4748 21Z" fill="url(#paint8_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.47476 21H4C3.44772 21 3 20.5523 3 20V17.525L12.525 8H17.4748L4.47476 21Z" fill="url(#paint9_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.47476 21H4C3.44772 21 3 20.5523 3 20V17.525L12.525 8H17.4748L4.47476 21Z" fill="url(#paint10_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.47476 21H4C3.44772 21 3 20.5523 3 20V17.525L12.525 8H17.4748L4.47476 21Z" fill="url(#paint11_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.47476 21H4C3.44772 21 3 20.5523 3 20V17.525L12.525 8H17.4748L4.47476 21Z" fill="url(#paint12_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.47476 8H4C3.44772 8 3 8.44772 3 9V12.4748L7.47476 8Z" fill="url(#paint13_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.47476 8H4C3.44772 8 3 8.44772 3 9V12.4748L7.47476 8Z" fill="url(#paint14_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.47476 8H4C3.44772 8 3 8.44772 3 9V12.4748L7.47476 8Z" fill="url(#paint15_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M26 21H24V30H26V21Z" fill="url(#paint16_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M26 21H24V30H26V21Z" fill="url(#paint17_radial_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 21H6V30H8V21Z" fill="url(#paint18_linear_18_12057)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 21H6V30H8V21Z" fill="url(#paint19_radial_18_12057)"/>
|
||||||
|
<path d="M6 7.5C6 6.67157 6.67157 6 7.5 6C8.32843 6 9 6.67157 9 7.5V8H6V7.5Z" fill="url(#paint20_linear_18_12057)"/>
|
||||||
|
<path d="M23 7.5C23 6.67157 23.6716 6 24.5 6C25.3284 6 26 6.67157 26 7.5V8H23V7.5Z" fill="url(#paint21_linear_18_12057)"/>
|
||||||
|
<g filter="url(#filter1_f_18_12057)">
|
||||||
|
<rect x="7.10938" y="21.5" width="0.47476" height="8" rx="0.23738" fill="#DFD0E9"/>
|
||||||
|
</g>
|
||||||
|
<g filter="url(#filter2_f_18_12057)">
|
||||||
|
<rect x="25" y="21.5" width="0.47476" height="8" rx="0.23738" fill="#DFD0E9"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_ii_18_12057" x="2.5" y="7.5" width="27" height="14" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dx="0.5" dy="-0.5"/>
|
||||||
|
<feGaussianBlur stdDeviation="0.375"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.831373 0 0 0 0 0.494118 0 0 0 0 0.282353 0 0 0 1 0"/>
|
||||||
|
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_18_12057"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dx="-0.5" dy="0.5"/>
|
||||||
|
<feGaussianBlur stdDeviation="0.375"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 0.415686 0 0 0 1 0"/>
|
||||||
|
<feBlend mode="normal" in2="effect1_innerShadow_18_12057" result="effect2_innerShadow_18_12057"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="filter1_f_18_12057" x="6.45938" y="20.85" width="1.77473" height="9.3" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||||
|
<feGaussianBlur stdDeviation="0.325" result="effect1_foregroundBlur_18_12057"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="filter2_f_18_12057" x="24.35" y="20.85" width="1.77473" height="9.3" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||||
|
<feGaussianBlur stdDeviation="0.325" result="effect1_foregroundBlur_18_12057"/>
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="paint0_linear_18_12057" x1="25.8457" y1="9.98514" x2="8.5" y2="18.625" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#F7D142"/>
|
||||||
|
<stop offset="1" stop-color="#FCB545"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear_18_12057" x1="3" y1="14.0109" x2="3.79" y2="14.0109" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#A57C4B"/>
|
||||||
|
<stop offset="1" stop-color="#A57C4B" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint2_radial_18_12057" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(4.68831 8) scale(12.0629 1.90436)">
|
||||||
|
<stop offset="0.135638" stop-color="#FBBC43"/>
|
||||||
|
<stop offset="1" stop-color="#FBBC43" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint3_linear_18_12057" x1="27.6071" y1="11.5249" x2="18.4096" y2="17.9823" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#353138"/>
|
||||||
|
<stop offset="1" stop-color="#503B62"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint4_linear_18_12057" x1="22.0781" y1="20.9999" x2="22.0781" y2="20.3116" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#4B2867"/>
|
||||||
|
<stop offset="1" stop-color="#4B2867" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint5_linear_18_12057" x1="29" y1="14.4905" x2="25.7434" y2="14.4905" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#353139"/>
|
||||||
|
<stop offset="0.166461" stop-color="#413D46"/>
|
||||||
|
<stop offset="0.455618" stop-color="#413D46" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint6_linear_18_12057" x1="24.836" y1="8" x2="11.4256" y2="21" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#353138"/>
|
||||||
|
<stop offset="1" stop-color="#503B62"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint7_linear_18_12057" x1="14.3616" y1="21" x2="14.3616" y2="20.0557" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#4B2867"/>
|
||||||
|
<stop offset="1" stop-color="#4B2867" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint8_linear_18_12057" x1="23.3427" y1="8" x2="23.3427" y2="12.0689" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#353139"/>
|
||||||
|
<stop offset="0.166461" stop-color="#413D46"/>
|
||||||
|
<stop offset="0.422206" stop-color="#413D46" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint9_linear_18_12057" x1="15.3468" y1="8" x2="2.32672" y2="18.1782" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#353138"/>
|
||||||
|
<stop offset="1" stop-color="#503B62"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint10_linear_18_12057" x1="6.90025" y1="21" x2="6.90025" y2="20.0557" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#4B2867"/>
|
||||||
|
<stop offset="1" stop-color="#4B2867" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint11_linear_18_12057" x1="14.1427" y1="8" x2="14.1427" y2="12.0689" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#353139"/>
|
||||||
|
<stop offset="0.166461" stop-color="#413D46"/>
|
||||||
|
<stop offset="0.422206" stop-color="#413D46" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint12_linear_18_12057" x1="3" y1="15.1145" x2="3.75" y2="15.1145" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#2F2C32"/>
|
||||||
|
<stop offset="1" stop-color="#2F2C32" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint13_linear_18_12057" x1="6.81693" y1="8" x2="2.47324" y2="11.0496" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#353138"/>
|
||||||
|
<stop offset="1" stop-color="#503B62"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint14_linear_18_12057" x1="6.44467" y1="8" x2="6.44467" y2="9.40056" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#353139"/>
|
||||||
|
<stop offset="0.166461" stop-color="#413D46"/>
|
||||||
|
<stop offset="0.422206" stop-color="#413D46" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint15_linear_18_12057" x1="3" y1="10.4489" x2="4" y2="10.4489" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#2F2C32"/>
|
||||||
|
<stop offset="1" stop-color="#2F2C32" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint16_linear_18_12057" x1="24" y1="23.9375" x2="25" y2="23.9375" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#857C8C"/>
|
||||||
|
<stop offset="1" stop-color="#B9A7C4"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint17_radial_18_12057" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(25 29.5937) rotate(-90) scale(8.79687 5.25369)">
|
||||||
|
<stop offset="0.912727" stop-color="#7E5E91" stop-opacity="0"/>
|
||||||
|
<stop offset="0.992895" stop-color="#7E5E91"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint18_linear_18_12057" x1="6" y1="23.9375" x2="7" y2="23.9375" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#857C8C"/>
|
||||||
|
<stop offset="1" stop-color="#B9A7C4"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint19_radial_18_12057" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(7 29.5937) rotate(-90) scale(8.79687 5.25369)">
|
||||||
|
<stop offset="0.912727" stop-color="#7E5E91" stop-opacity="0"/>
|
||||||
|
<stop offset="0.992895" stop-color="#7E5E91"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint20_linear_18_12057" x1="7.5" y1="6" x2="7.5" y2="8" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.5625" stop-color="#CB1E72"/>
|
||||||
|
<stop offset="1" stop-color="#E61C31"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint21_linear_18_12057" x1="24.5" y1="6" x2="24.5" y2="8" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.5625" stop-color="#CB1E72"/>
|
||||||
|
<stop offset="1" stop-color="#E61C31"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 10 KiB |
83
website/static/helpdesk/helpdesk.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
54
website/static/helpdesk/login.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
website/static/icons/arrow-right.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#366FFF" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm4.5 5.5a.5.5 0 0 0 0 1h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 400 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#ff5a36" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#366FFF" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/>
|
<path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/>
|
||||||
<path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/>
|
<path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 490 B After Width: | Height: | Size: 490 B |
@@ -1,3 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#ff5a36" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#366FFF" viewBox="0 0 16 16">
|
||||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/>
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 682 B After Width: | Height: | Size: 682 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#ff5a36" viewBox="2.5 2 11 11">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#366FFF" viewBox="2.5 2 11 11">
|
||||||
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/>
|
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/>
|
||||||
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/>
|
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 489 B After Width: | Height: | Size: 489 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#ff5a36" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#366FFF" viewBox="0 0 16 16">
|
||||||
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5"/>
|
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5"/>
|
||||||
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
|
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 431 B After Width: | Height: | Size: 431 B |
@@ -1,3 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#ff5a36" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#366FFF" viewBox="0 0 16 16">
|
||||||
<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334q.002-.211-.006-.422A6.7 6.7 0 0 0 16 3.542a6.7 6.7 0 0 1-1.889.518 3.3 3.3 0 0 0 1.447-1.817 6.5 6.5 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.32 9.32 0 0 1-6.767-3.429 3.29 3.29 0 0 0 1.018 4.382A3.3 3.3 0 0 1 .64 6.575v.045a3.29 3.29 0 0 0 2.632 3.218 3.2 3.2 0 0 1-.865.115 3 3 0 0 1-.614-.057 3.28 3.28 0 0 0 3.067 2.277A6.6 6.6 0 0 1 .78 13.58a6 6 0 0 1-.78-.045A9.34 9.34 0 0 0 5.026 15"/>
|
<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334q.002-.211-.006-.422A6.7 6.7 0 0 0 16 3.542a6.7 6.7 0 0 1-1.889.518 3.3 3.3 0 0 0 1.447-1.817 6.5 6.5 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.32 9.32 0 0 1-6.767-3.429 3.29 3.29 0 0 0 1.018 4.382A3.3 3.3 0 0 1 .64 6.575v.045a3.29 3.29 0 0 0 2.632 3.218 3.2 3.2 0 0 1-.865.115 3 3 0 0 1-.614-.057 3.28 3.28 0 0 0 3.067 2.277A6.6 6.6 0 0 1 .78 13.58a6 6 0 0 1-.78-.045A9.34 9.34 0 0 0 5.026 15"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 559 B After Width: | Height: | Size: 559 B |
@@ -1,9 +1,9 @@
|
|||||||
body {
|
body {
|
||||||
margin: 2em 0 0 0;
|
margin: 0;
|
||||||
font-family: 'Segoe UI Variable', sans-serif;
|
font-family: 'Segoe UI Variable', sans-serif;
|
||||||
|
|
||||||
background-color: #282828;
|
background-color: #282828;
|
||||||
color: #F1F1F1;
|
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@@ -18,8 +18,11 @@ body {
|
|||||||
gap: 2.5em;
|
gap: 2.5em;
|
||||||
|
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
background-color: #202020;
|
background-color: #fdfdfd;
|
||||||
box-shadow: 0 0 1em 0 #202020;
|
box-shadow: 0 0 1em 0 #202020;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
margin-top: 96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.linkColumn {
|
.linkColumn {
|
||||||
@@ -48,7 +51,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#nowplaying .nowPlayingLine2 {
|
#nowplaying .nowPlayingLine2 {
|
||||||
font-size: 1.4em;
|
font-size: 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.apiVersion {
|
.apiVersion {
|
||||||
@@ -68,7 +71,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a, a:link {
|
a, a:link {
|
||||||
color: #FF5A36;
|
color: #366FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
@@ -77,4 +80,10 @@ a:hover {
|
|||||||
|
|
||||||
a > img {
|
a > img {
|
||||||
margin-right: 0.2em;
|
margin-right: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#miauMiau {
|
||||||
|
position: absolute;
|
||||||
|
top: -90px;
|
||||||
|
right: 9%;
|
||||||
}
|
}
|
||||||
60
website/static/pages/tokenman.html
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>TokenMan</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: "Segoe UI Variable", sans-serif;
|
||||||
|
background-color: #111;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
function loginFunction() {
|
||||||
|
let loginWindow = window.open(`http://bottleneck.pizzly-catfish.ts.net:8081/api/auth/login?state=close`,`_blank`)
|
||||||
|
let loginWatcher = setInterval(() => {
|
||||||
|
if (loginWindow.closed) {
|
||||||
|
fetch(`http://bottleneck.pizzly-catfish.ts.net:8081/api/auth/whoami`).then(fetchRes => {
|
||||||
|
fetchRes.json().then(jsonRes => {
|
||||||
|
if (jsonRes.loggedIn) {
|
||||||
|
document.getElementById("loggedInText").innerText = `Logged in as ${jsonRes.username} with scopes ${jsonRes.scopes.join(", ")}`
|
||||||
|
document.getElementById("loginButton").disabled = true
|
||||||
|
document.getElementById("logoutButton").disabled = false
|
||||||
|
|
||||||
|
} else {
|
||||||
|
alert("An error occured during login.")
|
||||||
|
}
|
||||||
|
clearInterval(loginWatcher);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logoutFunction() {
|
||||||
|
fetch(`http://bottleneck.pizzly-catfish.ts.net:8081/api/auth/logout`).then(fetchRes => {
|
||||||
|
if (fetchRes.status == 200) {
|
||||||
|
document.getElementById("loggedInText").innerText = `Not Logged In`
|
||||||
|
document.getElementById("loginButton").disabled = false
|
||||||
|
document.getElementById("logoutButton").disabled = true
|
||||||
|
} else {
|
||||||
|
fetchRes.text().then(text => {
|
||||||
|
alert("An error occured during logout: " + text)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>TokenMan</h1>
|
||||||
|
<div>
|
||||||
|
<button onclick="loginFunction()" id="loginButton">Login</button>
|
||||||
|
<button onclick="logoutFunction()" id="logoutButton" disabled="true">Logout</button>
|
||||||
|
<span id="loggedInText">Not Logged In</span>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
42
website/templates/construction.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Under Construction</title>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
height: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #0b0b0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > div {
|
||||||
|
margin-left: 2em;
|
||||||
|
margin-right: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, a:hover {
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<img src="/static/construction.svg" alt="Construction Icon" width="64">
|
||||||
|
<h1>Under Construction</h1>
|
||||||
|
<p>
|
||||||
|
This page is undergoing a revamp and will be available again soon.<br>
|
||||||
|
All API endpoints are still available. Documentation is available <a href="/posts/20240409-API-Documentation.html">here</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -27,9 +27,12 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="mainContent">
|
<div class="mainContent">
|
||||||
|
<img src="/static/snow-leopard.svg" alt="Snow Leopard Icon" width="96" id="miauMiau">
|
||||||
<div>
|
<div>
|
||||||
<img src="/static/orange-globe.svg" alt="Globe Icon" width="64">
|
<!-- <img src="/static/orange-globe.svg" alt="Globe Icon" width="64"> -->
|
||||||
|
|
||||||
<h1>Welcome to Enstrayed.com</h1>
|
<h1>Welcome to Enstrayed.com</h1>
|
||||||
<div class="linkColumn">
|
<div class="linkColumn">
|
||||||
<div>
|
<div>
|
||||||
@@ -55,13 +58,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<img src="/static/snow-leopard.svg" alt="Snow Leopard Icon" width="64">
|
<!-- <img src="/static/snow-leopard.svg" alt="Snow Leopard Icon" width="64"> -->
|
||||||
<h1>@Enstrayed</h1>
|
<h1>@Enstrayed</h1>
|
||||||
<div class="linkColumn marginBottom1em">
|
<div class="linkColumn marginBottom1em">
|
||||||
<a href="https://twitter.com/enstrayed"><img src="/static/icons/twitter.svg" alt="">Twitter</a>
|
<a href="https://twitter.com/enstrayed"><img src="/static/icons/twitter.svg" alt="">Twitter</a>
|
||||||
<a href="https://github.com/enstrayed"><img src="/static/icons/github.svg" alt="">GitHub</a>
|
<a href="https://github.com/enstrayed"><img src="/static/icons/github.svg" alt="">GitHub</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="nowplaying"></div>
|
|
||||||
<div class="blogPostsList">
|
<div class="blogPostsList">
|
||||||
<h2 id="blogPostsHeader" onclick="rerollBlogPostsHeaderText()">Notes</h2>
|
<h2 id="blogPostsHeader" onclick="rerollBlogPostsHeaderText()">Notes</h2>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -70,5 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="nowplaying"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||