this code SUUUUCKs but i dont CARE

This commit is contained in:
split / 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
.env
.data
node_modules
.env
.data
out

44
.vscode/tasks.json vendored
View file

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

46
LICENSE
View file

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

View file

@ -1,33 +1,33 @@
# monofile
File sharing via Discord
<br>
## .env
```
TOKEN=KILL-YOURSELF.NOW
```
## versions & planned updates
- [X] 1.0.0 initial release
- [X] 1.1.0 add file cloning endpoint
- [X] 1.1.1 add file cloning webpage
- [X] 1.1.2 fix file cloning with binary data
- [X] 1.1.3 display current version on pages
- [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.1 add file counter to main page
- [X] 1.2.2 clean up this shitty code
- [X] 1.2.3 bugfixes
- [ ] 1.3.0 new ui; collections; accounts; utility endpoints; multi file uploads
- [ ] 1.3.1 self-destructing files
- [ ] 1.3.2 disable cloning of local ips
- [ ] 1.4.0 admin panel
- [ ] 2.0.0 rewrite using theUnfunny's code as a base/rewrite using monofile-core
also todo: monofile-core (written in eris)
## Disclaimer!
# monofile
File sharing via Discord
<br>
## .env
```
TOKEN=KILL-YOURSELF.NOW
```
## versions & planned updates
- [X] 1.0.0 initial release
- [X] 1.1.0 add file cloning endpoint
- [X] 1.1.1 add file cloning webpage
- [X] 1.1.2 fix file cloning with binary data
- [X] 1.1.3 display current version on pages
- [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.1 add file counter to main page
- [X] 1.2.2 clean up this shitty code
- [X] 1.2.3 bugfixes
- [ ] 1.3.0 new ui; collections; accounts; utility endpoints; multi file uploads
- [ ] 1.3.1 self-destructing files
- [ ] 1.3.2 disable cloning of local ips
- [ ] 1.4.0 admin panel
- [ ] 2.0.0 rewrite using theUnfunny's code as a base/rewrite using monofile-core
also todo: monofile-core (written in eris)
## 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.

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,131 +1,100 @@
import crypto from "crypto"
import * as auth from "./auth";
import { readFile, writeFile } from "fs/promises"
// this is probably horrible
// but i don't even care anymore
export let Accounts: Account[] = []
export interface Account {
id : string
username: string
password: {
hash: string
salt: string
}
accounts: string[]
admin : boolean
}
export function create(username:string,pwd:string,admin:boolean=false) {
let accId = crypto.randomBytes(12).toString("hex")
Accounts.push(
{
id: accId,
username: username,
password: password.hash(pwd),
accounts: [],
admin: admin
}
)
save()
return accId
}
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 getFromToken(token:string) {
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 namespace password {
export function hash(password:string,_salt?:string) {
let salt = _salt || crypto.randomBytes(12).toString('base64')
let hash = crypto.createHash('sha256').update(`${salt}${password}`).digest('hex')
return {
salt:salt,
hash:hash
}
}
export function set(id:string,password:string) {
let acc = Accounts.find(e => e.id == id)
if (!acc) return
acc.password = hash(password)
save()
}
export function check(id:string,password:string) {
let acc = Accounts.find(e => e.id == id)
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)
if (!acc) return
/* check for account that already has name */
let idx = acc.accounts.findIndex(e=>e==name)
if (idx > -1) return
acc.accounts = [...acc.accounts,name]
save()
return
}
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)
}
import crypto from "crypto"
import * as auth from "./auth";
import { readFile, writeFile } from "fs/promises"
// this is probably horrible
// but i don't even care anymore
export let Accounts: Account[] = []
export interface Account {
id : string
username : string
password : {
hash : string
salt : string
}
files : string[]
collections : string[]
admin : boolean
}
export function create(username:string,pwd:string,admin:boolean=false) {
let accId = crypto.randomBytes(12).toString("hex")
Accounts.push(
{
id: accId,
username: username,
password: password.hash(pwd),
files: [],
collections: [],
admin: admin
}
)
save()
return accId
}
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 getFromToken(token:string) {
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 namespace password {
export function hash(password:string,_salt?:string) {
let salt = _salt || crypto.randomBytes(12).toString('base64')
let hash = crypto.createHash('sha256').update(`${salt}${password}`).digest('hex')
return {
salt:salt,
hash:hash
}
}
export function set(id:string,password:string) {
let acc = Accounts.find(e => e.id == id)
if (!acc) return
acc.password = hash(password)
save()
}
export function check(id:string,password:string) {
let acc = Accounts.find(e => e.id == id)
if (!acc) return
return acc.password.hash == hash(password,acc.password.salt).hash
}
}
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 { readFile, writeFile } from "fs/promises"
export let AuthTokens: AuthToken[] = []
export let AuthTokenTO:{[key:string]:NodeJS.Timeout} = {}
export interface AuthToken {
account: string,
token: string,
expire: number
}
export function create(id:string,expire:number=(24*60*60*1000)) {
let token = {
account:id,
token:crypto.randomBytes(12).toString('hex'),
expire:Date.now()+expire
}
AuthTokens.push(token)
tokenTimer(token)
save()
return token.token
}
export function validate(token:string) {
return AuthTokens.find(e => e.token == token && Date.now() < e.expire)?.account
}
export function tokenTimer(token:AuthToken) {
if (Date.now() >= token.expire) {
invalidate(token.token)
return
}
AuthTokenTO[token.token] = setTimeout(() => invalidate(token.token),token.expire-Date.now())
}
export function invalidate(token:string) {
if (AuthTokenTO[token]) {
clearTimeout(AuthTokenTO[token])
}
AuthTokens.splice(AuthTokens.findIndex(e => e.token == token),1)
save()
}
export function save() {
writeFile(`${process.cwd()}/.data/tokens.json`,JSON.stringify(AuthTokens))
.catch((err) => console.error(err))
}
readFile(`${process.cwd()}/.data/tokens.json`)
.then((buf) => {
AuthTokens = JSON.parse(buf.toString())
AuthTokens.forEach(e => tokenTimer(e))
import crypto from "crypto"
import { readFile, writeFile } from "fs/promises"
export let AuthTokens: AuthToken[] = []
export let AuthTokenTO:{[key:string]:NodeJS.Timeout} = {}
export interface AuthToken {
account: string,
token: string,
expire: number
}
export function create(id:string,expire:number=(24*60*60*1000)) {
let token = {
account:id,
token:crypto.randomBytes(12).toString('hex'),
expire:Date.now()+expire
}
AuthTokens.push(token)
tokenTimer(token)
save()
return token.token
}
export function validate(token:string) {
return AuthTokens.find(e => e.token == token && Date.now() < e.expire)?.account
}
export function tokenTimer(token:AuthToken) {
if (Date.now() >= token.expire) {
invalidate(token.token)
return
}
AuthTokenTO[token.token] = setTimeout(() => invalidate(token.token),token.expire-Date.now())
}
export function invalidate(token:string) {
if (AuthTokenTO[token]) {
clearTimeout(AuthTokenTO[token])
}
AuthTokens.splice(AuthTokens.findIndex(e => e.token == token),1)
save()
}
export function save() {
writeFile(`${process.cwd()}/.data/tokens.json`,JSON.stringify(AuthTokens))
.catch((err) => console.error(err))
}
readFile(`${process.cwd()}/.data/tokens.json`)
.then((buf) => {
AuthTokens = JSON.parse(buf.toString())
AuthTokens.forEach(e => tokenTimer(e))
}).catch(err => console.error(err))

View file

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

View file

@ -1,256 +1,270 @@
import axios from "axios";
import Discord, { Client, TextBasedChannel } from "discord.js";
import { readFile, writeFile } from "fs";
import { Readable } from "node:stream"
export let id_check_regex = /[A-Za-z0-9_\-\.]+/
export let alphanum = Array.from("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
// bad solution but whatever
export function generateFileId() {
let fid = ""
for (let i = 0; i < 5; i++) {
fid += alphanum[Math.floor(Math.random()*alphanum.length)]
}
return fid
}
export interface FileUploadSettings {
name?: string,
mime: string,
uploadId?: string
}
export interface Configuration {
maxDiscordFiles: number,
maxDiscordFileSize: number,
targetGuild: string,
targetChannel: string,
requestTimeout: number
}
export interface FilePointer {
filename:string,
mime:string,
messageids:string[]
}
export interface StatusCodeError {
status: number,
message: string
}
/* */
export default class Files {
config: Configuration
client: Client
files: {[key:string]:FilePointer} = {}
uploadChannel?: TextBasedChannel
constructor(client: Client, config: Configuration) {
this.config = config;
this.client = client;
client.on("ready",() => {
console.log("Discord OK!")
client.guilds.fetch(config.targetGuild).then((g) => {
g.channels.fetch(config.targetChannel).then((a) => {
if (a?.isTextBased()) {
this.uploadChannel = a
}
})
})
})
readFile(process.cwd()+"/.data/files.json",(err,buf) => {
if (err) {console.log(err);return}
this.files = JSON.parse(buf.toString() || "{}")
})
}
uploadFile(settings:FileUploadSettings,fBuffer:Buffer):Promise<string|StatusCodeError> {
return new Promise<string>(async (resolve,reject) => {
if (!this.uploadChannel) {
reject({status:503,message:"server is not ready - please try again later"})
return
}
if (!settings.name || !settings.mime) {
reject({status:400,message:"missing name/mime"});
return
}
let uploadId = (settings.uploadId || generateFileId()).toString();
if ((uploadId.match(id_check_regex) || [])[0] != uploadId || uploadId.length > 30) {
reject({status:400,message:"invalid id"});return
}
if (this.files[uploadId]) {
reject({status:400,message:"a file with this id already exists"});
return
}
if (settings.name.length > 128) {
reject({status:400,message:"name too long"});
return
}
if (settings.mime.length > 128) {
reject({status:400,message:"mime too long"});
return
}
// get buffer
if (fBuffer.byteLength >= (this.config.maxDiscordFileSize*this.config.maxDiscordFiles)) {
reject({status:400,message:"file too large"});
return
}
// generate buffers to upload
let toUpload = []
for (let i = 0; i < Math.ceil(fBuffer.byteLength/this.config.maxDiscordFileSize); i++) {
toUpload.push(
fBuffer.subarray(
i*this.config.maxDiscordFileSize,
Math.min(
fBuffer.byteLength,
(i+1)*this.config.maxDiscordFileSize
)
)
)
}
// begin uploading
let uploadTmplt:Discord.AttachmentBuilder[] = toUpload.map((e) => {
return new Discord.AttachmentBuilder(e)
.setName(Math.random().toString().slice(2))
})
let uploadGroups = []
for (let i = 0; i < Math.ceil(uploadTmplt.length/10); i++) {
uploadGroups.push(uploadTmplt.slice(i*10,((i+1)*10)))
}
let msgIds = []
for (let i = 0; i < uploadGroups.length; i++) {
let ms = await this.uploadChannel.send({
files:uploadGroups[i]
}).catch((e) => {console.error(e)})
if (ms) {
msgIds.push(ms.id)
} else {
reject({status:500,message:"please try again"}); return
}
}
// save
resolve(await this.writeFile(
uploadId,
{
filename:settings.name,
messageids:msgIds,
mime:settings.mime
}
))
})
}
// fs
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) => {
if (err) {
reject({status:500,message:"please try again"});
delete this.files[uploadId];
return
}
resolve(uploadId)
})
})
}
// todo: move read code here
readFileStream(uploadId: string):Promise<{dataStream:Readable,contentType:string}> {
return new Promise(async (resolve,reject) => {
if (!this.uploadChannel) {
reject({status:503,message:"server is not ready - please try again later"})
return
}
if (this.files[uploadId]) {
let file = this.files[uploadId]
let dataStream = new Readable({
read(){}
})
resolve({
contentType: file.mime,
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) {
let attach = Array.from(msg.attachments.values())
for (let i = 0; i < attach.length; i++) {
let d = await axios.get(attach[i].url,{responseType:"arraybuffer"}).catch((e:Error) => {console.error(e)})
if (d) {
dataStream.push(d.data)
} else {
reject({status:500,message:"internal server error"})
dataStream.destroy(new Error("file read error"))
return
}
}
}
}
dataStream.push(null)
} else {
reject({status:404,message:"not found"})
}
})
}
unlink(uploadId:string):Promise<void> {
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
reject()
} else {
resolve()
}
})
})
}
getFilePointer(uploadId:string):FilePointer {
return this.files[uploadId]
}
}
import axios from "axios";
import Discord, { Client, TextBasedChannel } from "discord.js";
import { readFile, writeFile } from "fs";
import { Readable } from "node:stream"
export let id_check_regex = /[A-Za-z0-9_\-\.]+/
export let alphanum = Array.from("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
// bad solution but whatever
export function generateFileId() {
let fid = ""
for (let i = 0; i < 5; i++) {
fid += alphanum[Math.floor(Math.random()*alphanum.length)]
}
return fid
}
export interface FileUploadSettings {
name?: string,
mime: string,
uploadId?: string,
owner?:string
}
export interface Configuration {
maxDiscordFiles: number,
maxDiscordFileSize: number,
targetGuild: string,
targetChannel: string,
requestTimeout: number,
accounts: {
registrationEnabled: boolean,
requiredForUpload: boolean
}
}
export interface FilePointer {
filename:string,
mime:string,
messageids:string[],
owner?:string
}
export interface StatusCodeError {
status: number,
message: string
}
/* */
export default class Files {
config: Configuration
client: Client
files: {[key:string]:FilePointer} = {}
uploadChannel?: TextBasedChannel
constructor(client: Client, config: Configuration) {
this.config = config;
this.client = client;
client.on("ready",() => {
console.log("Discord OK!")
client.guilds.fetch(config.targetGuild).then((g) => {
g.channels.fetch(config.targetChannel).then((a) => {
if (a?.isTextBased()) {
this.uploadChannel = a
}
})
})
})
readFile(process.cwd()+"/.data/files.json",(err,buf) => {
if (err) {console.log(err);return}
this.files = JSON.parse(buf.toString() || "{}")
})
}
uploadFile(settings:FileUploadSettings,fBuffer:Buffer):Promise<string|StatusCodeError> {
return new Promise<string>(async (resolve,reject) => {
if (!this.uploadChannel) {
reject({status:503,message:"server is not ready - please try again later"})
return
}
if (!settings.name || !settings.mime) {
reject({status:400,message:"missing name/mime"});
return
}
if (!settings.owner && this.config.accounts.requiredForUpload) {
reject({status:401,message:"an account is required for upload"});
return
}
let uploadId = (settings.uploadId || generateFileId()).toString();
if ((uploadId.match(id_check_regex) || [])[0] != uploadId || uploadId.length > 30) {
reject({status:400,message:"invalid id"});return
}
if (this.files[uploadId]) {
reject({status:400,message:"a file with this id already exists"});
return
}
if (settings.name.length > 128) {
reject({status:400,message:"name too long"});
return
}
if (settings.mime.length > 128) {
reject({status:400,message:"mime too long"});
return
}
// get buffer
if (fBuffer.byteLength >= (this.config.maxDiscordFileSize*this.config.maxDiscordFiles)) {
reject({status:400,message:"file too large"});
return
}
// generate buffers to upload
let toUpload = []
for (let i = 0; i < Math.ceil(fBuffer.byteLength/this.config.maxDiscordFileSize); i++) {
toUpload.push(
fBuffer.subarray(
i*this.config.maxDiscordFileSize,
Math.min(
fBuffer.byteLength,
(i+1)*this.config.maxDiscordFileSize
)
)
)
}
// begin uploading
let uploadTmplt:Discord.AttachmentBuilder[] = toUpload.map((e) => {
return new Discord.AttachmentBuilder(e)
.setName(Math.random().toString().slice(2))
})
let uploadGroups = []
for (let i = 0; i < Math.ceil(uploadTmplt.length/10); i++) {
uploadGroups.push(uploadTmplt.slice(i*10,((i+1)*10)))
}
let msgIds = []
for (let i = 0; i < uploadGroups.length; i++) {
let ms = await this.uploadChannel.send({
files:uploadGroups[i]
}).catch((e) => {console.error(e)})
if (ms) {
msgIds.push(ms.id)
} else {
reject({status:500,message:"please try again"}); return
}
}
// save
resolve(await this.writeFile(
uploadId,
{
filename:settings.name,
messageids:msgIds,
mime:settings.mime,
owner:settings.owner
}
))
})
}
// fs
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) => {
if (err) {
reject({status:500,message:"please try again"});
delete this.files[uploadId];
return
}
resolve(uploadId)
})
})
}
// todo: move read code here
readFileStream(uploadId: string):Promise<{dataStream:Readable,contentType:string}> {
return new Promise(async (resolve,reject) => {
if (!this.uploadChannel) {
reject({status:503,message:"server is not ready - please try again later"})
return
}
if (this.files[uploadId]) {
let file = this.files[uploadId]
let dataStream = new Readable({
read(){}
})
resolve({
contentType: file.mime,
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) {
let attach = Array.from(msg.attachments.values())
for (let i = 0; i < attach.length; i++) {
let d = await axios.get(attach[i].url,{responseType:"arraybuffer"}).catch((e:Error) => {console.error(e)})
if (d) {
dataStream.push(d.data)
} else {
reject({status:500,message:"internal server error"})
dataStream.destroy(new Error("file read error"))
return
}
}
}
}
dataStream.push(null)
} else {
reject({status:404,message:"not found"})
}
})
}
unlink(uploadId:string):Promise<void> {
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
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
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');
$FallbackFonts:
-apple-system,
system-ui,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
%normal {
font-family: "Source Sans Pro", $FallbackFonts
}
/*
everything that's not a span
and/or has the normal class
(it's just in case)
*/
*:not(span), .normal { @extend %normal; }
/*
for code blocks / terminal
*/
.monospace {
font-family: "Fira Code", monospace
}
/*
colors
*/
$Background: #252525;
/* hsl(210,12.9,24.3) */
$darkish: rgb(54, 62, 70);
/*
then other stuff
*/
body {
background-color: rgb(30, 33, 36); // this is here so that
// pulling down to refresh
// on mobile looks good
}
#appContent {
background-color: $Background
}
/*
scrollbars
*/
* {
/* nice scrollbars aren't needed on mobile so */
@media screen and (min-width:500px) {
&::-webkit-scrollbar {
width:5px;
}
&::-webkit-scrollbar-track {
background-color:#191919;
}
&::-webkit-scrollbar-thumb {
background-color:#333;
&:hover {
background-color:#373737;
}
}
}
/*
could probably replace this with fonts served directly
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');
$FallbackFonts:
-apple-system,
system-ui,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
%normal {
font-family: "Source Sans Pro", $FallbackFonts
}
/*
everything that's not a span
and/or has the normal class
(it's just in case)
*/
*:not(span), .normal { @extend %normal; }
/*
for code blocks / terminal
*/
.monospace {
font-family: "Fira Code", monospace
}
/*
colors
*/
$Background: #252525;
/* hsl(210,12.9,24.3) */
$darkish: rgb(54, 62, 70);
/*
then other stuff
*/
body {
background-color: rgb(30, 33, 36); // this is here so that
// pulling down to refresh
// on mobile looks good
}
#appContent {
background-color: $Background
}
/*
scrollbars
*/
* {
/* nice scrollbars aren't needed on mobile so */
@media screen and (min-width:500px) {
&::-webkit-scrollbar {
width:5px;
}
&::-webkit-scrollbar-track {
background-color:#191919;
}
&::-webkit-scrollbar-thumb {
background-color:#333;
&:hover {
background-color:#373737;
}
}
}
}

View file

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

View file

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

View file

@ -1,120 +1,188 @@
.pulldown_display[name=accounts] {
.notLoggedIn {
.container_div {
position:absolute;
top:50%;
transform:translateY(-50%);
width:100%;
text-align:center;
h1 {
font-weight:600;
font-size:24px;
@media screen and (max-width:500px) {
font-size:30px;
}
}
.flavor {
font-size:14px;
/* good enoough */
@media screen and (max-width:500px) {
font-size:16px;
}
color:#999999;
margin: 0 0 10px 0;
}
button {
cursor:pointer;
background-color:#393939;
color:#DDDDDD;
border:none;
outline:none;
padding:5px;
transition-duration: 250ms;
/*overflow:clip;*/
@media screen and (max-width: 500px) {
font-size:16px;
padding:10px;
}
&:hover {
transition-duration: 250ms;
background-color:#434343;
color: #ffffff;
}
flex-basis:50%;
flex-grow:1;
}
input[type=text],input[type=password] {
border:none;
border-radius:0;
width:100%;
padding:5px;
background-color:#333333;
color:#dddddd;
outline:none;
@media screen and (max-width: 500px) {
font-size:16px;
padding:10px;
}
}
.lgBtnContainer {
display:flex;
position:relative;
left:20px;
width:calc( 100% - 40px );
gap:10px;
overflow:clip;
}
.fields {
display:flex;
flex-direction:column;
position:relative;
left:20px;
width:calc( 100% - 40px );
gap:5px;
overflow:clip;
}
/*
a {
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;
}
}
}
*/
}
}
.pulldown_display[name=accounts] {
.notLoggedIn {
.container_div {
position:absolute;
top:50%;
transform:translateY(-50%);
width:100%;
text-align:center;
h1 {
font-weight:600;
font-size:24px;
@media screen and (max-width:500px) {
font-size:30px;
}
}
.flavor {
font-size:14px;
/* good enoough */
@media screen and (max-width:500px) {
font-size:16px;
}
color:#999999;
margin: 0 0 10px 0;
}
button {
cursor:pointer;
background-color:#393939;
color:#DDDDDD;
border:none;
outline:none;
padding:5px;
transition-duration: 250ms;
/*overflow:clip;*/
@media screen and (max-width: 500px) {
font-size:16px;
padding:10px;
}
&:hover {
transition-duration: 250ms;
background-color:#434343;
color: #ffffff;
}
flex-basis:50%;
flex-grow:1;
}
input[type=text],input[type=password] {
border:none;
border-radius:0;
width:100%;
padding:5px;
background-color:#333333;
color:#dddddd;
outline:none;
@media screen and (max-width: 500px) {
font-size:16px;
padding:10px;
}
}
.pwError {
div {
border:none;
border-radius:0;
width:100%;
padding:5px;
background-color:#663333;
color:#dddddd;
outline:none;
font-size:14px;
text-align:left;
@media screen and (max-width: 500px) {
font-size:16px;
padding:10px;
}
}
}
.lgBtnContainer {
display:flex;
position:relative;
left:20px;
width:calc( 100% - 40px );
gap:10px;
overflow:clip;
}
.fields {
display:flex;
flex-direction:column;
position:relative;
left:20px;
width:calc( 100% - 40px );
gap:5px;
overflow:clip;
}
/*
a {
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] {
.notLoggedIn {
position:absolute;
top:100%;
transform:translateY(-100%);
width:100%;
text-align:center;
background-color:#202020;
.flavor {
font-size:16px;
color:#999999;
margin: 0 0 10px 0;
}
button {
--col: #999999;
background-color: #232323;
color:var(--col);
font-size:14px;
border:1px solid var(--col);
padding:2px 20px 2px 20px;
cursor:pointer;
transition-duration:250ms;
&:hover {
background-color:#333333;
transition-duration:250ms;
--col:#BBBBBB;
}
}
}
.pulldown_display[name=files] {
.notLoggedIn {
position:absolute;
top:100%;
transform:translateY(-100%);
width:100%;
text-align:center;
background-color:#202020;
.flavor {
font-size:16px;
color:#999999;
margin: 0 0 10px 0;
}
button {
--col: #999999;
background-color: #232323;
color:var(--col);
font-size:14px;
border:1px solid var(--col);
padding:2px 20px 2px 20px;
cursor:pointer;
transition-duration:250ms;
&:hover {
background-color:#333333;
transition-duration:250ms;
--col:#BBBBBB;
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,33 +1,133 @@
<script>
import Pulldown from "./Pulldown.svelte"
import { padding_scaleY } from "../transition/padding_scaleY"
import { circIn,circOut } from "svelte/easing"
let targetAction
</script>
<Pulldown name="accounts">
<div class="notLoggedIn">
<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>
<input placeholder="username" type="text">
<input placeholder="password" type="password">
<button>{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>
<script>
import Pulldown from "./Pulldown.svelte"
import { padding_scaleY } from "../transition/padding_scaleY"
import { circIn,circOut } from "svelte/easing"
import { account, fetchAccountData, serverStats } from "../stores.mjs";
import { fade } from "svelte/transition";
let targetAction
let inProgress
let authError
let pwErr
// lazy
let username
let password
let execute = () => {
if (inProgress) return
inProgress = true
fetch(`/auth/${targetAction}`, {
method: "POST",
body: JSON.stringify({
username, password
})
}).then(async (res) => {
inProgress = false
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>

View file

@ -1,31 +1,31 @@
<script>
import Pulldown from "./Pulldown.svelte"
import { pulldownManager } from "../stores.mjs";
</script>
<Pulldown name="files">
<div class="notLoggedIn">
<div style:height="2px" style:background-color="#66AAFF" />
<div style:height="10px" />
<p class="flavor">Log in to view uploads & collections</p>
<button on:click={$pulldownManager.openPulldown("account")}>OK</button>
<div style:height="14px" />
</div>
<!--
put scrolling div containing options here
if not logged in, most options will be hidden
& the div containing the options will be resized
(actually, maybe we could use flexbox for this)
-->
<!--
<div>
<h2>Anonymous file deletion</h2>
<p>Enter your deletion code</p>
<input placeholder="0000 0000 0000 0000">
</div>
-->
<script>
import Pulldown from "./Pulldown.svelte"
import { pulldownManager } from "../stores.mjs";
</script>
<Pulldown name="files">
<div class="notLoggedIn">
<div style:height="2px" style:background-color="#66AAFF" />
<div style:height="10px" />
<p class="flavor">Log in to view uploads & collections</p>
<button on:click={$pulldownManager.openPulldown("account")}>OK</button>
<div style:height="14px" />
</div>
<!--
put scrolling div containing options here
if not logged in, most options will be hidden
& the div containing the options will be resized
(actually, maybe we could use flexbox for this)
-->
<!--
<div>
<h2>Anonymous file deletion</h2>
<p>Enter your deletion code</p>
<input placeholder="0000 0000 0000 0000">
</div>
-->
</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 -->
<script>
import Pulldown from "./Pulldown.svelte"
let faq = [
{
question : "Are my files compressed on upload?",
answer : "No. Files should stay completely unchanged on download."
},
{
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."
}
]
</script>
<Pulldown name="help">
{#each faq as question}
<div class="faqGroup">
<h2>{question.question}</h2>
<p>{question.answer}</p>
</div>
{/each}
<!-- i'm lazy, could probably just use plain html here but hwatever, mgiht make this grab from the server idk -->
<script>
import Pulldown from "./Pulldown.svelte"
let faq = [
{
question : "Are my files compressed on upload?",
answer : "No. Files should stay completely unchanged on download."
},
{
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."
}
]
</script>
<Pulldown name="help">
{#each faq as question}
<div class="faqGroup">
<h2>{question.question}</h2>
<p>{question.answer}</p>
</div>
{/each}
</Pulldown>

View file

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

View file

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

View file

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

View file

@ -1,93 +1,93 @@
<script>
import { createEventDispatcher } from "svelte";
import { circIn, circOut } from "svelte/easing"
import { fade } from "svelte/transition";
import { _void } from "../transition/_void"
let uploadTypes = {
files: 1,
clone: 2
}
let uploadType = undefined
let dispatch = createEventDispatcher();
// file upload
/**
* @type HTMLInputElement
*/
let fileUpload;
$: {
if (fileUpload) {
fileUpload.addEventListener("change",() => {
dispatch("addFiles",{
type: "upload",
files: Array.from(fileUpload.files)
})
uploadType = undefined
})
}
}
// file clone
/**
* @type HTMLButtonElement
*/
let cloneButton;
/**
* @type HTMLInputElement
*/
let cloneUrlTextbox;
$: {
if (cloneButton && cloneUrlTextbox) {
cloneButton.addEventListener("click",() => {
if (cloneUrlTextbox.value) {
dispatch("addFiles",{
type: "clone",
url: cloneUrlTextbox.value
})
uploadType = undefined;
} else {
cloneUrlTextbox.animate([
{"transform":"translateX(0px)"},
{"transform":"translateX(-3px)"},
{"transform":"translateX(3px)"},
{"transform":"translateX(0px)"}
],100)
}
})
}
}
</script>
<!-- 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}}>
<p>
+<span class="_add_files_txt">add files</span>
</p>
{#if !uploadType}
<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.clone} >clone url...</button>
</div>
{:else}
{#if uploadType == uploadTypes.files}
<div id="file_add_btns" out:_void in:_void={{easingFunc:circOut}}>
<div class="fileUpload">
<p>click/tap to browse<br/>or drag files into this box</p>
<input type="file" multiple bind:this={fileUpload}>
</div>
</div>
{:else if uploadType == uploadTypes.clone}
<div id="file_add_btns" out:_void in:_void={{easingFunc:circOut}}>
<input placeholder="url" type="text" bind:this={cloneUrlTextbox}>
<button style:flex-basis="30%" bind:this={cloneButton}>add file</button>
</div>
{/if}
{/if}
<script>
import { createEventDispatcher } from "svelte";
import { circIn, circOut } from "svelte/easing"
import { fade } from "svelte/transition";
import { _void } from "../transition/_void"
let uploadTypes = {
files: 1,
clone: 2
}
let uploadType = undefined
let dispatch = createEventDispatcher();
// file upload
/**
* @type HTMLInputElement
*/
let fileUpload;
$: {
if (fileUpload) {
fileUpload.addEventListener("change",() => {
dispatch("addFiles",{
type: "upload",
files: Array.from(fileUpload.files)
})
uploadType = undefined
})
}
}
// file clone
/**
* @type HTMLButtonElement
*/
let cloneButton;
/**
* @type HTMLInputElement
*/
let cloneUrlTextbox;
$: {
if (cloneButton && cloneUrlTextbox) {
cloneButton.addEventListener("click",() => {
if (cloneUrlTextbox.value) {
dispatch("addFiles",{
type: "clone",
url: cloneUrlTextbox.value
})
uploadType = undefined;
} else {
cloneUrlTextbox.animate([
{"transform":"translateX(0px)"},
{"transform":"translateX(-3px)"},
{"transform":"translateX(3px)"},
{"transform":"translateX(0px)"}
],100)
}
})
}
}
</script>
<!-- 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}}>
<p>
+<span class="_add_files_txt">add files</span>
</p>
{#if !uploadType}
<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.clone} >clone url...</button>
</div>
{:else}
{#if uploadType == uploadTypes.files}
<div id="file_add_btns" out:_void in:_void={{easingFunc:circOut}}>
<div class="fileUpload">
<p>click/tap to browse<br/>or drag files into this box</p>
<input type="file" multiple bind:this={fileUpload}>
</div>
</div>
{:else if uploadType == uploadTypes.clone}
<div id="file_add_btns" out:_void in:_void={{easingFunc:circOut}}>
<input placeholder="url" type="text" bind:this={cloneUrlTextbox}>
<button style:flex-basis="30%" bind:this={cloneButton}>add file</button>
</div>
{/if}
{/if}
</div>

View file

@ -1,104 +1,104 @@
{
"include":["src/server/**/*"],
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* 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. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "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. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"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. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "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'. */
// "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*'. */
// "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. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./src/", /* Specify the root folder within your source files. */
// "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. */
// "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. */
// "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. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "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. */
/* JavaScript Support */
// "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. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not 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. */
"outDir": "./out/server", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "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. */
// "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. */
// "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. */
// "inlineSourceMap": true, /* Include sourcemap files 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. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "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. */
"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. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "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'. */
// "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. */
// "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'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren'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'. */
// "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. */
// "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. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
{
"include":["src/server/**/*"],
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* 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. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "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. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"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. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "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'. */
// "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*'. */
// "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. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./src/", /* Specify the root folder within your source files. */
// "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. */
// "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. */
// "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. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "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. */
/* JavaScript Support */
// "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. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not 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. */
"outDir": "./out/server", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "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. */
// "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. */
// "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. */
// "inlineSourceMap": true, /* Include sourcemap files 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. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "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. */
"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. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "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'. */
// "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. */
// "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'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren'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'. */
// "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. */
// "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. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}