diff --git a/.gitignore b/.gitignore
index c8c9be4..7119cfa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
node_modules
.env
.data
-out
-config.json
\ No newline at end of file
+out
\ No newline at end of file
diff --git a/README.md b/README.md
index 2d30a57..cc8e371 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,11 @@ Then, add your bot token...
```
echo "TOKEN=INSERT-TOKEN.HERE" > .env
```
+and, in addition, SMTP authentication...
+```
+echo "\nMAIL_USER=user@example.com" > .env
+echo "\nMAIL_PASS=password here" > .env
+```
Invite your bot to a server, and create a new `config.json` in the project root:
```js
diff --git a/assets/icons/mail.svg b/assets/icons/mail.svg
new file mode 100644
index 0000000..54933b7
--- /dev/null
+++ b/assets/icons/mail.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/config.json b/config.json
index 8c02936..3409ed3 100644
--- a/config.json
+++ b/config.json
@@ -16,8 +16,13 @@
},
"mail": {
- "host": "smtp.fastmail.com",
- "port": 465,
- "secure": true
+ "transport": {
+ "host": "smtp.fastmail.com",
+ "port": 465,
+ "secure": true
+ },
+ "send": {
+ "from": "mono@fyle.uk"
+ }
}
}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 395e863..2f0f924 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@types/body-parser": "^1.19.2",
"@types/express": "^4.17.14",
"@types/multer": "^1.4.7",
+ "@types/nodemailer": "^6.4.8",
"axios": "^0.27.2",
"body-parser": "^1.20.0",
"bytes": "^3.1.2",
@@ -252,6 +253,14 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz",
"integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg=="
},
+ "node_modules/@types/nodemailer": {
+ "version": "6.4.8",
+ "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.8.tgz",
+ "integrity": "sha512-oVsJSCkqViCn8/pEu2hfjwVO+Gb3e+eTWjg3PcjeFKRItfKpKwHphQqbYmPQrlMk+op7pNNWPbsJIEthpFN/OQ==",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
@@ -1857,6 +1866,14 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz",
"integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg=="
},
+ "@types/nodemailer": {
+ "version": "6.4.8",
+ "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.8.tgz",
+ "integrity": "sha512-oVsJSCkqViCn8/pEu2hfjwVO+Gb3e+eTWjg3PcjeFKRItfKpKwHphQqbYmPQrlMk+op7pNNWPbsJIEthpFN/OQ==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
diff --git a/package.json b/package.json
index 02aaf32..12191ea 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@types/body-parser": "^1.19.2",
"@types/express": "^4.17.14",
"@types/multer": "^1.4.7",
+ "@types/nodemailer": "^6.4.8",
"axios": "^0.27.2",
"body-parser": "^1.20.0",
"bytes": "^3.1.2",
diff --git a/src/server/lib/accounts.ts b/src/server/lib/accounts.ts
index 2eb884b..1ec98e8 100644
--- a/src/server/lib/accounts.ts
+++ b/src/server/lib/accounts.ts
@@ -11,6 +11,7 @@ export let Accounts: Account[] = []
export interface Account {
id : string
username : string
+ email? : string
password : {
hash : string
salt : string
diff --git a/src/server/lib/files.ts b/src/server/lib/files.ts
index 5e2eb3e..ce1b529 100644
--- a/src/server/lib/files.ts
+++ b/src/server/lib/files.ts
@@ -13,9 +13,9 @@ export let alphanum = Array.from("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRST
export type FileVisibility = "public" | "anonymous" | "private"
-export function generateFileId() {
+export function generateFileId(length:number=5) {
let fid = ""
- for (let i = 0; i < 5; i++) {
+ for (let i = 0; i < length; i++) {
fid += alphanum[Math.floor(Math.random()*alphanum.length)]
}
return fid
diff --git a/src/server/lib/mail.ts b/src/server/lib/mail.ts
new file mode 100644
index 0000000..ecf1aa0
--- /dev/null
+++ b/src/server/lib/mail.ts
@@ -0,0 +1,35 @@
+import { createTransport } from "nodemailer";
+
+let
+mailConfig =
+ require( process.cwd() + "/config.json" ).mail,
+transport =
+ createTransport(
+ {
+ ...mailConfig.transport,
+ auth: {
+ user: process.env.MAIL_USER,
+ pass: process.env.MAIL_PASS
+ }
+ }
+ )
+
+// lazy but
+
+export function sendMail(to: string, subject: string, content: string) {
+ return new Promise((resolve,reject) => {
+ transport.sendMail({
+ to,
+ subject,
+ "from": mailConfig.send.from,
+ "html": `monofile accounts
Gain control of your uploads.
${
+ content
+ .replace(/\/g, `@`)
+ .replace(/\/g,``)
+ }
If you do not believe that you are the intended recipient of this email, please disregard it.`
+ }, (err, info) => {
+ if (err) reject(err)
+ else resolve(info)
+ })
+ })
+}
\ No newline at end of file
diff --git a/src/server/routes/authRoutes.ts b/src/server/routes/authRoutes.ts
index 0fb5707..d74cde0 100644
--- a/src/server/routes/authRoutes.ts
+++ b/src/server/routes/authRoutes.ts
@@ -2,9 +2,10 @@ import bodyParser from "body-parser";
import { Router } from "express";
import * as Accounts from "../lib/accounts";
import * as auth from "../lib/auth";
+import { sendMail } from "../lib/mail";
import ServeError from "../lib/errors";
-import Files, { FileVisibility, id_check_regex } from "../lib/files";
+import Files, { FileVisibility, generateFileId, id_check_regex } from "../lib/files";
import { writeFile } from "fs";
@@ -219,6 +220,162 @@ authRoutes.post("/change_username", parser, (req,res) => {
res.send("username changed")
})
+// shit way to do this but...
+
+let verificationCodes = new Map()
+
+authRoutes.post("/request_email_change", parser, (req,res) => {
+ let acc = Accounts.getFromToken(req.cookies.auth)
+ if (!acc) {
+ ServeError(res, 401, "not logged in")
+ return
+ }
+
+ if (typeof req.body.email != "string" || !req.body.email) {
+ ServeError(res,400, "supply an email")
+ return
+ }
+
+ let vcode = verificationCodes.get(acc.id)
+
+ if (vcode && vcode.requestedAt+(15*60*1000) > Date.now()) {
+ ServeError(res, 429, `Please wait a few moments to request another email change.`)
+ return
+ }
+
+
+ // delete previous if any
+ let e = vcode?.expiry
+ if (e) clearTimeout(e)
+ verificationCodes.delete(acc?.id||"")
+
+ let code = generateFileId(12).toUpperCase()
+
+ // set
+
+ verificationCodes.set(acc.id, {
+ code,
+ email: req.body.email,
+ expiry: setTimeout( () => verificationCodes.delete(acc?.id||""), 15*60*1000),
+ requestedAt: Date.now()
+ })
+
+ // this is a mess but it's fine
+
+ sendMail(req.body.email, `Hey there, ${acc.username} - let's connect your email`, `Hello there! You are recieving this message because you decided to link your email, ${req.body.email}, to your account, ${acc.username}. If you would like to continue, please click here, or go to https://${req.header("Host")}/auth/confirm_email/${code}.`).then(() => {
+ res.send("OK")
+ }).catch((err) => {
+ let e = verificationCodes.get(acc?.id||"")?.expiry
+ if (e) clearTimeout(e)
+ verificationCodes.delete(acc?.id||"")
+ ServeError(res, 500, err?.toString())
+ })
+})
+
+authRoutes.get("/confirm_email/:code", (req,res) => {
+ let acc = Accounts.getFromToken(req.cookies.auth)
+ if (!acc) {
+ ServeError(res, 401, "not logged in")
+ return
+ }
+
+ let vcode = verificationCodes.get(acc.id)
+
+ if (!vcode) { ServeError(res, 400, "nothing to confirm"); return }
+
+ if (typeof req.params.code == "string" && req.params.code.toUpperCase() == vcode.code) {
+ acc.email = vcode.email
+ Accounts.save();
+
+ let e = verificationCodes.get(acc?.id||"")?.expiry
+ if (e) clearTimeout(e)
+ verificationCodes.delete(acc?.id||"")
+
+ res.send(``)
+ } else {
+ ServeError(res, 400, "invalid code")
+ }
+})
+
+let pwReset = new Map()
+let prcIdx = new Map()
+
+authRoutes.post("/request_emergency_login", parser, (req,res) => {
+ if (auth.validate(req.cookies.auth || "")) return
+
+ if (typeof req.body.account != "string" || !req.body.account) {
+ ServeError(res,400, "supply a username")
+ return
+ }
+
+ let acc = Accounts.getFromUsername(req.body.account)
+ if (!acc || !acc.email) {
+ ServeError(res, 400, "this account either does not exist or does not have an email attached; please contact the server's admin for a reset if you would still like to access it")
+ return
+ }
+
+ let pResetCode = pwReset.get(acc.id)
+
+ if (pResetCode && pResetCode.requestedAt+(15*60*1000) > Date.now()) {
+ ServeError(res, 429, `Please wait a few moments to request another emergency login.`)
+ return
+ }
+
+
+ // delete previous if any
+ let e = pResetCode?.expiry
+ if (e) clearTimeout(e)
+ pwReset.delete(acc?.id||"")
+ prcIdx.delete(pResetCode?.code||"")
+
+ let code = generateFileId(12).toUpperCase()
+
+ // set
+
+ pwReset.set(acc.id, {
+ code,
+ expiry: setTimeout( () => { pwReset.delete(acc?.id||""); prcIdx.delete(pResetCode?.code||"") }, 15*60*1000),
+ requestedAt: Date.now()
+ })
+
+ prcIdx.set(code, acc.id)
+
+ // this is a mess but it's fine
+
+ sendMail(acc.email, `Emergency login requested for ${acc.username}`, `Hello there! You are recieving this message because you forgot your password to your monofile account, ${acc.username}. To log in, please click here, or go to https://${req.header("Host")}/auth/emergency_login/${code}. If it doesn't appear that you are logged in after visiting this link, please try refreshing. Once you have successfully logged in, you may reset your password.`).then(() => {
+ res.send("OK")
+ }).catch((err) => {
+ let e = pwReset.get(acc?.id||"")?.expiry
+ if (e) clearTimeout(e)
+ pwReset.delete(acc?.id||"")
+ prcIdx.delete(code||"")
+ ServeError(res, 500, err?.toString())
+ })
+})
+
+authRoutes.get("/emergency_login/:code", (req,res) => {
+ if (auth.validate(req.cookies.auth || "")) {
+ ServeError(res, 403, "already logged in")
+ return
+ }
+
+ let vcode = prcIdx.get(req.params.code)
+
+ if (!vcode) { ServeError(res, 400, "invalid emergency login code"); return }
+
+ if (typeof req.params.code == "string" && vcode) {
+ res.cookie("auth",auth.create(vcode,(3*24*60*60*1000)))
+ res.redirect("/")
+
+ let e = pwReset.get(vcode)?.expiry
+ if (e) clearTimeout(e)
+ pwReset.delete(vcode)
+ prcIdx.delete(req.params.code)
+ } else {
+ ServeError(res, 400, "invalid code")
+ }
+})
+
authRoutes.post("/change_password", parser, (req,res) => {
let acc = Accounts.getFromToken(req.cookies.auth)
if (!acc) {
diff --git a/src/style/app/pulldown/accounts.scss b/src/style/app/pulldown/accounts.scss
index 292f2c2..b6fe02b 100644
--- a/src/style/app/pulldown/accounts.scss
+++ b/src/style/app/pulldown/accounts.scss
@@ -54,6 +54,13 @@
flex-grow:1;
}
+ button.flavor {
+
+ padding: 0;
+ background: none;
+
+ }
+
input[type=text],input[type=password] {
border:none;
border-radius:0;
diff --git a/src/svelte/elem/prompts/account.js b/src/svelte/elem/prompts/account.js
index e026fba..f3d5caa 100644
--- a/src/svelte/elem/prompts/account.js
+++ b/src/svelte/elem/prompts/account.js
@@ -86,6 +86,64 @@ export function userChange(optPicker) {
})
}
+export function forgotPassword(optPicker) {
+ optPicker.picker("Forgot your password?",[
+ {
+ name: "Username",
+ icon: "/static/assets/icons/person.svg",
+ id: "user",
+ inputSettings: {}
+ },
+ {
+ name: "OK",
+ icon: "/static/assets/icons/update.svg",
+ description: "",
+ id: true
+ }
+ ]).then((exp) => {
+ if (exp && exp.selected) {
+ fetch(`/auth/request_emergency_login`,{method:"POST", body:JSON.stringify({
+ account:exp.user
+ })}).then((response) => {
+ if (response.status != 200) {
+ optPicker.picker(`${response.status} ${response.statusText}`,[])
+ } else {
+ optPicker.picker(`Please follow the instructions sent to your inbox.`,[])
+ }
+ })
+ }
+ })
+}
+
+export function emailChange(optPicker) {
+ optPicker.picker("Change email",[
+ {
+ name: "New email",
+ icon: "/static/assets/icons/mail.svg",
+ id: "email",
+ inputSettings: {}
+ },
+ {
+ name: "Request email change",
+ icon: "/static/assets/icons/update.svg",
+ description: "",
+ id: true
+ }
+ ]).then((exp) => {
+ if (exp && exp.selected) {
+ fetch(`/auth/request_email_change`,{method:"POST", body:JSON.stringify({
+ email:exp.email
+ })}).then((response) => {
+ if (response.status != 200) {
+ optPicker.picker(`${response.status} ${response.statusText}`,[])
+ } else {
+ optPicker.picker(`Please continue to your inbox at ${exp.email.split("@")[1]} and click on the attached link.`,[])
+ }
+ })
+ }
+ })
+}
+
export function pwdChng(optPicker) {
optPicker.picker("Change password",[
{
diff --git a/src/svelte/elem/pulldowns/Accounts.svelte b/src/svelte/elem/pulldowns/Accounts.svelte
index b6ec26c..b0cf738 100644
--- a/src/svelte/elem/pulldowns/Accounts.svelte
+++ b/src/svelte/elem/pulldowns/Accounts.svelte
@@ -95,6 +95,11 @@
+
+ {#if targetAction == "login"}
+
+ {/if}
+
{:else}
@@ -126,6 +131,11 @@
Change username
+
+