mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-25 07:06:25 -08:00
work on the
This commit is contained in:
parent
74e18d3d9c
commit
c911896f6d
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
|
@ -3,7 +3,7 @@
|
|||
"tasks": [
|
||||
{
|
||||
"type": "shell",
|
||||
"command":"npx tsc",
|
||||
"command":"tsc\nsass src/style:out/style\nrollup -c",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
|
@ -12,7 +12,7 @@
|
|||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command":"npx tsc\nnode ./out/index.js\ndel ./out/* -Recurse",
|
||||
"command":"tsc\nsass src/style:out/style\nrollup -c\nnpx tsc\nnode ./out/index.js\ndel ./out/* -Recurse",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
|
|
|
@ -22,7 +22,8 @@ TOKEN=KILL-YOURSELF.NOW
|
|||
- [X] 1.2.2 clean up this shitty code
|
||||
- [X] 1.2.3 bugfixes
|
||||
- [ ] 1.3.0 new ui; accounts
|
||||
- [ ] 1.3.1 disable cloning of local ips
|
||||
- [ ] 1.3.1 add utility endpoints: `/embed/:fileId` for discord, `/:fileId` for quick access
|
||||
- [ ] 1.3.3 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
|
||||
|
||||
|
|
747
package-lock.json
generated
747
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -24,5 +24,12 @@
|
|||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,72 +1,34 @@
|
|||
<!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">
|
||||
|
||||
<meta name="og:site_name" content="monofile $Version">
|
||||
<meta name="application-name" content="$ErrorCode">
|
||||
<meta name="description" content="$ErrorMessage">
|
||||
<head>
|
||||
|
||||
<title>monofile</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/style/error.css"
|
||||
>
|
||||
|
||||
<style>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href="/static/assets/monofile-circ.png"
|
||||
>
|
||||
|
||||
* {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
h1,h2 {
|
||||
text-align:center;
|
||||
}
|
||||
#content {
|
||||
position:fixed;
|
||||
left:50%;
|
||||
top:50%;
|
||||
transform:translate(-50%,-50%);
|
||||
background-color:white;
|
||||
width:450px;
|
||||
/*height:100%;*/
|
||||
}
|
||||
body {
|
||||
background-color: darkgray;
|
||||
}
|
||||
.note {
|
||||
padding:5px;
|
||||
position:relative;
|
||||
width:calc( 100% - 60px );
|
||||
left:25px;
|
||||
border:1px solid #AAAAAAFF;
|
||||
border-radius: 8px;
|
||||
background-color: #AAAAAA66;
|
||||
}
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, user-scalable=0"
|
||||
>
|
||||
|
||||
@media only screen and (max-width: 450px) {
|
||||
#content {
|
||||
position:fixed;
|
||||
left:0%;
|
||||
top:0%;
|
||||
transform:translate(0%,0%);
|
||||
background-color:white;
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
}
|
||||
<title>$code</title>
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<h1>
|
||||
monofile<span style="font-style:italic;font-weight:bold;font-size:16px;color:#999999"> $Version</span>
|
||||
</h1>
|
||||
<h2><em>Discord-based file sharing</em></h2>
|
||||
<div class="note" style="border-color:#FFAAAAFF;background-color:#FFAAAA66">
|
||||
<h1>$ErrorCode</h1>
|
||||
<p style="width:calc( 100% - 50px );left:25px;position:relative;">$ErrorMessage</p>
|
||||
</div>
|
||||
<div style="width:100%;height:25px"></div>
|
||||
</div>
|
||||
</body>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p class="error">
|
||||
<span class="code">$code</span>
|
||||
$text
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
15
rollup.config.mjs
Normal file
15
rollup.config.mjs
Normal file
|
@ -0,0 +1,15 @@
|
|||
import svelte from 'rollup-plugin-svelte'
|
||||
import resolve from "@rollup/plugin-node-resolve"
|
||||
|
||||
export default {
|
||||
input: "src/script/client/main.js",
|
||||
output: {
|
||||
file: 'out/script/client/bundle.js',
|
||||
format: 'esm',
|
||||
sourcemap:true
|
||||
},
|
||||
plugins: [
|
||||
resolve(),
|
||||
svelte({})
|
||||
]
|
||||
}
|
0
src/client/main.js
Normal file
0
src/client/main.js
Normal file
|
@ -8,6 +8,7 @@ import Discord, { IntentsBitField, Client } from "discord.js"
|
|||
import express from "express"
|
||||
import fs from "fs"
|
||||
import axios, { AxiosResponse } from "axios"
|
||||
import ServeError from "./lib/errors"
|
||||
|
||||
import Files from "./lib/files"
|
||||
require("dotenv").config()
|
||||
|
@ -21,14 +22,6 @@ app.use("/static",express.static("assets"))
|
|||
app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
|
||||
// funcs
|
||||
|
||||
function ThrowError(response:express.Response,code:number,errorMessage:string) {
|
||||
fs.readFile(__dirname+"/../pages/error.html",(err,buf) => {
|
||||
if (err) {response.sendStatus(500);console.log(err);return}
|
||||
response.status(code)
|
||||
response.send(buf.toString().replace(/\$ErrorCode/g,code.toString()).replace(/\$ErrorMessage/g,errorMessage).replace(/\$Version/g,pkg.version))
|
||||
})
|
||||
}
|
||||
|
||||
// init data
|
||||
|
||||
if (!fs.existsSync(__dirname+"/../.data/")) fs.mkdirSync(__dirname+"/../.data/")
|
||||
|
@ -87,7 +80,10 @@ app.post("/upload",multerSetup.single('file'),async (req,res) => {
|
|||
try {
|
||||
files.uploadFile({name:req.file.originalname,mime:req.file.mimetype,uploadId:req.header("monofile-upload-id")},req.file.buffer)
|
||||
.then((uID) => res.send(uID))
|
||||
.catch((stat) => {res.status(stat.status);res.send(`[err] ${stat.message}`)})
|
||||
.catch((stat) => {
|
||||
res.status(stat.status);
|
||||
res.send(`[err] ${stat.message}`)
|
||||
})
|
||||
} catch {
|
||||
res.status(400)
|
||||
res.send("[err] bad request")
|
||||
|
@ -108,7 +104,10 @@ app.post("/clone",(req,res) => {
|
|||
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((stat) => {
|
||||
res.status(stat.status);
|
||||
res.send(`[err] ${stat.message}`)
|
||||
})
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
res.status(400)
|
||||
|
@ -141,7 +140,7 @@ app.get("/download/:fileId",(req,res) => {
|
|||
)
|
||||
})
|
||||
} else {
|
||||
ThrowError(res,404,"File not found.")
|
||||
ServeError(res,404,"File not found.")
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -151,12 +150,12 @@ app.get("/file/:fileId",async (req,res) => {
|
|||
res.status(200)
|
||||
f.dataStream.pipe(res)
|
||||
}).catch((err) => {
|
||||
ThrowError(res,err.status,err.message)
|
||||
ServeError(res,err.status,err.message)
|
||||
})
|
||||
})
|
||||
|
||||
app.get("*",(req,res) => {
|
||||
ThrowError(res,404,"Page not found.")
|
||||
ServeError(res,404,"Page not found.")
|
||||
})
|
||||
|
||||
app.get("/server",(req,res) => {
|
131
src/server/lib/accounts.ts
Normal file
131
src/server/lib/accounts.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
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)
|
||||
}
|
||||
})
|
58
src/server/lib/auth.ts
Normal file
58
src/server/lib/auth.ts
Normal file
|
@ -0,0 +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))
|
||||
}).catch(err => console.error(err))
|
35
src/server/lib/errors.ts
Normal file
35
src/server/lib/errors.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
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()}/src/pages/error.html`)
|
||||
.catch(() => {res.header("Content-Type","text/plain")})
|
||||
|| "<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()
|
||||
}
|
50
src/style/_base.scss
Normal file
50
src/style/_base.scss
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
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: $Background
|
||||
}
|
34
src/style/app/topbar.scss
Normal file
34
src/style/app/topbar.scss
Normal file
|
@ -0,0 +1,34 @@
|
|||
@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;
|
||||
|
||||
.monofile_ico {
|
||||
width:26px;
|
||||
height:26px;
|
||||
border:2px solid base.$darkish;
|
||||
border-radius:100%;
|
||||
transition-duration: 100ms;
|
||||
|
||||
&:hover {
|
||||
border:2px solid #999999;
|
||||
transition-duration: 100ms;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
20
src/style/error.scss
Normal file
20
src/style/error.scss
Normal file
|
@ -0,0 +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;
|
||||
}
|
||||
}
|
||||
|
0
src/svelte/App.svelte
Normal file
0
src/svelte/App.svelte
Normal file
16
src/svelte/elem/Topbar.svelte
Normal file
16
src/svelte/elem/Topbar.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script>
|
||||
|
||||
</script>
|
||||
|
||||
<div id="topbar">
|
||||
<button on:click={() => {}} class="menuBtn">my account</button>
|
||||
<button on:click={() => {}} class="menuBtn">files</button>
|
||||
|
||||
<!-- divider -->
|
||||
<div style="height:calc( 100% - 10px ); top:5px; width:2px; background-color:#333333; margin:0 5px 0 5px;" />
|
||||
|
||||
<button on:click={() => {}} class="menuBtn">upload</button>
|
||||
<button on:click={() => {}} class="menuBtn">clone</button>
|
||||
|
||||
<div />
|
||||
</div>
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"include":["src/**/*"],
|
||||
"include":["src/server/**/*"],
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
|
|
Loading…
Reference in a new issue