this code SUUUUCKs but i dont CARE

This commit is contained in:
May 2023-02-26 10:47:03 -08:00
parent 55e10f5408
commit b95a33e39d
50 changed files with 5456 additions and 5047 deletions

6
.gitignore vendored
View file

@ -1,4 +1,4 @@
node_modules node_modules
.env .env
.data .data
out out

44
.vscode/tasks.json vendored
View file

@ -1,23 +1,23 @@
{ {
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"type": "shell", "type": "shell",
"command":"tsc\nsass src/style:out/style\nrollup -c", "command":"tsc\nsass src/style:out/style\nrollup -c",
"group": { "group": {
"kind": "build", "kind": "build",
"isDefault": true "isDefault": true
}, },
"label": "Build (Bot Server)" "label": "Build (Bot Server)"
}, },
{ {
"type": "shell", "type": "shell",
"command":"tsc\nsass src/style:out/style\nrollup -c\nnode ./out/server/index.js\ndel ./out/* -Recurse", "command":"tsc\nsass src/style:out/style\nrollup -c\nnode ./out/server/index.js\ndel ./out/* -Recurse",
"group": { "group": {
"kind": "build", "kind": "build",
"isDefault": true "isDefault": true
}, },
"label": "Build & Test" "label": "Build & Test"
} }
] ]
} }

46
LICENSE
View file

@ -1,24 +1,24 @@
This is free and unencumbered software released into the public domain. This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any binary, for any purpose, commercial or non-commercial, and by any
means. means.
In jurisdictions that recognize copyright laws, the author or authors In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this relinquishment in perpetuity of all present and future rights to this
software under copyright law. software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE. OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/> For more information, please refer to <http://unlicense.org/>

View file

@ -1,33 +1,33 @@
# monofile # monofile
File sharing via Discord File sharing via Discord
<br> <br>
## .env ## .env
``` ```
TOKEN=KILL-YOURSELF.NOW TOKEN=KILL-YOURSELF.NOW
``` ```
## versions & planned updates ## versions & planned updates
- [X] 1.0.0 initial release - [X] 1.0.0 initial release
- [X] 1.1.0 add file cloning endpoint - [X] 1.1.0 add file cloning endpoint
- [X] 1.1.1 add file cloning webpage - [X] 1.1.1 add file cloning webpage
- [X] 1.1.2 fix file cloning with binary data - [X] 1.1.2 fix file cloning with binary data
- [X] 1.1.3 display current version on pages - [X] 1.1.3 display current version on pages
- [X] 1.1.4 serve /assets as static files & make /server endpoint - [X] 1.1.4 serve /assets as static files & make /server endpoint
- [X] 1.2.0 add file parameters section + custom ids - [X] 1.2.0 add file parameters section + custom ids
- [X] 1.2.1 add file counter to main page - [X] 1.2.1 add file counter to main page
- [X] 1.2.2 clean up this shitty code - [X] 1.2.2 clean up this shitty code
- [X] 1.2.3 bugfixes - [X] 1.2.3 bugfixes
- [ ] 1.3.0 new ui; collections; accounts; utility endpoints; multi file uploads - [ ] 1.3.0 new ui; collections; accounts; utility endpoints; multi file uploads
- [ ] 1.3.1 self-destructing files - [ ] 1.3.1 self-destructing files
- [ ] 1.3.2 disable cloning of local ips - [ ] 1.3.2 disable cloning of local ips
- [ ] 1.4.0 admin panel - [ ] 1.4.0 admin panel
- [ ] 2.0.0 rewrite using theUnfunny's code as a base/rewrite using monofile-core - [ ] 2.0.0 rewrite using theUnfunny's code as a base/rewrite using monofile-core
also todo: monofile-core (written in eris) also todo: monofile-core (written in eris)
## Disclaimer! ## Disclaimer!
This project does some stuff that can be considered questionable. Discord may not like you uploading files this way, and it's a grey area in Discord's TOS. We take no responsibility if Discord locks your account for API abuse. This project does some stuff that can be considered questionable. Discord may not like you uploading files this way, and it's a grey area in Discord's TOS. We take no responsibility if Discord locks your account for API abuse.

1
assets/icons/README.md Normal file
View file

@ -0,0 +1 @@
Icons are part of Microsoft's Fluent icons

View file

@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.95 8.6a6.554 6.554 0 0 1 6.55-6.55c3.596 0 6.55 2.819 6.55 6.45a6.554 6.554 0 0 1-6.55 6.55c-.531 0-1.055-.076-1.552-.204A1.25 1.25 0 0 1 12.7 16.05h-1.75v1.75c0 .69-.56 1.25-1.25 1.25H7.95v1.25a1.75 1.75 0 0 1-1.75 1.75H3.7a1.75 1.75 0 0 1-1.75-1.75v-2.172c0-.73.29-1.429.806-1.944L8.99 9.948a.275.275 0 0 0 .07-.244A6.386 6.386 0 0 1 8.95 8.6Zm9.3-1.6a1.25 1.25 0 1 0-2.5 0 1.25 1.25 0 0 0 2.5 0Z" fill="#DDDDDD"/></svg>

After

Width:  |  Height:  |  Size: 529 B

View file

@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17.5 12a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm-5.478 2A6.47 6.47 0 0 0 11 17.5c0 1.644.61 3.145 1.617 4.29-.802.141-1.675.21-2.617.21-2.89 0-5.128-.656-6.691-2a3.75 3.75 0 0 1-1.305-2.843v-.907A2.25 2.25 0 0 1 4.254 14h7.768Zm3.071.966-.07.058-.057.07a.5.5 0 0 0 0 .568l.058.069 1.77 1.77-1.768 1.766-.057.07a.5.5 0 0 0 0 .568l.058.07.069.057a.5.5 0 0 0 .568 0l.07-.058 1.766-1.767 1.77 1.77.069.058a.5.5 0 0 0 .568 0l.07-.058.058-.07a.5.5 0 0 0 0-.568l-.058-.07-1.77-1.768 1.772-1.77.058-.07a.5.5 0 0 0 0-.568l-.058-.069-.069-.058a.5.5 0 0 0-.569 0l-.069.058-1.771 1.77-1.77-1.77-.07-.058a.5.5 0 0 0-.492-.043l-.076.043ZM10 2.004a5 5 0 1 1 0 10 5 5 0 0 1 0-10Z" fill="#DDDDDD"/></svg>

After

Width:  |  Height:  |  Size: 791 B

1
assets/icons/logout.svg Normal file
View file

@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.25 2.75a1.5 1.5 0 0 0-1.5 1.5v15.5a1.5 1.5 0 0 0 1.5 1.5h5.94a6.5 6.5 0 0 1 7.06-10.012V4.25a1.5 1.5 0 0 0-1.5-1.5H6.25Zm2.25 10.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3Zm9 9.75a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11Zm3.5-5.5a.5.5 0 0 1-.5.5h-4.793l1.647 1.646a.5.5 0 0 1-.708.708l-2.5-2.5a.5.5 0 0 1 0-.708l2.5-2.5a.5.5 0 0 1 .708.708L15.707 17H20.5a.5.5 0 0 1 .5.5Z" fill="#DDDDDD"/></svg>

After

Width:  |  Height:  |  Size: 494 B

View file

@ -1,7 +1,12 @@
{ {
"maxDiscordFiles": 20, "maxDiscordFiles": 20,
"maxDiscordFileSize": 8388608, "maxDiscordFileSize": 8388608,
"targetGuild": "1024080490677936248", "targetGuild": "1024080490677936248",
"targetChannel": "1024080525993971913", "targetChannel": "1024080525993971913",
"requestTimeout":120000 "requestTimeout":120000,
"accounts": {
"registrationEnabled": true,
"requiredForUpload": true
}
} }

5684
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,35 +1,37 @@
{ {
"name": "monofile", "name": "monofile",
"version": "1.3.0-pa", "version": "1.3.0-pa",
"description": "Discord-based file sharing", "description": "Discord-based file sharing",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "node ./out/server/index.js", "start": "node ./out/server/index.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"keywords": [], "keywords": [],
"author": "nbitzz", "author": "nbitzz",
"license": "Unlicense", "license": "Unlicense",
"engines": { "engines": {
"node": ">=v18" "node": ">=v18"
}, },
"dependencies": { "dependencies": {
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"axios": "^0.27.2", "axios": "^0.27.2",
"body-parser": "^1.20.0", "body-parser": "^1.20.0",
"discord.js": "^14.7.1", "cookie-parser": "^1.4.6",
"dotenv": "^16.0.2", "discord.js": "^14.7.1",
"express": "^4.18.1", "dotenv": "^16.0.2",
"multer": "^1.4.5-lts.1", "express": "^4.18.1",
"typescript": "^4.8.3" "multer": "^1.4.5-lts.1",
}, "typescript": "^4.8.3"
"devDependencies": { },
"@rollup/plugin-node-resolve": "^15.0.1", "devDependencies": {
"rollup": "^3.11.0", "@rollup/plugin-node-resolve": "^15.0.1",
"rollup-plugin-svelte": "^7.1.0", "@types/cookie-parser": "^1.4.3",
"sass": "^1.57.1", "rollup": "^3.11.0",
"svelte": "^3.55.1" "rollup-plugin-svelte": "^7.1.0",
} "sass": "^1.57.1",
} "svelte": "^3.55.1"
}
}

View file

@ -1,24 +1,24 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>monofile</title> <title>monofile</title>
<meta name="og:site_name" content="monofile $Version"> <meta name="og:site_name" content="monofile $Version">
<meta name="title" content="$CollectionName"> <meta name="title" content="$CollectionName">
<meta name="description" content="$CollectionId - $Managers manager(s), $Files file(s)"> <meta name="description" content="$CollectionId - $Managers manager(s), $Files file(s)">
<!-- downloads.css is good eenough for this --> <!-- downloads.css is good eenough for this -->
<link <link
rel="stylesheet" rel="stylesheet"
href="/static/style/downloads.css" href="/static/style/downloads.css"
> >
</head> </head>
<body> <body>
</body> </body>
</html> </html>

View file

@ -1,40 +1,40 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>monofile</title> <title>monofile</title>
$metaTags $metaTags
<meta name="og:site_name" content="monofile $Version"> <meta name="og:site_name" content="monofile $Version">
<meta name="title" content="$FileName"> <meta name="title" content="$FileName">
<meta name="description" content="ID: $FileId"> <meta name="description" content="ID: $FileId">
<link <link
rel="stylesheet" rel="stylesheet"
href="/static/style/downloads.css" href="/static/style/downloads.css"
> >
</head> </head>
<body> <body>
<div id="appContent"> <div id="appContent">
<div id="uploadWindow"> <div id="uploadWindow">
<h1> <h1>
$FileName $FileName
</h1> </h1>
<p style="color:#999999"> <p style="color:#999999">
file id <span class="number">$FileId</span> file id <span class="number">$FileId</span>
</p> </p>
<button style="position:relative;width:100%;top:10px;"> <button style="position:relative;width:100%;top:10px;">
<a id="dlbtn" href="/file/$FileId" download="$FileName" style="position:absolute;left:0px;top:0px;height:100%;width:100%;"></a> <a id="dlbtn" href="/file/$FileId" download="$FileName" style="position:absolute;left:0px;top:0px;height:100%;width:100%;"></a>
download download
</button> </button>
<div style="min-height:20px" /> <div style="min-height:20px" />
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View file

@ -1,34 +1,34 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<link <link
rel="stylesheet" rel="stylesheet"
href="/static/style/error.css" href="/static/style/error.css"
> >
<link <link
rel="icon" rel="icon"
type="image/png" type="image/png"
href="/static/assets/monofile-circ.png" href="/static/assets/monofile-circ.png"
> >
<meta <meta
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=0" content="width=device-width, initial-scale=1.0, user-scalable=0"
> >
<title>$code</title> <title>$code</title>
</head> </head>
<body> <body>
<p class="error"> <p class="error">
<span class="code">$code</span> <span class="code">$code</span>
&nbsp;$text &nbsp;$text
</p> </p>
</body> </body>
</html> </html>

View file

@ -1,31 +1,32 @@
<html lang="en"> <!DOCTYPE html>
<html lang="en">
<head>
<head>
<link
rel="stylesheet" <link
href="/static/style/app.css" rel="stylesheet"
> href="/static/style/app.css"
>
<link
rel="icon" <link
type="image/png" rel="icon"
href="/static/assets/monofile-circ.png" type="image/png"
> href="/static/assets/monofile-circ.png"
>
<meta
name="viewport" <meta
content="width=device-width, initial-scale=1.0, user-scalable=0" name="viewport"
> content="width=device-width, initial-scale=1.0, user-scalable=0"
>
<script type="module" src="/static/js/index.js"></script>
<script type="module" src="/static/js/index.js"></script>
<title>monofile</title>
<title>monofile</title>
</head>
</head>
<body>
<body>
</body>
</body>
</html> </html>

View file

@ -1,29 +1,29 @@
import svelte from 'rollup-plugin-svelte' import svelte from 'rollup-plugin-svelte'
import resolve from "@rollup/plugin-node-resolve" import resolve from "@rollup/plugin-node-resolve"
export default [ export default [
{ {
input: "src/client/index.js", input: "src/client/index.js",
output: { output: {
file: 'out/client/index.js', file: 'out/client/index.js',
format: 'esm', format: 'esm',
sourcemap:true sourcemap:true
}, },
plugins: [ plugins: [
resolve(), resolve(),
svelte({}) svelte({})
] ]
}, },
{ {
input: "src/client/collection.js", input: "src/client/collection.js",
output: { output: {
file: 'out/client/collection.js', file: 'out/client/collection.js',
format: 'esm', format: 'esm',
sourcemap:true sourcemap:true
}, },
plugins: [ plugins: [
resolve(), resolve(),
svelte({}) svelte({})
] ]
} }
] ]

View file

@ -1,5 +1,5 @@
import App from "../svelte/Collections.svelte" import App from "../svelte/Collections.svelte"
new App({ new App({
target: document.body target: document.body
}) })

View file

@ -1,5 +1,5 @@
import App from "../svelte/App.svelte" import App from "../svelte/App.svelte"
new App({ new App({
target: document.body target: document.body
}) })

View file

@ -1,165 +1,191 @@
/* import bodyParser from "body-parser"
i really should split this up into different modules import multer, {memoryStorage} from "multer"
*/ import cookieParser from "cookie-parser";
import Discord, { IntentsBitField, Client } from "discord.js"
import bodyParser from "body-parser" import express from "express"
import multer, {memoryStorage} from "multer" import fs, { link } from "fs"
import Discord, { IntentsBitField, Client } from "discord.js" import axios, { AxiosResponse } from "axios"
import express from "express"
import fs, { link } from "fs" import ServeError from "./lib/errors"
import axios, { AxiosResponse } from "axios" import Files from "./lib/files"
import ServeError from "./lib/errors" import * as auth from "./lib/auth"
import * as Accounts from "./lib/accounts"
import Files from "./lib/files"
require("dotenv").config() import { authRoutes } from "./routes/authRoutes";
require("dotenv").config()
const multerSetup = multer({storage:memoryStorage()})
let pkg = require(`${process.cwd()}/package.json`) const multerSetup = multer({storage:memoryStorage()})
let app = express() let pkg = require(`${process.cwd()}/package.json`)
let config = require(`${process.cwd()}/config.json`) let app = express()
let config = require(`${process.cwd()}/config.json`)
app.use("/static/assets",express.static("assets"))
app.use("/static/style",express.static("out/style")) app.use("/static/assets",express.static("assets"))
app.use("/static/js",express.static("out/client")) app.use("/static/style",express.static("out/style"))
app.use("/static/js",express.static("out/client"))
app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
// funcs app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
app.use(cookieParser())
// init data
app.use("/auth",authRoutes)
if (!fs.existsSync(__dirname+"/../.data/")) fs.mkdirSync(__dirname+"/../.data/") // funcs
// init data
// discord if (!fs.existsSync(__dirname+"/../.data/")) fs.mkdirSync(__dirname+"/../.data/")
let client = new Client({intents:[
IntentsBitField.Flags.GuildMessages,
IntentsBitField.Flags.MessageContent // discord
],rest:{timeout:config.requestTimeout}})
let client = new Client({intents:[
let files = new Files(client,config) IntentsBitField.Flags.GuildMessages,
IntentsBitField.Flags.MessageContent
// routes (could probably make these use routers) ],rest:{timeout:config.requestTimeout}})
// index, clone let files = new Files(client,config)
app.get("/", function(req,res) { // routes (could probably make these use routers)
res.sendFile(process.cwd()+"/pages/index.html")
}) // index, clone
// upload handlers app.get("/", function(req,res) {
res.sendFile(process.cwd()+"/pages/index.html")
app.post("/upload",multerSetup.single('file'),async (req,res) => { })
if (req.file) {
try { // upload handlers
let prm = req.header("monofile-params")
let params:{[key:string]:any} = {} app.post("/upload",multerSetup.single('file'),async (req,res) => {
if (prm) { if (req.file) {
params = JSON.parse(prm) try {
} let prm = req.header("monofile-params")
let params:{[key:string]:any} = {}
files.uploadFile({uploadId:params.uploadId,name:req.file.originalname,mime:req.file.mimetype},req.file.buffer) if (prm) {
.then((uID) => res.send(uID)) params = JSON.parse(prm)
.catch((stat) => { }
res.status(stat.status);
res.send(`[err] ${stat.message}`) files.uploadFile({
}) owner: auth.validate(req.cookies.auth),
} catch {
res.status(400) uploadId:params.uploadId,
res.send("[err] bad request") name:req.file.originalname,
} mime:req.file.mimetype
} else { },req.file.buffer)
res.status(400) .then((uID) => res.send(uID))
res.send("[err] bad request") .catch((stat) => {
} res.status(stat.status);
}) res.send(`[err] ${stat.message}`)
})
app.post("/clone",(req,res) => { } catch {
try { res.status(400)
let j = JSON.parse(req.body) res.send("[err] bad request")
if (!j.url) { }
res.status(400) } else {
res.send("[err] invalid url") res.status(400)
} res.send("[err] bad request")
axios.get(j.url,{responseType:"arraybuffer"}).then((data:AxiosResponse) => { }
files.uploadFile({name:j.url.split("/")[req.body.split("/").length-1] || "generic",mime:data.headers["content-type"],uploadId:j.uploadId},Buffer.from(data.data)) })
.then((uID) => res.send(uID))
.catch((stat) => { app.post("/clone",(req,res) => {
res.status(stat.status); try {
res.send(`[err] ${stat.message}`) let j = JSON.parse(req.body)
}) if (!j.url) {
}).catch((err) => { res.status(400)
console.log(err) res.send("[err] invalid url")
res.status(400) }
res.send(`[err] failed to fetch data`) axios.get(j.url,{responseType:"arraybuffer"}).then((data:AxiosResponse) => {
})
} catch { files.uploadFile({
res.status(500) owner: auth.validate(req.cookies.auth),
res.send("[err] an error occured")
} name:j.url.split("/")[req.body.split("/").length-1] || "generic",
}) mime:data.headers["content-type"],
uploadId:j.uploadId
// serve files & download page },Buffer.from(data.data))
.then((uID) => res.send(uID))
app.get("/download/:fileId",(req,res) => { .catch((stat) => {
if (files.getFilePointer(req.params.fileId)) { res.status(stat.status);
let file = files.getFilePointer(req.params.fileId) res.send(`[err] ${stat.message}`)
})
fs.readFile(process.cwd()+"/pages/download.html",(err,buf) => {
if (err) {res.sendStatus(500);console.log(err);return} }).catch((err) => {
res.send( console.log(err)
buf.toString() res.status(400)
.replace(/\$FileId/g,req.params.fileId) res.send(`[err] failed to fetch data`)
.replace(/\$Version/g,pkg.version) })
.replace(/\$FileName/g, } catch {
file.filename res.status(500)
.replace(/\&/g,"&amp;") res.send("[err] an error occured")
.replace(/\</g,"&lt;") }
.replace(/\>/g,"&gt;") })
)
.replace(/\$metaTags/g,file.mime.startsWith("image/") ? `<meta name="og:image" content="https://${req.headers.host}/file/${req.params.fileId}" />` : "") // serve files & download page
)
}) app.get("/download/:fileId",(req,res) => {
} else { if (files.getFilePointer(req.params.fileId)) {
ServeError(res,404,"file not found") let file = files.getFilePointer(req.params.fileId)
}
}) fs.readFile(process.cwd()+"/pages/download.html",(err,buf) => {
if (err) {res.sendStatus(500);console.log(err);return}
let fgRQH = async (req:express.Request,res:express.Response) => { res.send(
files.readFileStream(req.params.fileId).then(f => { buf.toString()
res.setHeader("Content-Type",f.contentType) .replace(/\$FileId/g,req.params.fileId)
res.status(200) .replace(/\$Version/g,pkg.version)
f.dataStream.pipe(res) .replace(/\$FileName/g,
}).catch((err) => { file.filename
ServeError(res,err.status,err.message) .replace(/\&/g,"&amp;")
}) .replace(/\</g,"&lt;")
} .replace(/\>/g,"&gt;")
)
app.get("/server",(req,res) => { .replace(/\$metaTags/g,
res.send(JSON.stringify({ file.mime.startsWith("image/")
...config, ? `<meta name="og:image" content="https://${req.headers.host}/file/${req.params.fileId}" />`
version:pkg.version, : (
files:Object.keys(files.files).length file.mime.startsWith("video/")
})) ? `<meta name="og:video:url" content="https://${req.headers.host}/file/${req.params.fileId}" />\n<meta name="og:video:type" content="${file.mime.replace(/\"/g,"")}">`
}) : ""
)
app.get("/file/:fileId",fgRQH) )
app.get("/:fileId",fgRQH) )
})
/* } else {
routes should be in this order: ServeError(res,404,"file not found")
}
index })
api
dl pages let fgRQH = async (req:express.Request,res:express.Response) => {
file serving files.readFileStream(req.params.fileId).then(f => {
*/ res.setHeader("Content-Type",f.contentType)
res.status(200)
// listen on 3000 or MONOFILE_PORT f.dataStream.pipe(res)
}).catch((err) => {
app.listen(process.env.MONOFILE_PORT || 3000,function() { ServeError(res,err.status,err.message)
console.log("Web OK!") })
}) }
app.get("/server",(req,res) => {
res.send(JSON.stringify({
...config,
version:pkg.version,
files:Object.keys(files.files).length
}))
})
app.get("/file/:fileId",fgRQH)
app.get("/:fileId",fgRQH)
/*
routes should be in this order:
index
api
dl pages
file serving
*/
// listen on 3000 or MONOFILE_PORT
app.listen(process.env.MONOFILE_PORT || 3000,function() {
console.log("Web OK!")
})
client.login(process.env.TOKEN) client.login(process.env.TOKEN)

View file

@ -1,131 +1,100 @@
import crypto from "crypto" import crypto from "crypto"
import * as auth from "./auth"; import * as auth from "./auth";
import { readFile, writeFile } from "fs/promises" import { readFile, writeFile } from "fs/promises"
// this is probably horrible // this is probably horrible
// but i don't even care anymore // but i don't even care anymore
export let Accounts: Account[] = [] export let Accounts: Account[] = []
export interface Account { export interface Account {
id : string id : string
username: string username : string
password: { password : {
hash: string hash : string
salt: string salt : string
} }
accounts: string[] files : string[]
admin : boolean collections : string[]
} admin : boolean
}
export function create(username:string,pwd:string,admin:boolean=false) {
let accId = crypto.randomBytes(12).toString("hex") export function create(username:string,pwd:string,admin:boolean=false) {
let accId = crypto.randomBytes(12).toString("hex")
Accounts.push(
{ Accounts.push(
id: accId, {
username: username, id: accId,
password: password.hash(pwd), username: username,
accounts: [], password: password.hash(pwd),
admin: admin files: [],
} collections: [],
) admin: admin
}
save() )
return accId save()
}
return accId
export function getFromUsername(username:string) { }
return Accounts.find(e => e.username == username)
} export function getFromUsername(username:string) {
return Accounts.find(e => e.username == username)
export function getFromId(id:string) { }
return Accounts.find(e => e.id == id)
} export function getFromId(id:string) {
return Accounts.find(e => e.id == id)
export function getFromToken(token:string) { }
let accId = auth.validate(token)
if (!accId) return export function getFromToken(token:string) {
return getFromId(accId) let accId = auth.validate(token)
} if (!accId) return
return getFromId(accId)
export function deleteAccount(id:string) { }
Accounts.splice(Accounts.findIndex(e => e.id == id),1)
save() export function deleteAccount(id:string) {
} Accounts.splice(Accounts.findIndex(e => e.id == id),1)
save()
export namespace password { }
export function hash(password:string,_salt?:string) {
let salt = _salt || crypto.randomBytes(12).toString('base64') export namespace password {
let hash = crypto.createHash('sha256').update(`${salt}${password}`).digest('hex') export function hash(password:string,_salt?:string) {
let salt = _salt || crypto.randomBytes(12).toString('base64')
return { let hash = crypto.createHash('sha256').update(`${salt}${password}`).digest('hex')
salt:salt,
hash:hash return {
} salt:salt,
} hash:hash
}
export function set(id:string,password:string) { }
let acc = Accounts.find(e => e.id == id)
if (!acc) return export function set(id:string,password:string) {
let acc = Accounts.find(e => e.id == id)
acc.password = hash(password) if (!acc) return
save()
} acc.password = hash(password)
save()
export function check(id:string,password:string) { }
let acc = Accounts.find(e => e.id == id)
if (!acc) return export function check(id:string,password:string) {
let acc = Accounts.find(e => e.id == id)
return acc.password.hash == hash(password,acc.password.salt).hash if (!acc) return
}
} return acc.password.hash == hash(password,acc.password.salt).hash
}
export namespace rbxaccounts { }
export function add(id:string,name:string) {
let acc = getFromId(id) export function save() {
if (!acc) return writeFile(`${process.cwd()}/.data/accounts.json`,JSON.stringify(Accounts))
.catch((err) => console.error(err))
/* check for account that already has name */ }
let idx = acc.accounts.findIndex(e=>e==name)
if (idx > -1) return readFile(`${process.cwd()}/.data/accounts.json`)
.then((buf) => {
acc.accounts = [...acc.accounts,name] Accounts = JSON.parse(buf.toString())
save() }).catch(err => console.error(err))
return .finally(() => {
} if (!Accounts.find(e => e.admin)) {
create("admin","admin",true)
export function remove(id:string,name:string) { }
let acc = getFromId(id)
if (!acc) return
let idx = acc.accounts.findIndex(e=>e==name)
if (idx < 0) return
acc.accounts.splice(idx,1)
save()
return
}
export function clear(id:string) {
let acc = getFromId(id)
if (!acc) return
acc.accounts = []
save()
return
}
}
export function save() {
writeFile(`${process.cwd()}/.data/accounts.json`,JSON.stringify(Accounts))
.catch((err) => console.error(err))
}
readFile(`${process.cwd()}/.data/accounts.json`)
.then((buf) => {
Accounts = JSON.parse(buf.toString())
}).catch(err => console.error(err))
.finally(() => {
if (!Accounts.find(e => e.admin)) {
create("admin","admin",true)
}
}) })

View file

@ -1,58 +1,58 @@
import crypto from "crypto" import crypto from "crypto"
import { readFile, writeFile } from "fs/promises" import { readFile, writeFile } from "fs/promises"
export let AuthTokens: AuthToken[] = [] export let AuthTokens: AuthToken[] = []
export let AuthTokenTO:{[key:string]:NodeJS.Timeout} = {} export let AuthTokenTO:{[key:string]:NodeJS.Timeout} = {}
export interface AuthToken { export interface AuthToken {
account: string, account: string,
token: string, token: string,
expire: number expire: number
} }
export function create(id:string,expire:number=(24*60*60*1000)) { export function create(id:string,expire:number=(24*60*60*1000)) {
let token = { let token = {
account:id, account:id,
token:crypto.randomBytes(12).toString('hex'), token:crypto.randomBytes(12).toString('hex'),
expire:Date.now()+expire expire:Date.now()+expire
} }
AuthTokens.push(token) AuthTokens.push(token)
tokenTimer(token) tokenTimer(token)
save() save()
return token.token return token.token
} }
export function validate(token:string) { export function validate(token:string) {
return AuthTokens.find(e => e.token == token && Date.now() < e.expire)?.account return AuthTokens.find(e => e.token == token && Date.now() < e.expire)?.account
} }
export function tokenTimer(token:AuthToken) { export function tokenTimer(token:AuthToken) {
if (Date.now() >= token.expire) { if (Date.now() >= token.expire) {
invalidate(token.token) invalidate(token.token)
return return
} }
AuthTokenTO[token.token] = setTimeout(() => invalidate(token.token),token.expire-Date.now()) AuthTokenTO[token.token] = setTimeout(() => invalidate(token.token),token.expire-Date.now())
} }
export function invalidate(token:string) { export function invalidate(token:string) {
if (AuthTokenTO[token]) { if (AuthTokenTO[token]) {
clearTimeout(AuthTokenTO[token]) clearTimeout(AuthTokenTO[token])
} }
AuthTokens.splice(AuthTokens.findIndex(e => e.token == token),1) AuthTokens.splice(AuthTokens.findIndex(e => e.token == token),1)
save() save()
} }
export function save() { export function save() {
writeFile(`${process.cwd()}/.data/tokens.json`,JSON.stringify(AuthTokens)) writeFile(`${process.cwd()}/.data/tokens.json`,JSON.stringify(AuthTokens))
.catch((err) => console.error(err)) .catch((err) => console.error(err))
} }
readFile(`${process.cwd()}/.data/tokens.json`) readFile(`${process.cwd()}/.data/tokens.json`)
.then((buf) => { .then((buf) => {
AuthTokens = JSON.parse(buf.toString()) AuthTokens = JSON.parse(buf.toString())
AuthTokens.forEach(e => tokenTimer(e)) AuthTokens.forEach(e => tokenTimer(e))
}).catch(err => console.error(err)) }).catch(err => console.error(err))

View file

@ -1,35 +1,36 @@
import { Response } from "express"; import { Response } from "express";
import { readFile } from "fs/promises" import { readFile } from "fs/promises"
let errorPage:string let errorPage:string
export default async function ServeError( export default async function ServeError(
res:Response, res:Response,
code:number, code:number,
reason:string reason:string
) { ) {
// fetch error page if not cached // fetch error page if not cached
if (!errorPage) { if (!errorPage) {
errorPage = errorPage =
( (
await readFile(`${process.cwd()}/pages/error.html`) await readFile(`${process.cwd()}/pages/error.html`)
.catch((err) => console.error(err)) .catch((err) => console.error(err))
|| "<pre>$code $text</pre>" || "<pre>$code $text</pre>"
) )
.toString() .toString()
} }
// serve error // serve error
res.status(code) res.statusMessage = reason
res.send( res.status(code)
errorPage res.send(
.replace(/\$code/g,code.toString()) errorPage
.replace(/\$text/g,reason) .replace(/\$code/g,code.toString())
) .replace(/\$text/g,reason)
} )
}
export function Redirect(res:Response,url:string) {
res.status(302) export function Redirect(res:Response,url:string) {
res.header("Location",url) res.status(302)
res.send() res.header("Location",url)
res.send()
} }

View file

@ -1,256 +1,270 @@
import axios from "axios"; import axios from "axios";
import Discord, { Client, TextBasedChannel } from "discord.js"; import Discord, { Client, TextBasedChannel } from "discord.js";
import { readFile, writeFile } from "fs"; import { readFile, writeFile } from "fs";
import { Readable } from "node:stream" import { Readable } from "node:stream"
export let id_check_regex = /[A-Za-z0-9_\-\.]+/ export let id_check_regex = /[A-Za-z0-9_\-\.]+/
export let alphanum = Array.from("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") export let alphanum = Array.from("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
// bad solution but whatever // bad solution but whatever
export function generateFileId() { export function generateFileId() {
let fid = "" let fid = ""
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
fid += alphanum[Math.floor(Math.random()*alphanum.length)] fid += alphanum[Math.floor(Math.random()*alphanum.length)]
} }
return fid return fid
} }
export interface FileUploadSettings { export interface FileUploadSettings {
name?: string, name?: string,
mime: string, mime: string,
uploadId?: string uploadId?: string,
} owner?:string
}
export interface Configuration {
maxDiscordFiles: number, export interface Configuration {
maxDiscordFileSize: number, maxDiscordFiles: number,
targetGuild: string, maxDiscordFileSize: number,
targetChannel: string, targetGuild: string,
requestTimeout: number targetChannel: string,
} requestTimeout: number,
export interface FilePointer { accounts: {
filename:string, registrationEnabled: boolean,
mime:string, requiredForUpload: boolean
messageids:string[] }
} }
export interface StatusCodeError { export interface FilePointer {
status: number, filename:string,
message: string mime:string,
} messageids:string[],
owner?:string
/* */ }
export default class Files { export interface StatusCodeError {
status: number,
config: Configuration message: string
client: Client }
files: {[key:string]:FilePointer} = {}
uploadChannel?: TextBasedChannel /* */
constructor(client: Client, config: Configuration) { export default class Files {
this.config = config; config: Configuration
this.client = client; client: Client
files: {[key:string]:FilePointer} = {}
client.on("ready",() => { uploadChannel?: TextBasedChannel
console.log("Discord OK!")
constructor(client: Client, config: Configuration) {
client.guilds.fetch(config.targetGuild).then((g) => {
g.channels.fetch(config.targetChannel).then((a) => { this.config = config;
if (a?.isTextBased()) { this.client = client;
this.uploadChannel = a
} client.on("ready",() => {
}) console.log("Discord OK!")
})
}) client.guilds.fetch(config.targetGuild).then((g) => {
g.channels.fetch(config.targetChannel).then((a) => {
readFile(process.cwd()+"/.data/files.json",(err,buf) => { if (a?.isTextBased()) {
if (err) {console.log(err);return} this.uploadChannel = a
this.files = JSON.parse(buf.toString() || "{}") }
}) })
})
} })
uploadFile(settings:FileUploadSettings,fBuffer:Buffer):Promise<string|StatusCodeError> { readFile(process.cwd()+"/.data/files.json",(err,buf) => {
return new Promise<string>(async (resolve,reject) => { if (err) {console.log(err);return}
if (!this.uploadChannel) { this.files = JSON.parse(buf.toString() || "{}")
reject({status:503,message:"server is not ready - please try again later"}) })
return
} }
if (!settings.name || !settings.mime) { uploadFile(settings:FileUploadSettings,fBuffer:Buffer):Promise<string|StatusCodeError> {
reject({status:400,message:"missing name/mime"}); return new Promise<string>(async (resolve,reject) => {
return if (!this.uploadChannel) {
} reject({status:503,message:"server is not ready - please try again later"})
return
let uploadId = (settings.uploadId || generateFileId()).toString(); }
if ((uploadId.match(id_check_regex) || [])[0] != uploadId || uploadId.length > 30) { if (!settings.name || !settings.mime) {
reject({status:400,message:"invalid id"});return reject({status:400,message:"missing name/mime"});
} return
}
if (this.files[uploadId]) {
reject({status:400,message:"a file with this id already exists"}); if (!settings.owner && this.config.accounts.requiredForUpload) {
return reject({status:401,message:"an account is required for upload"});
} return
}
if (settings.name.length > 128) {
reject({status:400,message:"name too long"}); let uploadId = (settings.uploadId || generateFileId()).toString();
return
} if ((uploadId.match(id_check_regex) || [])[0] != uploadId || uploadId.length > 30) {
reject({status:400,message:"invalid id"});return
if (settings.mime.length > 128) { }
reject({status:400,message:"mime too long"});
return if (this.files[uploadId]) {
} reject({status:400,message:"a file with this id already exists"});
return
// get buffer }
if (fBuffer.byteLength >= (this.config.maxDiscordFileSize*this.config.maxDiscordFiles)) {
reject({status:400,message:"file too large"}); if (settings.name.length > 128) {
return reject({status:400,message:"name too long"});
} return
}
// generate buffers to upload
let toUpload = [] if (settings.mime.length > 128) {
for (let i = 0; i < Math.ceil(fBuffer.byteLength/this.config.maxDiscordFileSize); i++) { reject({status:400,message:"mime too long"});
toUpload.push( return
fBuffer.subarray( }
i*this.config.maxDiscordFileSize,
Math.min( // get buffer
fBuffer.byteLength, if (fBuffer.byteLength >= (this.config.maxDiscordFileSize*this.config.maxDiscordFiles)) {
(i+1)*this.config.maxDiscordFileSize reject({status:400,message:"file too large"});
) return
) }
)
} // generate buffers to upload
let toUpload = []
// begin uploading for (let i = 0; i < Math.ceil(fBuffer.byteLength/this.config.maxDiscordFileSize); i++) {
let uploadTmplt:Discord.AttachmentBuilder[] = toUpload.map((e) => { toUpload.push(
return new Discord.AttachmentBuilder(e) fBuffer.subarray(
.setName(Math.random().toString().slice(2)) i*this.config.maxDiscordFileSize,
}) Math.min(
let uploadGroups = [] fBuffer.byteLength,
for (let i = 0; i < Math.ceil(uploadTmplt.length/10); i++) { (i+1)*this.config.maxDiscordFileSize
uploadGroups.push(uploadTmplt.slice(i*10,((i+1)*10))) )
} )
)
let msgIds = [] }
for (let i = 0; i < uploadGroups.length; i++) { // begin uploading
let uploadTmplt:Discord.AttachmentBuilder[] = toUpload.map((e) => {
let ms = await this.uploadChannel.send({ return new Discord.AttachmentBuilder(e)
files:uploadGroups[i] .setName(Math.random().toString().slice(2))
}).catch((e) => {console.error(e)}) })
let uploadGroups = []
if (ms) { for (let i = 0; i < Math.ceil(uploadTmplt.length/10); i++) {
msgIds.push(ms.id) uploadGroups.push(uploadTmplt.slice(i*10,((i+1)*10)))
} else { }
reject({status:500,message:"please try again"}); return
} let msgIds = []
}
for (let i = 0; i < uploadGroups.length; i++) {
// save
let ms = await this.uploadChannel.send({
resolve(await this.writeFile( files:uploadGroups[i]
uploadId, }).catch((e) => {console.error(e)})
{
filename:settings.name, if (ms) {
messageids:msgIds, msgIds.push(ms.id)
mime:settings.mime } else {
} reject({status:500,message:"please try again"}); return
)) }
}) }
}
// save
// fs
resolve(await this.writeFile(
writeFile(uploadId: string, file: FilePointer):Promise<string> { uploadId,
return new Promise((resolve, reject) => { {
filename:settings.name,
this.files[uploadId] = file messageids:msgIds,
mime:settings.mime,
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => {
owner:settings.owner
if (err) { }
reject({status:500,message:"please try again"}); ))
delete this.files[uploadId]; })
return }
}
// fs
resolve(uploadId)
writeFile(uploadId: string, file: FilePointer):Promise<string> {
}) return new Promise((resolve, reject) => {
}) this.files[uploadId] = file
}
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => {
// todo: move read code here
if (err) {
readFileStream(uploadId: string):Promise<{dataStream:Readable,contentType:string}> { reject({status:500,message:"please try again"});
return new Promise(async (resolve,reject) => { delete this.files[uploadId];
if (!this.uploadChannel) { return
reject({status:503,message:"server is not ready - please try again later"}) }
return
} resolve(uploadId)
if (this.files[uploadId]) { })
let file = this.files[uploadId]
})
let dataStream = new Readable({ }
read(){}
}) // todo: move read code here
resolve({ readFileStream(uploadId: string):Promise<{dataStream:Readable,contentType:string}> {
contentType: file.mime, return new Promise(async (resolve,reject) => {
dataStream: dataStream if (!this.uploadChannel) {
}) reject({status:503,message:"server is not ready - please try again later"})
return
for (let i = 0; i < file.messageids.length; i++) { }
let msg = await this.uploadChannel.messages.fetch(file.messageids[i]).catch(() => {return null})
if (msg?.attachments) { if (this.files[uploadId]) {
let attach = Array.from(msg.attachments.values()) let file = this.files[uploadId]
for (let i = 0; i < attach.length; i++) {
let d = await axios.get(attach[i].url,{responseType:"arraybuffer"}).catch((e:Error) => {console.error(e)}) let dataStream = new Readable({
if (d) { read(){}
dataStream.push(d.data) })
} else {
reject({status:500,message:"internal server error"}) resolve({
dataStream.destroy(new Error("file read error")) contentType: file.mime,
return dataStream: dataStream
} })
}
} for (let i = 0; i < file.messageids.length; i++) {
} let msg = await this.uploadChannel.messages.fetch(file.messageids[i]).catch(() => {return null})
if (msg?.attachments) {
dataStream.push(null) let attach = Array.from(msg.attachments.values())
for (let i = 0; i < attach.length; i++) {
} else { let d = await axios.get(attach[i].url,{responseType:"arraybuffer"}).catch((e:Error) => {console.error(e)})
reject({status:404,message:"not found"}) if (d) {
} dataStream.push(d.data)
}) } else {
} reject({status:500,message:"internal server error"})
dataStream.destroy(new Error("file read error"))
unlink(uploadId:string):Promise<void> { return
return new Promise((resolve,reject) => { }
let tmp = this.files[uploadId]; }
delete this.files[uploadId]; }
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => { }
if (err) {
this.files[uploadId] = tmp dataStream.push(null)
reject()
} else { } else {
resolve() reject({status:404,message:"not found"})
} }
}) })
}
})
} unlink(uploadId:string):Promise<void> {
return new Promise((resolve,reject) => {
getFilePointer(uploadId:string):FilePointer { let tmp = this.files[uploadId];
return this.files[uploadId] delete this.files[uploadId];
} writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => {
if (err) {
} this.files[uploadId] = tmp
reject()
} else {
resolve()
}
})
})
}
getFilePointer(uploadId:string):FilePointer {
return this.files[uploadId]
}
}

View file

@ -0,0 +1,137 @@
import bodyParser from "body-parser";
import { Router } from "express";
import * as Accounts from "../lib/accounts";
import * as auth from "../lib/auth";
import ServeError from "../lib/errors";
let parser = bodyParser.json({
type: ["text/plain","application/json"]
})
export let authRoutes = Router();
let config = require(`${process.cwd()}/config.json`)
authRoutes.post("/login", parser, (req,res) => {
let body:{[key:string]:any}
try {
body = JSON.parse(req.body)
} catch {
ServeError(res,400,"bad request")
return
}
if (typeof body.username != "string" || typeof body.password != "string") {
ServeError(res,400,"please provide a username or password")
return
}
if (auth.validate(req.cookies.auth)) return
/*
check if account exists
*/
let acc = Accounts.getFromUsername(body.username)
if (!acc) {
ServeError(res,401,"username or password incorrect")
return
}
if (!Accounts.password.check(acc.id,body.password)) {
ServeError(res,401,"username or password incorrect")
return
}
/*
assign token
*/
res.cookie("auth",auth.create(acc.id,(3*24*60*60*1000)))
res.status(200)
res.end()
})
authRoutes.post("/create", parser, (req,res) => {
if (!config.accounts.registrationEnabled) {
ServeError(res,403,"account registration disabled")
return
}
let body:{[key:string]:any}
try {
body = JSON.parse(req.body)
} catch {
ServeError(res,400,"bad request")
return
}
if (auth.validate(req.cookies.auth)) return
if (typeof body.username != "string" || typeof body.password != "string") {
ServeError(res,400,"please provide a username or password")
return
}
/*
check if account exists
*/
let acc = Accounts.getFromUsername(body.username)
if (acc) {
ServeError(res,400,"account with this username already exists")
return
}
if (body.username.length < 3 || body.username.length > 20) {
ServeError(res,400,"username must be over or equal to 3 characters or under or equal to 20 characters in length")
return
}
if ((body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username) {
ServeError(res,400,"username contains invalid characters")
return
}
if (body.password.length < 8) {
ServeError(res,400,"password must be 8 characters or longer")
return
}
let newAcc = Accounts.create(body.username,body.password)
/*
assign token
*/
res.cookie("auth",auth.create(newAcc,(3*24*60*60*1000)))
res.status(200)
res.end()
})
authRoutes.post("/logout", (req,res) => {
if (!auth.validate(req.cookies.auth)) {
ServeError(res, 401, "not logged in")
return
}
auth.invalidate(req.cookies.auth)
res.send("logged out")
})
authRoutes.get("/me", (req,res) => {
if (!auth.validate(req.cookies.auth)) {
ServeError(res, 401, "not logged in")
return
}
// lazy rn so
let acc = Accounts.getFromToken(req.cookies.auth)
res.send(acc)
})

View file

@ -1,83 +1,83 @@
/* /*
could probably replace this with fonts served directly could probably replace this with fonts served directly
from the server but it's fine for now from the server but it's fine for now
*/ */
@import url('https://fonts.googleapis.com/css2?family=Fira+Code&family=Inconsolata&family=Source+Sans+Pro:wght@300;400;600;700;900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Fira+Code&family=Inconsolata&family=Source+Sans+Pro:wght@300;400;600;700;900&display=swap');
$FallbackFonts: $FallbackFonts:
-apple-system, -apple-system,
system-ui, system-ui,
BlinkMacSystemFont, BlinkMacSystemFont,
"Segoe UI", "Segoe UI",
Roboto, Roboto,
sans-serif; sans-serif;
%normal { %normal {
font-family: "Source Sans Pro", $FallbackFonts font-family: "Source Sans Pro", $FallbackFonts
} }
/* /*
everything that's not a span everything that's not a span
and/or has the normal class and/or has the normal class
(it's just in case) (it's just in case)
*/ */
*:not(span), .normal { @extend %normal; } *:not(span), .normal { @extend %normal; }
/* /*
for code blocks / terminal for code blocks / terminal
*/ */
.monospace { .monospace {
font-family: "Fira Code", monospace font-family: "Fira Code", monospace
} }
/* /*
colors colors
*/ */
$Background: #252525; $Background: #252525;
/* hsl(210,12.9,24.3) */ /* hsl(210,12.9,24.3) */
$darkish: rgb(54, 62, 70); $darkish: rgb(54, 62, 70);
/* /*
then other stuff then other stuff
*/ */
body { body {
background-color: rgb(30, 33, 36); // this is here so that background-color: rgb(30, 33, 36); // this is here so that
// pulling down to refresh // pulling down to refresh
// on mobile looks good // on mobile looks good
} }
#appContent { #appContent {
background-color: $Background background-color: $Background
} }
/* /*
scrollbars scrollbars
*/ */
* { * {
/* nice scrollbars aren't needed on mobile so */ /* nice scrollbars aren't needed on mobile so */
@media screen and (min-width:500px) { @media screen and (min-width:500px) {
&::-webkit-scrollbar { &::-webkit-scrollbar {
width:5px; width:5px;
} }
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background-color:#191919; background-color:#191919;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background-color:#333; background-color:#333;
&:hover { &:hover {
background-color:#373737; background-color:#373737;
} }
} }
} }
} }

View file

@ -1,37 +1,37 @@
@use "base"; @use "base";
@use "app/topbar"; @use "app/topbar";
@use "app/pulldown"; @use "app/pulldown";
@use "app/uploads"; @use "app/uploads";
.menuBtn { .menuBtn {
text-decoration:none; text-decoration:none;
font-size:16px; font-size:16px;
transition-duration: 100ms; transition-duration: 100ms;
color:#555555; color:#555555;
background-color: #00000000; background-color: #00000000;
border:none; border:none;
margin:0 0 0 0; margin:0 0 0 0;
cursor:pointer; cursor:pointer;
position:relative; position:relative;
top:-1px; top:-1px;
&:hover { &:hover {
color:slategray; color:slategray;
transition-duration: 100ms; transition-duration: 100ms;
} }
} }
#appContent { #appContent {
position:absolute; position:absolute;
left:0px; left:0px;
top:40px; top:40px;
width:100%; width:100%;
height: calc( 100% - 40px ); height: calc( 100% - 40px );
background-image: linear-gradient(#333,base.$Background); background-image: linear-gradient(#333,base.$Background);
@media screen and (max-width:500px) { @media screen and (max-width:500px) {
background-image: linear-gradient(#303030,base.$Background); background-image: linear-gradient(#303030,base.$Background);
} }
} }

View file

@ -1,49 +1,49 @@
@use "../base"; @use "../base";
@use "pulldown/help"; @use "pulldown/help";
@use "pulldown/accounts"; @use "pulldown/accounts";
@use "pulldown/files"; @use "pulldown/files";
#overlay { #overlay {
position:absolute; position:absolute;
left:0px; left:0px;
height: 100%; height: 100%;
width:100%; width:100%;
top:0px; top:0px;
opacity:0.25; opacity:0.25;
border:none; border:none;
outline:none; outline:none;
background-color:#AAAAAA; background-color:#AAAAAA;
z-index: 1000; z-index: 1000;
} }
.pulldown { .pulldown {
position: absolute; position: absolute;
width: 300px; width: 300px;
height: 400px; height: 400px;
background-color: #191919; background-color: #191919;
color: #dddddd; color: #dddddd;
top:0px; top:0px;
left:50%; left:50%;
transform:translateX(-50%); transform:translateX(-50%);
@media screen and (max-width: 500px) { @media screen and (max-width: 500px) {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
p, h1, h2 { p, h1, h2 {
margin:0px; margin:0px;
} }
z-index: 1001; z-index: 1001;
} }
.pulldown_display { .pulldown_display {
position:absolute; position:absolute;
left:0px; left:0px;
top:0px; top:0px;
width:100%; width:100%;
height:100%; height:100%;
} }

View file

@ -1,120 +1,188 @@
.pulldown_display[name=accounts] { .pulldown_display[name=accounts] {
.notLoggedIn { .notLoggedIn {
.container_div { .container_div {
position:absolute; position:absolute;
top:50%; top:50%;
transform:translateY(-50%); transform:translateY(-50%);
width:100%; width:100%;
text-align:center; text-align:center;
h1 { h1 {
font-weight:600; font-weight:600;
font-size:24px; font-size:24px;
@media screen and (max-width:500px) { @media screen and (max-width:500px) {
font-size:30px; font-size:30px;
} }
} }
.flavor { .flavor {
font-size:14px; font-size:14px;
/* good enoough */ /* good enoough */
@media screen and (max-width:500px) { @media screen and (max-width:500px) {
font-size:16px; font-size:16px;
} }
color:#999999; color:#999999;
margin: 0 0 10px 0; margin: 0 0 10px 0;
} }
button { button {
cursor:pointer; cursor:pointer;
background-color:#393939; background-color:#393939;
color:#DDDDDD; color:#DDDDDD;
border:none; border:none;
outline:none; outline:none;
padding:5px; padding:5px;
transition-duration: 250ms; transition-duration: 250ms;
/*overflow:clip;*/ /*overflow:clip;*/
@media screen and (max-width: 500px) { @media screen and (max-width: 500px) {
font-size:16px; font-size:16px;
padding:10px; padding:10px;
} }
&:hover { &:hover {
transition-duration: 250ms; transition-duration: 250ms;
background-color:#434343; background-color:#434343;
color: #ffffff; color: #ffffff;
} }
flex-basis:50%; flex-basis:50%;
flex-grow:1; flex-grow:1;
} }
input[type=text],input[type=password] { input[type=text],input[type=password] {
border:none; border:none;
border-radius:0; border-radius:0;
width:100%; width:100%;
padding:5px; padding:5px;
background-color:#333333; background-color:#333333;
color:#dddddd; color:#dddddd;
outline:none; outline:none;
@media screen and (max-width: 500px) { @media screen and (max-width: 500px) {
font-size:16px; font-size:16px;
padding:10px; padding:10px;
} }
} }
.lgBtnContainer { .pwError {
display:flex; div {
position:relative; border:none;
left:20px; border-radius:0;
width:calc( 100% - 40px ); width:100%;
gap:10px; padding:5px;
overflow:clip; background-color:#663333;
} color:#dddddd;
outline:none;
.fields { font-size:14px;
display:flex; text-align:left;
flex-direction:column;
position:relative; @media screen and (max-width: 500px) {
left:20px; font-size:16px;
width:calc( 100% - 40px ); padding:10px;
gap:5px; }
overflow:clip; }
} }
/* .lgBtnContainer {
a { display:flex;
text-decoration: none; position:relative;
color:#999999; left:20px;
font-size:14px; width:calc( 100% - 40px );
gap:10px;
@media screen and (max-width:500px) { overflow:clip;
font-size:16px; }
}
.fields {
&::after { display:flex;
content:""; flex-direction:column;
font-size:0px; position:relative;
opacity: 0; left:20px;
transition-duration:250ms; width:calc( 100% - 40px );
} gap:5px;
overflow:clip;
&:hover { }
&::after {
font-size:13px; /*
opacity: 1; a {
transition-duration:250ms; text-decoration: none;
} color:#999999;
} font-size:14px;
}
*/ @media screen and (max-width:500px) {
} font-size:16px;
}
}
&::after {
content:"";
font-size:0px;
opacity: 0;
transition-duration:250ms;
}
&:hover {
&::after {
font-size:13px;
opacity: 1;
transition-duration:250ms;
}
}
}
*/
}
}
.loggedIn {
position:absolute;
left:10px;
top:10px;
width:calc( 100% - 20px );
height:calc( 100% - 20px );
h1 {
font-weight:600;
font-size:20px;
color: #AAAAAA;
}
.accountOptions {
button {
position:relative;
width:100%;
cursor:pointer;
height:50px;
background-color: #191919;
border:none;
border-bottom:1px solid #AAAAAA;
transition-duration:150ms;
img {
position:absolute;
left:13px;
top:50%;
transform:translateY(-50%);
}
p {
position:absolute;
top:50%;
left:50px;
color:#DDDDDD;
transform:translateY(-50%);
font-size:14px;
}
&:hover {
transition-duration:150ms;
background-color: #252525;
}
}
}
}
} }

View file

@ -1,34 +1,34 @@
.pulldown_display[name=files] { .pulldown_display[name=files] {
.notLoggedIn { .notLoggedIn {
position:absolute; position:absolute;
top:100%; top:100%;
transform:translateY(-100%); transform:translateY(-100%);
width:100%; width:100%;
text-align:center; text-align:center;
background-color:#202020; background-color:#202020;
.flavor { .flavor {
font-size:16px; font-size:16px;
color:#999999; color:#999999;
margin: 0 0 10px 0; margin: 0 0 10px 0;
} }
button { button {
--col: #999999; --col: #999999;
background-color: #232323; background-color: #232323;
color:var(--col); color:var(--col);
font-size:14px; font-size:14px;
border:1px solid var(--col); border:1px solid var(--col);
padding:2px 20px 2px 20px; padding:2px 20px 2px 20px;
cursor:pointer; cursor:pointer;
transition-duration:250ms; transition-duration:250ms;
&:hover { &:hover {
background-color:#333333; background-color:#333333;
transition-duration:250ms; transition-duration:250ms;
--col:#BBBBBB; --col:#BBBBBB;
} }
} }
} }
} }

View file

@ -1,22 +1,22 @@
.pulldown_display[name=help] { .pulldown_display[name=help] {
overflow-y:auto; overflow-y:auto;
.faqGroup { .faqGroup {
padding:6px 10px 4px 10px; padding:6px 10px 4px 10px;
h2 { h2 {
font-weight: 400; font-weight: 400;
color:#DDDDDD; color:#DDDDDD;
font-size:16px; font-size:16px;
margin:0 0 0 0; margin:0 0 0 0;
} }
p { p {
color:#999999; color:#999999;
font-size:16px; font-size:16px;
margin:0 0 0 0; margin:0 0 0 0;
} }
} }
} }

View file

@ -1,21 +1,21 @@
@use "../base"; @use "../base";
#topbar { #topbar {
position:absolute; position:absolute;
left:0px; left:0px;
top:0px; top:0px;
width:100%; width:100%;
height:40px; height:40px;
/* hsl(210,9.1,12.9) */ /* hsl(210,9.1,12.9) */
background-color: rgb(30, 33, 36); background-color: rgb(30, 33, 36);
display:flex; display:flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
column-gap:5px; column-gap:5px;
} }

View file

@ -1,115 +1,115 @@
#uploadWindow { #uploadWindow {
#add_new_files { #add_new_files {
background-color:#191919; background-color:#191919;
border: 1px solid gray; border: 1px solid gray;
padding: 0px 0px 10px 0px; padding: 0px 0px 10px 0px;
p { p {
font-family: "Fira Code", monospace; font-family: "Fira Code", monospace;
text-align: left; text-align: left;
margin: 0px 0px 0px 10px; margin: 0px 0px 0px 10px;
font-size: 30px; font-size: 30px;
span { span {
position:relative; position:relative;
&._add_files_txt { &._add_files_txt {
font-size:16px; font-size:16px;
top:-4px; top:-4px;
left:10px; left:10px;
@media screen and (max-width:500px) { @media screen and (max-width:500px) {
font-size:20px; font-size:20px;
top:-6px; top:-6px;
left:10px; left:10px;
} }
} }
} }
@media screen and (max-width:500px) { @media screen and (max-width:500px) {
font-size: 40px; font-size: 40px;
span._add_files_txt { span._add_files_txt {
font-size:20px; font-size:20px;
top:-6px; top:-6px;
left:10px; left:10px;
} }
} }
} }
#file_add_btns { #file_add_btns {
width:calc( 100% - 20px ); width:calc( 100% - 20px );
margin:auto; margin:auto;
position:relative; position:relative;
display:flex; display:flex;
flex-direction:row; flex-direction:row;
column-gap:10px; column-gap:10px;
button, input[type=text] { button, input[type=text] {
background-color:#333333; background-color:#333333;
color:#DDDDDD; color:#DDDDDD;
border:none; border:none;
border-radius: 0px; border-radius: 0px;
outline:none; outline:none;
padding:5px; padding:5px;
flex-basis:50%; flex-basis:50%;
flex-grow:1; flex-grow:1;
transition-duration:250ms; transition-duration:250ms;
@media screen and (max-width: 500px) { @media screen and (max-width: 500px) {
font-size:16px; font-size:16px;
padding:10px; padding:10px;
} }
} }
button { button {
cursor:pointer; cursor:pointer;
&:hover { &:hover {
@media screen and (min-width: 500px) { @media screen and (min-width: 500px) {
transition-duration:250ms; transition-duration:250ms;
flex-basis: 60%; flex-basis: 60%;
} }
background-color:#393939; background-color:#393939;
color: #ffffff; color: #ffffff;
} }
} }
.fileUpload { .fileUpload {
width:100%; width:100%;
height:100px; height:100px;
position:relative; position:relative;
background-color:#262626; background-color:#262626;
transition-duration:250ms; transition-duration:250ms;
input[type=file] { input[type=file] {
opacity: 0; opacity: 0;
position:absolute; position:absolute;
left:0px; left:0px;
top:0px; top:0px;
width:100%; width:100%;
height:100%; height:100%;
cursor:pointer; cursor:pointer;
} }
p { p {
position:absolute; position:absolute;
top:50%; top:50%;
transform:translateY(-50%); transform:translateY(-50%);
font-size:12px; font-size:12px;
width:100%; width:100%;
text-align:center; text-align:center;
padding:0px; padding:0px;
margin: 0px; margin: 0px;
} }
&:hover { &:hover {
transition-duration:250ms; transition-duration:250ms;
background-color:#292929; background-color:#292929;
} }
} }
} }
} }
} }

View file

@ -1,59 +1,59 @@
// should probably start using mixins for thingss like this // should probably start using mixins for thingss like this
#uploadWindow { #uploadWindow {
.file { .file {
background-color:#191919; background-color:#191919;
border: 1px solid gray; border: 1px solid gray;
padding: 10px; padding: 10px;
overflow:clip; overflow:clip;
position:relative; position:relative;
h2 { h2 {
font-size: 16px; font-size: 16px;
margin: 0px; margin: 0px;
font-weight:600; font-weight:600;
width:calc( 100% - 20px ); width:calc( 100% - 20px );
} }
input[type=text] { input[type=text] {
background-color:#333333; background-color:#333333;
color:#DDDDDD; color:#DDDDDD;
border:none; border:none;
outline:none; outline:none;
padding:5px; padding:5px;
position:relative; position:relative;
width:100%; width:100%;
transition-duration:250ms; transition-duration:250ms;
@media screen and (max-width: 500px) { @media screen and (max-width: 500px) {
font-size:16px; font-size:16px;
padding:10px; padding:10px;
} }
} }
.buttonContainer { .buttonContainer {
display:flex; display:flex;
column-gap:10px; column-gap:10px;
button { button {
flex-basis: 50%; flex-basis: 50%;
flex-grow: 1; flex-grow: 1;
padding:5px; padding:5px;
} }
} }
.uploadingContainer { .uploadingContainer {
color: #AAAAAA; color: #AAAAAA;
} }
.hitbox { .hitbox {
opacity:0; opacity:0;
position:absolute; position:absolute;
left:0px; left:0px;
top:0px; top:0px;
height:100%; height:100%;
width:100%; width:100%;
} }
} }
} }

View file

@ -1,76 +1,76 @@
@use "uploader/add_new_files"; @use "uploader/add_new_files";
@use "uploader/file"; @use "uploader/file";
#uploadWindow { #uploadWindow {
position:absolute; position:absolute;
left:50%; left:50%;
top:50%; top:50%;
transform:translate(-50%,-50%); transform:translate(-50%,-50%);
padding:10px 15px 10px 15px; padding:10px 15px 10px 15px;
display:flex; display:flex;
flex-direction: column; flex-direction: column;
width:350px; width:350px;
@media screen and (min-width:500px) { @media screen and (min-width:500px) {
max-height: calc( 100% - 80px ); max-height: calc( 100% - 80px );
} }
background-color:#222222; background-color:#222222;
color:#ddd; color:#ddd;
h1, p, a { h1, p, a {
margin: 0px; margin: 0px;
font-size: 14px; font-size: 14px;
} }
a { a {
color:#999; color:#999;
} }
h1 { h1 {
font-weight:600; font-weight:600;
font-size: 25px; font-size: 25px;
} }
.number { .number {
font-family: "Inconsolata", monospace; font-family: "Inconsolata", monospace;
} }
.uploadContainer { .uploadContainer {
overflow:auto; overflow:auto;
} }
button { button {
cursor:pointer; cursor:pointer;
background-color:#393939; background-color:#393939;
color:#DDDDDD; color:#DDDDDD;
border:none; border:none;
outline:none; outline:none;
padding:5px; padding:5px;
transition-duration: 250ms; transition-duration: 250ms;
/*overflow:clip;*/ /*overflow:clip;*/
@media screen and (max-width: 500px) { @media screen and (max-width: 500px) {
font-size:16px; font-size:16px;
padding:10px; padding:10px;
} }
&:hover { &:hover {
transition-duration: 250ms; transition-duration: 250ms;
background-color:#434343; background-color:#434343;
color: #ffffff; color: #ffffff;
} }
} }
@media screen and (max-width: 500px) { @media screen and (max-width: 500px) {
width: calc( 100% - 20px ); width: calc( 100% - 20px );
height: calc( 100% - 20px ); height: calc( 100% - 20px );
border-radius:0px; border-radius:0px;
background-color:#00000000; background-color:#00000000;
transform:none; transform:none;
left:10px; left:10px;
top:10px; top:10px;
padding:0px; padding:0px;
} }
} }

View file

@ -1,20 +1,20 @@
// probably dont need to import the entire // probably dont need to import the entire
// uploads css file // uploads css file
// so i might just make a separate file with mixins // so i might just make a separate file with mixins
// and import them // and import them
@use "app/uploads"; @use "app/uploads";
@use "base"; @use "base";
#appContent { #appContent {
position:absolute; position:absolute;
left:0px; left:0px;
top:0px; top:0px;
width:100%; width:100%;
height:100%; height:100%;
background-image: linear-gradient(#333,base.$Background); background-image: linear-gradient(#333,base.$Background);
@media screen and (max-width:500px) { @media screen and (max-width:500px) {
background-image: linear-gradient(#303030,base.$Background); background-image: linear-gradient(#303030,base.$Background);
} }
} }

View file

@ -1,20 +1,20 @@
@use "_base"; @use "_base";
.error { .error {
font-size:20px; font-size:20px;
color: lightslategray; color: lightslategray;
position: fixed; position: fixed;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%,-50%); transform: translate(-50%,-50%);
text-align:center; text-align:center;
.code { .code {
font-size:25px; font-size:25px;
font-family: "Inconsolata", monospace; font-family: "Inconsolata", monospace;
color: white; color: white;
} }
} }

View file

@ -1,28 +1,28 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import Topbar from "./elem/Topbar.svelte"; import Topbar from "./elem/Topbar.svelte";
import PulldownManager from "./elem/PulldownManager.svelte"; import PulldownManager from "./elem/PulldownManager.svelte";
import UploadWindow from "./elem/UploadWindow.svelte"; import UploadWindow from "./elem/UploadWindow.svelte";
import { pulldownManager } from "./elem/stores.mjs"; import { pulldownManager } from "./elem/stores.mjs";
/** /**
* @type Topbar * @type Topbar
*/ */
let topbar; let topbar;
/** /**
* @type PulldownManager * @type PulldownManager
*/ */
let pulldown; let pulldown;
onMount(() => { onMount(() => {
pulldownManager.set(pulldown) pulldownManager.set(pulldown)
}) })
</script> </script>
<Topbar bind:this={topbar} pulldown={pulldown} /> <Topbar bind:this={topbar} pulldown={pulldown} />
<div id="appContent"> <div id="appContent">
<PulldownManager bind:this={pulldown} /> <PulldownManager bind:this={pulldown} />
<UploadWindow/> <UploadWindow/>
</div> </div>

View file

@ -1,12 +1,12 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
let collection = {} let collection = {}
</script> </script>
<div id="appContent"> <div id="appContent">
<div id="uploadWindow"> <div id="uploadWindow">
<h2>{collection.name}</h2> <h2>{collection.name}</h2>
<p><span class="number">{collection.id}</span>&nbsp;&nbsp—&nbsp;&nbsp;by <strong>@{collection.owner}</strong></p> <p><span class="number">{collection.id}</span>&nbsp;&nbsp—&nbsp;&nbsp;by <strong>@{collection.owner}</strong></p>
</div> </div>
</div> </div>

View file

@ -1,49 +1,49 @@
<script context="module"> <script context="module">
import { writable } from "svelte/store"; import { writable } from "svelte/store";
// can't find a better way to do this // can't find a better way to do this
import Files from "./pulldowns/Files.svelte"; import Files from "./pulldowns/Files.svelte";
import Accounts from "./pulldowns/Accounts.svelte"; import Accounts from "./pulldowns/Accounts.svelte";
import Help from "./pulldowns/Help.svelte"; import Help from "./pulldowns/Help.svelte";
export let allPulldowns = new Map() export let allPulldowns = new Map()
allPulldowns allPulldowns
.set("account",Accounts) .set("account",Accounts)
.set("help",Help) .set("help",Help)
.set("files",Files) .set("files",Files)
export const pulldownOpen = writable(false); export const pulldownOpen = writable(false);
</script> </script>
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import { fade, scale } from "svelte/transition"; import { fade, scale } from "svelte/transition";
export function isOpen() { export function isOpen() {
return $pulldownOpen return $pulldownOpen
} }
export function openPulldown(name) { export function openPulldown(name) {
pulldownOpen.set(name) pulldownOpen.set(name)
} }
export function closePulldown() { export function closePulldown() {
pulldownOpen.set(false) pulldownOpen.set(false)
} }
onMount(() => { onMount(() => {
}) })
</script> </script>
{#if $pulldownOpen} {#if $pulldownOpen}
<div class="pulldown" transition:fade={{duration:200}}> <div class="pulldown" transition:fade={{duration:200}}>
<svelte:component this={allPulldowns.get($pulldownOpen)} /> <svelte:component this={allPulldowns.get($pulldownOpen)} />
</div> </div>
<button <button
id="overlay" id="overlay"
on:click={closePulldown} on:click={closePulldown}
transition:fade={{duration:200}} transition:fade={{duration:200}}
/> />
{/if} {/if}

View file

@ -1,30 +1,31 @@
<script> <script>
import { circOut } from "svelte/easing"; import { circOut } from "svelte/easing";
import { scale } from "svelte/transition"; import { scale } from "svelte/transition";
import PulldownManager, {pulldownOpen} from "./PulldownManager.svelte"; import PulldownManager, {pulldownOpen} from "./PulldownManager.svelte";
import { _void } from "./transition/_void"; import { account } from "./stores.mjs";
import { _void } from "./transition/_void";
/**
* @type PulldownManager /**
*/ * @type PulldownManager
export let pulldown; */
</script> export let pulldown;
</script>
<div id="topbar">
{#if $pulldownOpen} <div id="topbar">
<button {#if $pulldownOpen}
class="menuBtn" <button
on:click={pulldown.closePulldown} class="menuBtn"
transition:_void={{duration:200,prop:"width",easingFunc:circOut}} on:click={pulldown.closePulldown}
>close</button> transition:_void={{duration:200,prop:"width",easingFunc:circOut}}
{/if} >close</button>
{/if}
<!-- too lazy to make this better -->
<!-- too lazy to make this better -->
<button class="menuBtn" on:click={() => pulldown.openPulldown("files")}>files</button>
<button class="menuBtn" on:click={() => pulldown.openPulldown("account")}>account</button> <button class="menuBtn" on:click={() => pulldown.openPulldown("files")}>files</button>
<button class="menuBtn" on:click={() => pulldown.openPulldown("help")}>help</button> <button class="menuBtn" on:click={() => pulldown.openPulldown("account")}>{$account.username ? `@${$account.username}` : "account"}</button>
<button class="menuBtn" on:click={() => pulldown.openPulldown("help")}>help</button>
<div /> <!-- not sure what's offcenter but something is
so this div is here to ""fix"" that --> <div /> <!-- not sure what's offcenter but something is
so this div is here to ""fix"" that -->
</div> </div>

View file

@ -1,217 +1,222 @@
<script> <script>
import { _void } from "./transition/_void.js"; import { _void } from "./transition/_void.js";
import { padding_scaleY } from "./transition/padding_scaleY.js" import { padding_scaleY } from "./transition/padding_scaleY.js"
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { circIn, circOut } from "svelte/easing"; import { circIn, circOut } from "svelte/easing";
import { serverStats, refresh_stats, account } from "./stores.mjs";
import AttachmentZone from "./uploader/AttachmentZone.svelte";
import AttachmentZone from "./uploader/AttachmentZone.svelte";
// stats
// stats
let ServerStats = {}
refresh_stats()
let refresh_stats = () => {
fetch("/server").then(async (data) => { // uploads
ServerStats = await data.json()
}) let attachmentZone;
} let uploads = {};
let uploadInProgress = false;
refresh_stats()
let handle_file_upload = (ev) => {
// uploads if (ev.detail.type == "clone") {
uploads[Math.random().toString().slice(2)] = {
let attachmentZone; type: "clone",
let uploads = {}; name: ev.detail.url,
let uploadInProgress = false; url: ev.detail.url,
let handle_file_upload = (ev) => { params: {
if (ev.detail.type == "clone") { uploadId: ""
uploads[Math.random().toString().slice(2)] = { },
type: "clone",
name: ev.detail.url, uploadStatus:{
url: ev.detail.url, fileId: null,
error: null,
params: { }
uploadId: "" }
},
uploads = uploads
uploadStatus:{ } else if (ev.detail.type == "upload") {
fileId: null, ev.detail.files.forEach((v,x) => {
error: null, uploads[Math.random().toString().slice(2)] = {
} type: "upload",
} name: v.name,
file: v,
uploads = uploads
} else if (ev.detail.type == "upload") { params: {
ev.detail.files.forEach((v,x) => { uploadId: ""
uploads[Math.random().toString().slice(2)] = { },
type: "upload",
name: v.name, uploadStatus:{
file: v, fileId: null,
error: null,
params: { }
uploadId: "" }
}, })
uploadStatus:{ uploads = uploads
fileId: null, }
error: null, }
}
} let handle_fetch_promise = (x,prom) => {
}) return prom.then(async (res) => {
let txt = await res.text()
uploads = uploads if (txt.startsWith("[err]")) uploads[x].uploadStatus.error = txt;
} else {
} uploads[x].uploadStatus.fileId = txt;
let handle_fetch_promise = (x,prom) => { refresh_stats();
return prom.then(async (res) => { }
let txt = await res.text() }).catch((err) => {
if (txt.startsWith("[err]")) uploads[x].uploadStatus.error = txt; uploads[x].uploadStatus.error = err.toString();
else { })
uploads[x].uploadStatus.fileId = txt; }
refresh_stats(); let upload_files = () => {
} uploadInProgress = true
}).catch((err) => {
uploads[x].uploadStatus.error = err.toString(); // go through all files
}) Object.entries(uploads).forEach(([x,v]) => {
} switch(v.type) {
case "upload":
let upload_files = () => { let fd = new FormData()
uploadInProgress = true fd.append("file",v.file)
// go through all files handle_fetch_promise(x,fetch("/upload",{
Object.entries(uploads).forEach(([x,v]) => { headers: {
switch(v.type) { "monofile-params": JSON.stringify(v.params)
case "upload": },
let fd = new FormData() method: "POST",
fd.append("file",v.file) body: fd
}))
handle_fetch_promise(x,fetch("/upload",{ break
headers: { case "clone":
"monofile-params": JSON.stringify(v.params) handle_fetch_promise(x,fetch("/clone",{
}, method: "POST",
method: "POST", body: JSON.stringify({
body: fd url: v.url,
})) ...v.params
break })
case "clone": }))
handle_fetch_promise(x,fetch("/clone",{ break
method: "POST", }
body: JSON.stringify({ })
url: v.url, }
...v.params
}) // animation
}))
break function fileTransition(node) {
} return {
}) duration: 300,
} css: t => {
let eased = circOut(t)
// animation
return `
function fileTransition(node) { height: ${eased*(node.offsetHeight-22)}px;
return { padding: ${eased*10}px 10px;
duration: 300, `
css: t => { }
let eased = circOut(t) }
}
return `
height: ${eased*(node.offsetHeight-20)}px; </script>
padding: ${eased*10}px 10px;
` <div id="uploadWindow">
} <h1>monofile</h1>
} <p style:color="#999999">
} <span class="number">{$serverStats.version ? `v${$serverStats.version}` : "•••"}</span>&nbsp;&nbsp—&nbsp;&nbsp;Discord based file sharing
</p>
</script>
<div style:min-height="10px" />
<div id="uploadWindow">
<h1>monofile</h1> <!-- consider splitting the file thing into a separate element maybe -->
<p style:color="#999999">
<span class="number">{ServerStats.version ? `v${ServerStats.version}` : "•••"}</span>&nbsp;&nbsp—&nbsp;&nbsp;Discord based file sharing <div class="uploadContainer">
</p> {#each Object.entries(uploads) as upload (upload[0])}
<!-- container to allow for animate directive -->
<div style:min-height="10px" /> <div>
<div class="file" transition:fileTransition style:border={upload[1].uploadStatus.error ? "1px solid #BB7070" : ""}>
<!-- consider splitting the file thing into a separate element maybe --> <h2>{upload[1].name} <span style:color="#999999" style:font-weight="400">{upload[1].type}{@html upload[1].type == "upload" ? `&nbsp;(${Math.round(upload[1].file.size/1048576)}MB)` : ""}</span></h2>
<div class="uploadContainer"> {#if upload[1].maximized && !uploadInProgress}
{#each Object.entries(uploads) as upload (upload[0])} <div transition:padding_scaleY|local>
<!-- container to allow for animate directive --> <div style:height="10px" />
<div> <input placeholder="custom id" type="text" bind:value={ uploads[upload[0]].params.uploadId }>
<div class="file" transition:fileTransition style:border={upload[1].uploadStatus.error ? "1px solid #BB7070" : ""}> <div style:height="10px" />
<h2>{upload[1].name} <span style:color="#999999" style:font-weight="400">{upload[1].type}{@html upload[1].type == "upload" ? `&nbsp;(${Math.round(upload[1].file.size/1048576)}MB)` : ""}</span></h2> <div class="buttonContainer">
<button on:click={() => {delete uploads[upload[0]];uploads=uploads;}}>
{#if upload[1].maximized && !uploadInProgress} delete
<div transition:padding_scaleY|local> </button>
<div style:height="10px" /> <button on:click={() => uploads[upload[0]].maximized = false}>
<input placeholder="custom id" type="text" bind:value={ uploads[upload[0]].params.uploadId }> minimize
<div style:height="10px" /> </button>
<div class="buttonContainer"> </div>
<button on:click={() => {delete uploads[upload[0]];uploads=uploads;}}> </div>
delete {:else if !uploadInProgress}
</button> <button on:click={() => uploads[upload[0]].maximized = true} class="hitbox"></button>
<button on:click={() => uploads[upload[0]].maximized = false}> {:else}
minimize <div transition:padding_scaleY|local class="uploadingContainer">
</button> {#if !upload[1].uploadStatus.fileId}
</div> <p in:fade={{duration:300, delay:400, easingFunc:circOut}} out:padding_scaleY={{easingFunc:circIn,op:true}}>{upload[1].uploadStatus.error ?? "Uploading..."}</p>
</div> {/if}
{:else if !uploadInProgress}
<button on:click={() => uploads[upload[0]].maximized = true} class="hitbox"></button> {#if upload[1].uploadStatus.fileId}
{:else} <div style:height="10px" transition:padding_scaleY />
<div transition:padding_scaleY|local class="uploadingContainer"> {#if !upload[1].viewingUrl}
{#if !upload[1].uploadStatus.fileId} <div class="buttonContainer" out:_void in:_void={{easingFunc:circOut}}>
<p in:fade={{duration:300, delay:400, easingFunc:circOut}} out:padding_scaleY={{easingFunc:circIn,op:true}}>{upload[1].uploadStatus.error ?? "Uploading..."}</p> <button on:click={() => uploads[upload[0]].viewingUrl = true}>
{/if} view url
</button>
{#if upload[1].uploadStatus.fileId} <button on:click={() => navigator.clipboard.writeText(`https://${window.location.host}/download/${upload[1].uploadStatus.fileId}`)}>
<div style:height="10px" transition:padding_scaleY /> copy url
{#if !upload[1].viewingUrl} </button>
<div class="buttonContainer" out:_void in:_void={{easingFunc:circOut}}> </div>
<button on:click={() => uploads[upload[0]].viewingUrl = true}> {:else}
view url <div class="buttonContainer" out:_void in:_void={{easingFunc:circOut}}>
</button> <input type="text" readonly value={`https://${window.location.host}/download/${upload[1].uploadStatus.fileId}`} style:flex-basis="80%">
<button on:click={() => navigator.clipboard.writeText(`https://${window.location.host}/download/${upload[1].uploadStatus.fileId}`)}> <button on:click={() => uploads[upload[0]].viewingUrl = false} style:flex-basis="20%">
copy url ok
</button> </button>
</div> </div>
{:else} {/if}
<div class="buttonContainer" out:_void in:_void={{easingFunc:circOut}}> {/if}
<input type="text" readonly value={`https://${window.location.host}/download/${upload[1].uploadStatus.fileId}`} style:flex-basis="80%"> </div>
<button on:click={() => uploads[upload[0]].viewingUrl = false} style:flex-basis="20%"> {/if}
ok </div>
</button> <div style:height="10px" transition:padding_scaleY />
</div> </div>
{/if} {/each}
{/if} </div>
</div>
{/if} {#if uploadInProgress == false}
</div>
<div style:height="10px" transition:padding_scaleY /> <!-- if required for upload, check if logged in -->
</div> {#if ($serverStats.accounts||{}).requiredForUpload ? !!$account.username : true}
{/each}
</div> <AttachmentZone bind:this={attachmentZone} on:addFiles={handle_file_upload}/>
<div style:min-height="10px" transition:_void={{rTarg:"height",prop:"min-height"}} />
{#if uploadInProgress == false} {#if Object.keys(uploads).length > 0}
<AttachmentZone bind:this={attachmentZone} on:addFiles={handle_file_upload}/> <button in:padding_scaleY={{easingFunc:circOut}} out:_void on:click={upload_files}>upload</button>
<div style:min-height="10px" transition:padding_scaleY /> <div transition:_void={{rTarg:"height",prop:"min-height"}} style:min-height="10px" />
{#if Object.keys(uploads).length > 0} {/if}
<button in:padding_scaleY={{easingFunc:circOut}} out:_void on:click={upload_files}>upload</button>
<div transition:_void style:min-height="10px" /> {:else}
{/if}
{/if} <p transition:_void style:color="#999999" style:text-align="center">Please log in to upload files.</p>
<div transition:_void={{rTarg:"height",prop:"min-height"}} style:min-height="10px" />
<p style:color="#999999" style:text-align="center">
Hosting <span class="number" style:font-weight="600">{ServerStats.files || "•••"}</span> files {/if}
Maximum filesize is <span class="number" style:font-weight="600">{((ServerStats.maxDiscordFileSize || 0)*(ServerStats.maxDiscordFiles || 0))/1048576 || "•••"}MB</span> {/if}
<br />
</p> <p style:color="#999999" style:text-align="center">
Hosting <span class="number" style:font-weight="600">{$serverStats.files || "•••"}</span> files
<p style:color="#999999" style:text-align="center" style:font-size="12px">
Made with {Math.floor(Math.random()*10)==0 ? "🐟" : "❤"} by <a href="https://github.com/nbitzz" style:font-size="12px">@nbitzz</a><a href="https://github.com/nbitzz/monofile" style:font-size="12px">source</a> Maximum filesize is <span class="number" style:font-weight="600">{(($serverStats.maxDiscordFileSize || 0)*($serverStats.maxDiscordFiles || 0))/1048576 || "•••"}MB</span>
</p> <br />
<div style:height="10px" /> </p>
<p style:color="#999999" style:text-align="center" style:font-size="12px">
Made with {Math.floor(Math.random()*10)==0 ? "🐟" : "❤"} by <a href="https://github.com/nbitzz" style:font-size="12px">@nbitzz</a><a href="https://github.com/nbitzz/monofile" style:font-size="12px">source</a>
</p>
<div style:height="10px" />
</div> </div>

View file

@ -1,33 +1,133 @@
<script> <script>
import Pulldown from "./Pulldown.svelte" import Pulldown from "./Pulldown.svelte"
import { padding_scaleY } from "../transition/padding_scaleY" import { padding_scaleY } from "../transition/padding_scaleY"
import { circIn,circOut } from "svelte/easing" import { circIn,circOut } from "svelte/easing"
import { account, fetchAccountData, serverStats } from "../stores.mjs";
let targetAction import { fade } from "svelte/transition";
</script>
let targetAction
<Pulldown name="accounts"> let inProgress
<div class="notLoggedIn"> let authError
<div class="container_div">
<h1>monofile <span style:color="#999999">accounts</span></h1> let pwErr
<p class="flavor">Gain control of your uploads.</p>
// lazy
{#if targetAction}
let username
<div class="fields" out:padding_scaleY|local={{easingFunc:circIn}} in:padding_scaleY|local> let password
<input placeholder="username" type="text">
<input placeholder="password" type="password"> let execute = () => {
<button>{targetAction=="login" ? "Log in" : "Create account"}</button> if (inProgress) return
</div>
inProgress = true
{:else}
fetch(`/auth/${targetAction}`, {
<div class="lgBtnContainer" out:padding_scaleY|local={{easingFunc:circIn}} in:padding_scaleY|local> method: "POST",
<button on:click={() => targetAction="login"}>Log in</button> body: JSON.stringify({
<button on:click={() => targetAction="create"}>Sign up</button> username, password
</div> })
}).then(async (res) => {
{/if} inProgress = false
</div>
</div> if (res.status != 200) {
authError = await res.json().catch(() => {
return {
status: res.status,
message: res.statusText
}
})
}
fetchAccountData();
}).catch(() => {})
}
$: {
if (pwErr && authError) {
pwErr.animate({
backgroundColor: ["#885555","#663333"],
easing: "ease-out"
},650)
}
}
// actual account menu
</script>
<Pulldown name="accounts">
{#if Object.keys($account).length == 0}
<div class="notLoggedIn" transition:fade={{duration:200}}>
<div class="container_div">
<h1>monofile <span style:color="#999999">accounts</span></h1>
<p class="flavor">Gain control of your uploads.</p>
{#if targetAction}
<div class="fields" out:padding_scaleY|local={{easingFunc:circIn}} in:padding_scaleY|local>
{#if !$serverStats.accounts.registrationEnabled && targetAction == "create"}
<div class="pwError">
<div style:background-color="#554C33">
<p>Account registration has been disabled by this instance's owner</p>
</div>
</div>
{/if}
{#if authError}
<div class="pwError" out:padding_scaleY|local={{easingFunc:circIn}} in:padding_scaleY|local>
<div bind:this={pwErr}>
<p><strong>{authError.status}</strong> {authError.message}</p>
</div>
</div>
{/if}
<input placeholder="username" type="text" bind:value={username}>
<input placeholder="password" type="password" bind:value={password}>
<button on:click={execute}>{ inProgress ? "• • •" : (targetAction=="login" ? "Log in" : "Create account") }</button>
</div>
{:else}
<div class="lgBtnContainer" out:padding_scaleY|local={{easingFunc:circIn}} in:padding_scaleY|local>
<button on:click={() => targetAction="login"}>Log in</button>
<button on:click={() => targetAction="create"}>Sign up</button>
</div>
{/if}
</div>
</div>
{:else}
<div class="loggedIn" transition:fade={{duration:200}}>
<h1>
Hey there, <span class="monospace" style:font-size="18px">@{$account.username}</span>
</h1>
<div style:min-height="10px" style:border-bottom="1px solid #AAAAAA" />
<div class="accountOptions">
<button>
<img src="/static/assets/icons/change_password.svg" alt="change password">
<p>Change password</p>
</button>
<button>
<img src="/static/assets/icons/delete_account.svg" alt="delete account">
<p>Delete account</p>
</button>
<button on:click={() => fetch(`/auth/logout`,{method:"POST"}).then(() => fetchAccountData())}>
<img src="/static/assets/icons/logout.svg" alt="logout">
<p>Log out</p>
</button>
</div>
</div>
{/if}
</Pulldown> </Pulldown>

View file

@ -1,31 +1,31 @@
<script> <script>
import Pulldown from "./Pulldown.svelte" import Pulldown from "./Pulldown.svelte"
import { pulldownManager } from "../stores.mjs"; import { pulldownManager } from "../stores.mjs";
</script> </script>
<Pulldown name="files"> <Pulldown name="files">
<div class="notLoggedIn"> <div class="notLoggedIn">
<div style:height="2px" style:background-color="#66AAFF" /> <div style:height="2px" style:background-color="#66AAFF" />
<div style:height="10px" /> <div style:height="10px" />
<p class="flavor">Log in to view uploads & collections</p> <p class="flavor">Log in to view uploads & collections</p>
<button on:click={$pulldownManager.openPulldown("account")}>OK</button> <button on:click={$pulldownManager.openPulldown("account")}>OK</button>
<div style:height="14px" /> <div style:height="14px" />
</div> </div>
<!-- <!--
put scrolling div containing options here put scrolling div containing options here
if not logged in, most options will be hidden if not logged in, most options will be hidden
& the div containing the options will be resized & the div containing the options will be resized
(actually, maybe we could use flexbox for this) (actually, maybe we could use flexbox for this)
--> -->
<!-- <!--
<div> <div>
<h2>Anonymous file deletion</h2> <h2>Anonymous file deletion</h2>
<p>Enter your deletion code</p> <p>Enter your deletion code</p>
<input placeholder="0000 0000 0000 0000"> <input placeholder="0000 0000 0000 0000">
</div> </div>
--> -->
</Pulldown> </Pulldown>

View file

@ -1,25 +1,25 @@
<!-- i'm lazy, could probably just use plain html here but hwatever, mgiht make this grab from the server idk --> <!-- i'm lazy, could probably just use plain html here but hwatever, mgiht make this grab from the server idk -->
<script> <script>
import Pulldown from "./Pulldown.svelte" import Pulldown from "./Pulldown.svelte"
let faq = [ let faq = [
{ {
question : "Are my files compressed on upload?", question : "Are my files compressed on upload?",
answer : "No. Files should stay completely unchanged on download." answer : "No. Files should stay completely unchanged on download."
}, },
{ {
question : "How do I replace a file that I have previously uploaded?", question : "How do I replace a file that I have previously uploaded?",
answer : "You can modify the content of a file that is linked to a file ID by reuploading the file using the same custom ID." answer : "You can modify the content of a file that is linked to a file ID by reuploading the file using the same custom ID."
} }
] ]
</script> </script>
<Pulldown name="help"> <Pulldown name="help">
{#each faq as question} {#each faq as question}
<div class="faqGroup"> <div class="faqGroup">
<h2>{question.question}</h2> <h2>{question.question}</h2>
<p>{question.answer}</p> <p>{question.answer}</p>
</div> </div>
{/each} {/each}
</Pulldown> </Pulldown>

View file

@ -1,14 +1,14 @@
<script> <script>
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
export let name; export let name;
</script> </script>
<div <div
class="pulldown_display" class="pulldown_display"
name={name} name={name}
transition:fade={{duration:150}} transition:fade={{duration:150}}
> >
<slot /> <slot />
</div> </div>

View file

@ -1,3 +1,23 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
export let pulldownManager = writable(0) export let pulldownManager = writable(0)
export let account = writable({})
export let serverStats = writable({})
export let fetchAccountData = function() {
fetch("/auth/me").then(async (response) => {
if (response.status == 200) {
account.set(await response.json())
} else {
account.set({})
}
}).catch((err) => { console.error(err) })
}
export let refresh_stats = () => {
fetch("/server").then(async (data) => {
serverStats.set(await data.json())
}).catch((err) => { console.error(err) })
}
fetchAccountData()

View file

@ -1,20 +1,20 @@
import { circIn, circOut } from "svelte/easing" import { circIn, circOut } from "svelte/easing"
export function _void(node, { duration, easingFunc, op, prop }) { export function _void(node, { duration, easingFunc, op, prop, rTarg }) {
let rect = node.getBoundingClientRect() let rect = node.getBoundingClientRect()
return { return {
duration: duration||300, duration: duration||300,
css: t => { css: t => {
let eased = (easingFunc || circIn)(t) let eased = (easingFunc || circIn)(t)
return ` return `
white-space: nowrap; white-space: nowrap;
${prop||"height"}: ${(eased)*(rect[prop||"height"])}px; ${prop||"height"}: ${(eased)*(rect[rTarg||prop||"height"])}px;
padding: 0px; padding: 0px;
opacity:${eased}; opacity:${eased};
overflow: clip; overflow: clip;
` `
} }
} }
} }

View file

@ -1,18 +1,18 @@
import { circIn, circOut } from "svelte/easing" import { circIn, circOut } from "svelte/easing"
export function padding_scaleY(node, { duration, easingFunc, padY, padX, op }) { export function padding_scaleY(node, { duration, easingFunc, padY, padX, op }) {
let rect = node.getBoundingClientRect() let rect = node.getBoundingClientRect()
return { return {
duration: duration||300, duration: duration||300,
css: t => { css: t => {
let eased = (easingFunc || circOut)(t) let eased = (easingFunc || circOut)(t)
return ` return `
height: ${eased*(rect.height-(padY||0))}px; height: ${eased*(rect.height-(padY||0))}px;
${padX&&padY ? `padding: ${(eased)*(padY)}px ${(padX)}px;` : ""} ${padX&&padY ? `padding: ${(eased)*(padY)}px ${(padX)}px;` : ""}
${op ? `opacity: ${eased};` : ""} ${op ? `opacity: ${eased};` : ""}
` `
} }
} }
} }

View file

@ -1,93 +1,93 @@
<script> <script>
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { circIn, circOut } from "svelte/easing" import { circIn, circOut } from "svelte/easing"
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { _void } from "../transition/_void" import { _void } from "../transition/_void"
let uploadTypes = { let uploadTypes = {
files: 1, files: 1,
clone: 2 clone: 2
} }
let uploadType = undefined let uploadType = undefined
let dispatch = createEventDispatcher(); let dispatch = createEventDispatcher();
// file upload // file upload
/** /**
* @type HTMLInputElement * @type HTMLInputElement
*/ */
let fileUpload; let fileUpload;
$: { $: {
if (fileUpload) { if (fileUpload) {
fileUpload.addEventListener("change",() => { fileUpload.addEventListener("change",() => {
dispatch("addFiles",{ dispatch("addFiles",{
type: "upload", type: "upload",
files: Array.from(fileUpload.files) files: Array.from(fileUpload.files)
}) })
uploadType = undefined uploadType = undefined
}) })
} }
} }
// file clone // file clone
/** /**
* @type HTMLButtonElement * @type HTMLButtonElement
*/ */
let cloneButton; let cloneButton;
/** /**
* @type HTMLInputElement * @type HTMLInputElement
*/ */
let cloneUrlTextbox; let cloneUrlTextbox;
$: { $: {
if (cloneButton && cloneUrlTextbox) { if (cloneButton && cloneUrlTextbox) {
cloneButton.addEventListener("click",() => { cloneButton.addEventListener("click",() => {
if (cloneUrlTextbox.value) { if (cloneUrlTextbox.value) {
dispatch("addFiles",{ dispatch("addFiles",{
type: "clone", type: "clone",
url: cloneUrlTextbox.value url: cloneUrlTextbox.value
}) })
uploadType = undefined; uploadType = undefined;
} else { } else {
cloneUrlTextbox.animate([ cloneUrlTextbox.animate([
{"transform":"translateX(0px)"}, {"transform":"translateX(0px)"},
{"transform":"translateX(-3px)"}, {"transform":"translateX(-3px)"},
{"transform":"translateX(3px)"}, {"transform":"translateX(3px)"},
{"transform":"translateX(0px)"} {"transform":"translateX(0px)"}
],100) ],100)
} }
}) })
} }
} }
</script> </script>
<!-- there are 100% better ways to do this but idgaf, it's still easier to manage than <1.3 lmao --> <!-- there are 100% better ways to do this but idgaf, it's still easier to manage than <1.3 lmao -->
<div id="add_new_files" transition:_void={{duration:200}}> <div id="add_new_files" transition:_void={{duration:200}}>
<p> <p>
+<span class="_add_files_txt">add files</span> +<span class="_add_files_txt">add files</span>
</p> </p>
{#if !uploadType} {#if !uploadType}
<div id="file_add_btns" out:_void in:_void={{easingFunc:circOut}}> <div id="file_add_btns" out:_void in:_void={{easingFunc:circOut}}>
<button on:click={() => uploadType = uploadTypes.files} >upload files...</button> <button on:click={() => uploadType = uploadTypes.files} >upload files...</button>
<button on:click={() => uploadType = uploadTypes.clone} >clone url...</button> <button on:click={() => uploadType = uploadTypes.clone} >clone url...</button>
</div> </div>
{:else} {:else}
{#if uploadType == uploadTypes.files} {#if uploadType == uploadTypes.files}
<div id="file_add_btns" out:_void in:_void={{easingFunc:circOut}}> <div id="file_add_btns" out:_void in:_void={{easingFunc:circOut}}>
<div class="fileUpload"> <div class="fileUpload">
<p>click/tap to browse<br/>or drag files into this box</p> <p>click/tap to browse<br/>or drag files into this box</p>
<input type="file" multiple bind:this={fileUpload}> <input type="file" multiple bind:this={fileUpload}>
</div> </div>
</div> </div>
{:else if uploadType == uploadTypes.clone} {:else if uploadType == uploadTypes.clone}
<div id="file_add_btns" out:_void in:_void={{easingFunc:circOut}}> <div id="file_add_btns" out:_void in:_void={{easingFunc:circOut}}>
<input placeholder="url" type="text" bind:this={cloneUrlTextbox}> <input placeholder="url" type="text" bind:this={cloneUrlTextbox}>
<button style:flex-basis="30%" bind:this={cloneButton}>add file</button> <button style:flex-basis="30%" bind:this={cloneButton}>add file</button>
</div> </div>
{/if} {/if}
{/if} {/if}
</div> </div>

View file

@ -1,104 +1,104 @@
{ {
"include":["src/server/**/*"], "include":["src/server/**/*"],
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */ /* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */ /* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */ // "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */ /* Modules */
"module": "commonjs", /* Specify what module code is generated. */ "module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./src/", /* Specify the root folder within your source files. */ // "rootDir": "./src/", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */ // "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */ /* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */ /* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./out/server", /* Specify an output folder for all emitted files. */ "outDir": "./out/server", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */ // "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */ // "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */ /* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */ /* Type Checking */
"strict": true, /* Enable all strict type-checking options. */ "strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */ /* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */
} }
} }