mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-22 22:06:25 -08:00
Experimental, and very bad & likely broken email
This commit is contained in:
parent
3ac2518578
commit
42931ab3cf
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,4 @@
|
|||
node_modules
|
||||
.env
|
||||
.data
|
||||
out
|
||||
config.json
|
||||
out
|
|
@ -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
|
||||
|
|
1
assets/icons/mail.svg
Normal file
1
assets/icons/mail.svg
Normal 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="M22 8.608v8.142a3.25 3.25 0 0 1-3.066 3.245L18.75 20H5.25a3.25 3.25 0 0 1-3.245-3.066L2 16.75V8.608l9.652 5.056a.75.75 0 0 0 .696 0L22 8.608ZM5.25 4h13.5a3.25 3.25 0 0 1 3.234 2.924L12 12.154l-9.984-5.23a3.25 3.25 0 0 1 3.048-2.919L5.25 4h13.5-13.5Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 377 B |
11
config.json
11
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"
|
||||
}
|
||||
}
|
||||
}
|
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -11,6 +11,7 @@ export let Accounts: Account[] = []
|
|||
export interface Account {
|
||||
id : string
|
||||
username : string
|
||||
email? : string
|
||||
password : {
|
||||
hash : string
|
||||
salt : string
|
||||
|
|
|
@ -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
|
||||
|
|
35
src/server/lib/mail.ts
Normal file
35
src/server/lib/mail.ts
Normal file
|
@ -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": `<span style="font-size:x-large;font-weight:600;">monofile <span style="opacity:0.5">accounts</span></span><br><span style="opacity:0.5">Gain control of your uploads.</span><hr><br>${
|
||||
content
|
||||
.replace(/\<span username\>/g, `<span code><span style="color:#DDAA66;padding-right:3px;">@</span>`)
|
||||
.replace(/\<span code\>/g,`<span style="font-family:monospace;padding:3px 5px 3px 5px;border-radius:8px;background-color:#1C1C1C;color:#DDDDDD;">`)
|
||||
}<br><br><span style="opacity:0.5">If you do not believe that you are the intended recipient of this email, please disregard it.</span>`
|
||||
}, (err, info) => {
|
||||
if (err) reject(err)
|
||||
else resolve(info)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -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<string, {code: string, email: string, expiry: NodeJS.Timeout, requestedAt:number}>()
|
||||
|
||||
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`, `<b>Hello there!</b> You are recieving this message because you decided to link your email, <span code>${req.body.email}</span>, to your account, <span username>${acc.username}</span>. If you would like to continue, please <a href="https://${req.header("Host")}/auth/confirm_email/${code}"><span code>click here</span></a>, 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(`<script>window.close()</script>`)
|
||||
} else {
|
||||
ServeError(res, 400, "invalid code")
|
||||
}
|
||||
})
|
||||
|
||||
let pwReset = new Map<string, {code: string, expiry: NodeJS.Timeout, requestedAt:number}>()
|
||||
let prcIdx = new Map<string, string>()
|
||||
|
||||
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}`, `<b>Hello there!</b> You are recieving this message because you forgot your password to your monofile account, <span username>${acc.username}</span>. To log in, please <a href="https://${req.header("Host")}/auth/emergency_login/${code}"><span code>click here</span></a>, 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) {
|
||||
|
|
|
@ -54,6 +54,13 @@
|
|||
flex-grow:1;
|
||||
}
|
||||
|
||||
button.flavor {
|
||||
|
||||
padding: 0;
|
||||
background: none;
|
||||
|
||||
}
|
||||
|
||||
input[type=text],input[type=password] {
|
||||
border:none;
|
||||
border-radius:0;
|
||||
|
|
|
@ -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",[
|
||||
{
|
||||
|
|
|
@ -95,6 +95,11 @@
|
|||
<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>
|
||||
|
||||
{#if targetAction == "login"}
|
||||
<button class="flavor" on:click={() => accOpts.forgotPassword(optPicker)}>I forgot my password</button>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
|
@ -126,6 +131,11 @@
|
|||
<p>Change username</p>
|
||||
</button>
|
||||
|
||||
<button on:click={() => accOpts.emailChange(optPicker)}>
|
||||
<img src="/static/assets/icons/mail.svg" alt="change email">
|
||||
<p>Change email{#if $account.email}<span class="monospaceText"><br />{$account.email}</span>{/if}</p>
|
||||
</button>
|
||||
|
||||
<button on:click={() => accOpts.pwdChng(optPicker)}>
|
||||
<img src="/static/assets/icons/change_password.svg" alt="change password">
|
||||
<p>Change password<span><br />You will be logged out of all sessions</span></p>
|
||||
|
@ -149,7 +159,7 @@
|
|||
|
||||
<button on:click={() => uplOpts.update_all_files(optPicker)}>
|
||||
<img src="/static/assets/icons/update.svg" alt="update">
|
||||
<p>Make all of my files {$account.defaultFileVisibility || "public"}<span><br />Matches your default file visibility</p>
|
||||
<p>Make all of my files {$account.defaultFileVisibility || "public"}<span><br />Matches your default file visibility</span></p>
|
||||
</button>
|
||||
|
||||
<div class="category">
|
||||
|
|
Loading…
Reference in a new issue