First commit
This commit is contained in:
parent
1dae13fe7e
commit
35f37491af
4
.gitignore
vendored
4
.gitignore
vendored
@ -144,3 +144,7 @@ dist
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
instances/*
|
||||
!instances/default
|
||||
instances/default/cert.*
|
||||
instances/default/vault.json
|
||||
|
66
instances/default/config.json
Normal file
66
instances/default/config.json
Normal file
@ -0,0 +1,66 @@
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"features": {
|
||||
"api": false,
|
||||
"client": true,
|
||||
"create": true,
|
||||
"createPath": "/"
|
||||
},
|
||||
"http": {
|
||||
"port": 3080
|
||||
},
|
||||
"https": {
|
||||
"cert": "./cert.crt",
|
||||
"key": "./cert.key",
|
||||
"port": 3443
|
||||
}
|
||||
},
|
||||
{
|
||||
"features": {
|
||||
"api": true,
|
||||
"client": false,
|
||||
"create": true
|
||||
},
|
||||
"http": {
|
||||
"port": 3081
|
||||
},
|
||||
"https": {
|
||||
"cert": "./cert.crt",
|
||||
"key": "./cert.key",
|
||||
"port": 3444
|
||||
}
|
||||
}
|
||||
],
|
||||
"vault": {
|
||||
"db": {
|
||||
"type": "file",
|
||||
"filename": "./vault.json"
|
||||
},
|
||||
"crypto": {
|
||||
"method": "aes-256-gcm",
|
||||
"key": "Djblt6b8RQ+mQC6/ilpjC6y9bkfUEzkt",
|
||||
"iv": "dsfnuo3"
|
||||
},
|
||||
"publicID": {
|
||||
"minLength": 37,
|
||||
"maxLength": 45
|
||||
},
|
||||
"otLinkID": {
|
||||
"minLength": 37,
|
||||
"maxLength": 45,
|
||||
"timeout": 360
|
||||
}
|
||||
},
|
||||
"mailer": {
|
||||
"sender": "Secret Sender <me@domain>",
|
||||
"server": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 25,
|
||||
"secure": false,
|
||||
"tls": {
|
||||
"rejectUnauthorized": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
instances/default/gencert.sh
Executable file
3
instances/default/gencert.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -x509 -nodes -days 3650 -out ./cert.crt -keyout ./cert.key -subj "/C=/ST=/L=/O=/OU=/CN="
|
3
instances/default/start.sh
Executable file
3
instances/default/start.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
node ../../src/app.js
|
11
src/app.js
Normal file
11
src/app.js
Normal file
@ -0,0 +1,11 @@
|
||||
import path from "path"
|
||||
import config from "./core/config.js"
|
||||
import server from "./core/server.js"
|
||||
|
||||
global.__dirname = path.dirname(process.argv[1])
|
||||
|
||||
const _config = config.get()
|
||||
|
||||
_config.servers.forEach(srvConfig => {
|
||||
server.start(srvConfig)
|
||||
})
|
23
src/core/config.js
Normal file
23
src/core/config.js
Normal file
@ -0,0 +1,23 @@
|
||||
import fs from "fs"
|
||||
|
||||
let _configFile = "config.json"
|
||||
if(process.argv[2]) _configFile = process.argv[2]
|
||||
|
||||
let _config = null
|
||||
|
||||
const load = () => {
|
||||
if(! fs.existsSync(_configFile)) {
|
||||
throw new Error('Config file not found')
|
||||
}
|
||||
_config = JSON.parse(fs.readFileSync(_configFile))
|
||||
}
|
||||
|
||||
if(! _config) load()
|
||||
|
||||
const get = () => {
|
||||
return structuredClone(_config)
|
||||
}
|
||||
|
||||
export default {
|
||||
get,
|
||||
}
|
29
src/core/mailer.js
Normal file
29
src/core/mailer.js
Normal file
@ -0,0 +1,29 @@
|
||||
import nodemailer from 'nodemailer'
|
||||
import config from './config.js'
|
||||
|
||||
const _config = config.get()
|
||||
|
||||
const send = (email = {
|
||||
from: null,
|
||||
to: '',
|
||||
subject: '',
|
||||
text: null,
|
||||
html: null,
|
||||
}) => {
|
||||
if(! email.from) email.from = _config.mailer.sender
|
||||
const transporter = nodemailer.createTransport(_config.mailer.server)
|
||||
const mailOptions = {
|
||||
from: email.from,
|
||||
to: email.to,
|
||||
subject: email.subject,
|
||||
}
|
||||
if(email.html) {
|
||||
mailOptions.html = email.html
|
||||
if(email.text) mailOptions.text = email.text
|
||||
} else mailOptions.text = email.text || ''
|
||||
transporter.sendMail(mailOptions, function(error, info){})
|
||||
}
|
||||
|
||||
export default {
|
||||
send,
|
||||
}
|
101
src/core/routes.js
Normal file
101
src/core/routes.js
Normal file
@ -0,0 +1,101 @@
|
||||
import bodyParser from "body-parser"
|
||||
import vault from "./vault.js"
|
||||
|
||||
const _title = "Secret Sender"
|
||||
|
||||
const load = (router, features) => {
|
||||
|
||||
if(features.api == true) {
|
||||
|
||||
router.get("/api/getlink/:publicID", (req, res, next) => {
|
||||
const link = vault.getLink(req.params.publicID)
|
||||
res.json(link)
|
||||
})
|
||||
|
||||
router.get("/api/createOTLink/:publicID", (req, res, next) => {
|
||||
const otLink = vault.createOTLink(req.params.publicID)
|
||||
res.json(otLink)
|
||||
})
|
||||
|
||||
router.get("/api/read/:otLinkID", (req, res, next) => {
|
||||
const secret = vault.read(req.params.otLinkID)
|
||||
res.json(secret)
|
||||
})
|
||||
|
||||
if(features.create == true) {
|
||||
|
||||
router.post("/api/create", bodyParser.urlencoded({ extended: false }), bodyParser.json(), (req, res, next) => {
|
||||
const link = vault.create(req.body)
|
||||
res.json(link)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(features.client !== false) {
|
||||
|
||||
router.get("/link/:publicID", (req, res, next) => {
|
||||
const link = vault.getLink(req.params.publicID)
|
||||
if(link) {
|
||||
router.pug(res, "link.pug", { title : _title, link: link, linkUrl: req.protocol + "://" + req.headers.host + "/link/" + link.publicID })
|
||||
} else {
|
||||
res.redirect('/')
|
||||
}
|
||||
})
|
||||
|
||||
router.post("/requestotl", bodyParser.urlencoded({ extended: false }), bodyParser.json(), (req, res, next) => {
|
||||
const link = vault.getLink(req.body.publicID)
|
||||
if(link) {
|
||||
const otLink = vault.createOTLink(link.publicID)
|
||||
if(otLink) {
|
||||
res.json(otLink)
|
||||
} else {
|
||||
res.json(null)
|
||||
}
|
||||
} else {
|
||||
res.json(null)
|
||||
}
|
||||
})
|
||||
|
||||
router.get("/read/:otLinkID", (req, res, next) => {
|
||||
const secret = vault.read(req.params.otLinkID)
|
||||
if(secret) {
|
||||
router.pug(res, "read.pug", { title : _title, secret: secret })
|
||||
} else {
|
||||
res.redirect('/')
|
||||
}
|
||||
})
|
||||
|
||||
if(features.create == true) {
|
||||
|
||||
router.get(features.createPath, (req, res, next) => {
|
||||
router.pug(res, "create.pug", { title: _title })
|
||||
})
|
||||
|
||||
router.post(features.createPath, bodyParser.urlencoded({ extended: false }), bodyParser.json(), (req, res, next) => {
|
||||
const link = vault.create(req.body)
|
||||
if(link) {
|
||||
res.json(link)
|
||||
} else {
|
||||
res.json(null)
|
||||
}
|
||||
})
|
||||
|
||||
router.post("/newlink", bodyParser.urlencoded({ extended: false }), bodyParser.json(), (req, res, next) => {
|
||||
router.pug(res, "newlink.pug", { title : _title, link: req.body, linkUrl: req.protocol + "://" + req.headers.host + "/link/" + req.body.publicID })
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
router.get("/", (req, res, next) => {
|
||||
router.pug(res, "index.pug", { title: _title })
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default {
|
||||
load,
|
||||
}
|
60
src/core/server.js
Normal file
60
src/core/server.js
Normal file
@ -0,0 +1,60 @@
|
||||
import fs from "fs"
|
||||
import express from "express"
|
||||
import http from "http"
|
||||
import https from "https"
|
||||
import routes from "./routes.js"
|
||||
|
||||
const defaulthandler = (req, res, next) => {
|
||||
res.redirect("/")
|
||||
}
|
||||
|
||||
const pug = (res, file, value, allowcache) => {
|
||||
//app.srv.cache(res,allowcache!=false)
|
||||
res.setHeader("X-Frame-Options", "sameorigin")
|
||||
res.setHeader("X-XSS-Protection", "1; mode=block")
|
||||
res.render(file, value)
|
||||
}
|
||||
/*
|
||||
const getremoteip = (req) => {
|
||||
return req.ip
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
const start = (srvConfig) => {
|
||||
const app = express()
|
||||
const router = express.Router()
|
||||
router.use(express.json())
|
||||
|
||||
router.pug = pug
|
||||
|
||||
routes.load(router, srvConfig.features)
|
||||
|
||||
app.disable("x-powered-by")
|
||||
|
||||
app.set('trust proxy', true)
|
||||
app.set("views", __dirname + "/pug")
|
||||
app.set("view engine", "pug")
|
||||
|
||||
app.use(router)
|
||||
app.use(express.static(__dirname + "/html", {maxAge: 86400000000}))
|
||||
app.use(defaulthandler)
|
||||
|
||||
if(srvConfig.http) {
|
||||
const srv = http.createServer({}, app)
|
||||
srv.listen(srvConfig.http.port, '0.0.0.0')
|
||||
console.log("Listen http " + srvConfig.http.port + " : " + JSON.stringify(srvConfig.features))
|
||||
}
|
||||
if(srvConfig.https && fs.existsSync(srvConfig.https.cert) && fs.existsSync(srvConfig.https.key)) {
|
||||
const sslsrv = https.createServer({
|
||||
cert: fs.readFileSync(srvConfig.https.cert),
|
||||
key: fs.readFileSync(srvConfig.https.key),
|
||||
}, app)
|
||||
sslsrv.listen(srvConfig.https.port, '0.0.0.0')
|
||||
console.log("Listen https " + srvConfig.https.port + " : " + JSON.stringify(srvConfig.features))
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
start,
|
||||
}
|
223
src/core/vault.js
Normal file
223
src/core/vault.js
Normal file
@ -0,0 +1,223 @@
|
||||
import fs from 'fs'
|
||||
import cron from 'node-cron'
|
||||
import crypto from 'crypto'
|
||||
import config from "./config.js"
|
||||
import mailer from './mailer.js'
|
||||
|
||||
const _config = config.get()
|
||||
let _cache = null
|
||||
const _readLinks = []
|
||||
const _dbFile = _config.vault.db.filename
|
||||
|
||||
const init = () => {
|
||||
if(fs.existsSync(_dbFile)) {
|
||||
_cache = JSON.parse(fs.readFileSync(_dbFile))
|
||||
} else {
|
||||
_cache = []
|
||||
}
|
||||
clean()
|
||||
cron.schedule('* * * * *', clean)
|
||||
}
|
||||
const clean = () => {
|
||||
let needSave = false
|
||||
for(let i = _cache.length - 1; i >= 0; i--) {
|
||||
if(calcTimeLeft(_cache[i]) <= 0) {
|
||||
_cache.splice(i, 1)
|
||||
needSave = true
|
||||
}
|
||||
}
|
||||
if(needSave === true) save()
|
||||
for(let i = _readLinks.length - 1; i >= 0; i--) {
|
||||
if(Math.round(_config.vault.otLinkID.timeout - ((new Date() - new Date(_readLinks[i].date)) / 60000)) <= 0) {
|
||||
_readLinks.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
const save = () => {
|
||||
fs.writeFileSync(_dbFile, JSON.stringify(_cache))
|
||||
}
|
||||
|
||||
const newRandomNumber = (min, max) => {
|
||||
return Math.floor(Math.random() * (max - min) + min)
|
||||
}
|
||||
const newRandomString = (length) => {
|
||||
const randomBytes = crypto.randomBytes(Math.ceil(length / 2))
|
||||
return randomBytes.toString('hex').slice(0, length)
|
||||
}
|
||||
const calcReadsLeft = (item) => {
|
||||
if(item.maxReads <= 0) return -1
|
||||
return item.maxReads - item.readCount
|
||||
}
|
||||
const calcTimeLeft = (item) => {
|
||||
return Math.round(item.expireTime - ((new Date() - new Date(item.date)) / 60000))
|
||||
}
|
||||
const encrypt = (data) => {
|
||||
const cipher = crypto.createCipheriv(_config.vault.crypto.method, _config.vault.crypto.key, _config.vault.crypto.iv)
|
||||
let ciphertext = cipher.update(data, 'utf8', 'base64')
|
||||
ciphertext += cipher.final('base64')
|
||||
let value = cipher.getAuthTag().toString('hex') + '=' + ciphertext
|
||||
return value
|
||||
}
|
||||
const decrypt = (data) => {
|
||||
if(!data || data === '') return ''
|
||||
const decipher = crypto.createDecipheriv(
|
||||
_config.vault.crypto.method,
|
||||
_config.vault.crypto.key,
|
||||
_config.vault.crypto.iv
|
||||
)
|
||||
const splitIndex = data.indexOf('=')
|
||||
const tag = data.substring(0, splitIndex)
|
||||
const value = data.substring(splitIndex + 1)
|
||||
decipher.setAuthTag(Buffer.from(tag, 'hex'))
|
||||
let plaintext = decipher.update(value, 'base64', 'utf8')
|
||||
plaintext += decipher.final('utf8')
|
||||
return plaintext
|
||||
}
|
||||
const emailReadNotify = (data) => {
|
||||
const email = {
|
||||
to: data.emailNotify,
|
||||
subject: "Secret read",
|
||||
text: 'Secret read'
|
||||
}
|
||||
if(data.subject) email.subject = email.subject + ' - ' + data.subject
|
||||
if(data.message) email.text = data.message
|
||||
if(data.senderName) email.text = email.text + '\n\n' + data.senderName
|
||||
mailer.send(email)
|
||||
}
|
||||
|
||||
if(! _cache) init()
|
||||
|
||||
const createData = (data = {
|
||||
senderName: null,
|
||||
subject: null,
|
||||
message: null,
|
||||
secret: null,
|
||||
emailNotify: null,
|
||||
expireDays: null,
|
||||
expireHours: null,
|
||||
expireTime: null,
|
||||
maxReads: null,
|
||||
emailOTL: null,
|
||||
}) => {
|
||||
if(! data) return null
|
||||
const validatedData = {}
|
||||
if(data.senderName && data.senderName != "") validatedData.senderName = data.senderName
|
||||
if(data.subject && data.subject != "") validatedData.subject = data.subject
|
||||
if(data.message && data.message != "") validatedData.message = data.message
|
||||
if(data.secret && data.secret != "") validatedData.secret = data.secret
|
||||
if(data.emailNotify && data.emailNotify != "") validatedData.emailNotify = data.emailNotify
|
||||
if(data.emailOTL && data.emailOTL != "") validatedData.emailOTL = data.emailOTL
|
||||
if(data.maxReads && data.maxReads != "") validatedData.maxReads = data.maxReads
|
||||
let expireTime = 0
|
||||
if(data.expireDays && data.expireDays > 0) expireTime = +data.expireDays * 24 * 60
|
||||
if(data.expireHours && data.expireHours > 0) expireTime = expireTime + +data.expireHours * 60
|
||||
if(data.expireTime && data.expireTime > 0) expireTime = +data.expireTime
|
||||
if(expireTime > 0) validatedData.expireTime = expireTime
|
||||
|
||||
if(! validatedData.secret || validatedData.secret === '') return null
|
||||
if(! validatedData.expireTime) validatedData.expireTime = 24 * 60
|
||||
else if(validatedData.expireTime < 1) validatedData.expireTime = 1
|
||||
else if(validatedData.expireTime > 560 * 60) validatedData.expireTime = 560 * 60
|
||||
if(! validatedData.maxReads || isNaN(validatedData.maxReads)) validatedData.maxReads = 1
|
||||
else if(validatedData.maxReads < 0) validatedData.maxReads = 0
|
||||
else if(validatedData.maxReads > 999) validatedData.maxReads = 999
|
||||
|
||||
return validatedData
|
||||
}
|
||||
const createLinkData = (data) => {
|
||||
if(data) return {
|
||||
date: data.date,
|
||||
publicID: data.publicID,
|
||||
senderName: data.senderName,
|
||||
subject: data.subject,
|
||||
message: data.message,
|
||||
readsLeft: calcReadsLeft(data),
|
||||
timeLeft: calcTimeLeft(data),
|
||||
}
|
||||
return null
|
||||
}
|
||||
const create = (secretData) => {
|
||||
const data = createData(secretData)
|
||||
if(! data) return null
|
||||
data.date = new Date()
|
||||
data.readCount = 0
|
||||
data.publicID = newRandomString(newRandomNumber(_config.vault.publicID.minLength, _config.vault.publicID.maxLength))
|
||||
data.secret = encrypt(data.secret)
|
||||
_cache.push(data)
|
||||
save()
|
||||
const returnData = createLinkData(data)
|
||||
returnData.emailNotify = data.emailNotify
|
||||
returnData.emailOTL = data.emailOTL
|
||||
return returnData
|
||||
}
|
||||
const remove = (publicID) => {
|
||||
for(let i = 0; i < _cache.length; i++) {
|
||||
if(_cache[i].publicID == publicID) {
|
||||
_cache.splice(i, 1)
|
||||
save()
|
||||
for(let i = _readLinks.length - 1; i >= 0; i--) {
|
||||
if(_readLinks[i].publicID == publicID) {
|
||||
_readLinks.splice(i, 1)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
const getLink = (publicID) => {
|
||||
const data = _cache.find((item) => item.publicID === publicID)
|
||||
return createLinkData(data)
|
||||
}
|
||||
const createOTLink = (publicID) => {
|
||||
const value = _cache.find((item) => item.publicID === publicID)
|
||||
if(value) {
|
||||
const readLink = {
|
||||
date: new Date(),
|
||||
otLinkID: newRandomString(newRandomNumber(_config.vault.otLinkID.minLength, _config.vault.otLinkID.maxLength)),
|
||||
publicID: publicID,
|
||||
}
|
||||
_readLinks.push(readLink)
|
||||
return {
|
||||
otLinkID: readLink.otLinkID
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
const removeOTL = (otLinkID) => {
|
||||
for(let i = _readLinks.length - 1; i >= 0; i--) {
|
||||
if(_readLinks[i].otLinkID == otLinkID) {
|
||||
_readLinks.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
const read = (otLinkID) => {
|
||||
const linkValue = _readLinks.find((item) => item.otLinkID === otLinkID)
|
||||
if(linkValue) {
|
||||
removeOTL(otLinkID)
|
||||
const value = _cache.find((item) => item.publicID === linkValue.publicID)
|
||||
if(value) {
|
||||
value.readCount += 1
|
||||
if(value.readCount < value.maxReads) {
|
||||
save()
|
||||
} else {
|
||||
remove(value.publicID)
|
||||
}
|
||||
value.timeLeft = calcTimeLeft(value)
|
||||
value.readsLeft = calcReadsLeft(value)
|
||||
if(value.emailNotify) emailReadNotify(value)
|
||||
const returnValue = structuredClone(value)
|
||||
returnValue.secret = decrypt(returnValue.secret)
|
||||
return returnValue
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
create,
|
||||
getLink,
|
||||
createOTLink,
|
||||
read,
|
||||
}
|
99
src/html/style.css
Normal file
99
src/html/style.css
Normal file
@ -0,0 +1,99 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
scrollbar-color: #3eb0ec #232629;
|
||||
scrollbar-width: 10px;
|
||||
}
|
||||
body {
|
||||
font: 400 12pt "Droid Sans", sans-serif;
|
||||
background-color: #454c53;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: #e3d1c7;
|
||||
}
|
||||
input {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
input:focus, textarea:focus {
|
||||
outline: 1px solid #3eb0ec;
|
||||
}
|
||||
input:invalid, textarea:invalid {
|
||||
border: 1px dashed red;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: #e3d1c7;
|
||||
}
|
||||
|
||||
.left {
|
||||
text-align: left;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.fullwidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inlineleftspace {
|
||||
margin-left: 4px ;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
.form {
|
||||
max-width: 800px;
|
||||
min-width: 200px;
|
||||
margin: auto;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
.panel {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.controlpanel {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
|
||||
.titlebox {
|
||||
font-size:8pt;
|
||||
margin-left: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.textbox {
|
||||
border: none;
|
||||
color: #e3d1c7;
|
||||
background-color: #31363b;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.messagebox {
|
||||
border: none;
|
||||
color: #e3d1c7;
|
||||
background-color: #31363b;
|
||||
padding: 4px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #31363b;
|
||||
color: #e3d1c7;
|
||||
padding: 4px;
|
||||
border: solid 1px #2cc4db;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.button:hover {
|
||||
background-color: #2cc4db;
|
||||
color: #31363b;
|
||||
}
|
1198
src/package-lock.json
generated
Normal file
1198
src/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
src/package.json
Normal file
19
src/package.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "secret-sender",
|
||||
"version": "1.0.0",
|
||||
"main": "app.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"express": "^4.21.1",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^6.9.15",
|
||||
"pug": "^3.0.3"
|
||||
}
|
||||
}
|
12
src/pug/_main.pug
Normal file
12
src/pug/_main.pug
Normal file
@ -0,0 +1,12 @@
|
||||
doctype html
|
||||
html(lang='fr-FR')
|
||||
head
|
||||
meta(name='viewport', content='width=device-width, height=device-height, initial-scale=1, maximum-scale=1')
|
||||
meta(charset='utf-8')
|
||||
title #{title}
|
||||
link(rel="stylesheet", href="/style.css")
|
||||
block style
|
||||
block script
|
||||
block head
|
||||
body
|
||||
block content
|
86
src/pug/create.pug
Normal file
86
src/pug/create.pug
Normal file
@ -0,0 +1,86 @@
|
||||
extends _main
|
||||
|
||||
block script
|
||||
script.
|
||||
const post = (path, value) => {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'post'
|
||||
form.action = path
|
||||
document.body.appendChild(form)
|
||||
for (const key in value) {
|
||||
const formField = document.createElement('input')
|
||||
formField.type = 'hidden'
|
||||
formField.name = key
|
||||
formField.value = value[key]
|
||||
form.appendChild(formField)
|
||||
}
|
||||
form.submit()
|
||||
}
|
||||
const createLink = () => {
|
||||
const data = {}
|
||||
if(SenderName.value != '') data.senderName = SenderName.value
|
||||
if(Subject.value != '') data.subject = Subject.value
|
||||
if(Message.value != '') data.message = Message.value
|
||||
if(Secret.value != '') data.secret = Secret.value
|
||||
if(EMailNotify.value != '') data.emailNotify = EMailNotify.value
|
||||
data.expireDays = ExpireDays.value
|
||||
data.expireHours = ExpireHours.value
|
||||
data.maxReads = MaxReads.value
|
||||
if(EMailOTL.value != '') data.emailOTL = EMailOTL.value
|
||||
const path = window.location.pathname
|
||||
fetch(path, {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
}).then((res) => {
|
||||
res.json().then((value) => {
|
||||
if(value) post('/newlink', value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
block content
|
||||
div.center
|
||||
div.form
|
||||
h1 Nouveau lien
|
||||
div.panel.left
|
||||
div.titlebox Expéditeur
|
||||
div
|
||||
input#SenderName(type="text", placeholder="(Optionel)").textbox.fullwidth
|
||||
div.panel.left
|
||||
div.titlebox Sujet
|
||||
div
|
||||
input#Subject(type="text", placeholder="(Optionel)").textbox.fullwidth
|
||||
div.panel.left
|
||||
div.titlebox Message
|
||||
div
|
||||
textarea#Message(rows="3", placeholder="(Optionel)").messagebox.fullwidth
|
||||
div.panel.left
|
||||
div.titlebox Secret (*)
|
||||
div
|
||||
textarea#Secret(rows="4", placeholder="Votre secret", required).messagebox.fullwidth.monospace
|
||||
div.panel.left
|
||||
div.titlebox E-Mail (Notifier)
|
||||
div
|
||||
input#EMailNotify(type="email", placeholder="(Optionel) Votre e-mail pour être notifié").messagebox.fullwidth
|
||||
div.panel.left
|
||||
div.titlebox Expiration
|
||||
table.fullwidth
|
||||
tr
|
||||
td
|
||||
input#ExpireDays(type="number", value="2", min="0", max="13").textbox.small
|
||||
span.inlineleftspace jour(s)
|
||||
input#ExpireHours(type="number", value="1", min="1", max="23").textbox.small.inlineleftspace
|
||||
span.inlineleftspace heure(s)
|
||||
td.right
|
||||
input#MaxReads(type="number", value="1", min="0", max="999").textbox.small.inlineleftspace
|
||||
span.inlineleftspace affichage(s)
|
||||
div.panel.left
|
||||
div.titlebox E-Mail (OTL)
|
||||
div
|
||||
input#EMailOTL(type="email", placeholder="(Optionel) E-mail du destinatire pour OTL").messagebox.fullwidth
|
||||
div.panel.controlpanel
|
||||
button(onclick="createLink()").button.fullwidth Créer le lien
|
5
src/pug/index.pug
Normal file
5
src/pug/index.pug
Normal file
@ -0,0 +1,5 @@
|
||||
extends _main
|
||||
|
||||
block content
|
||||
div.center
|
||||
h1 Secret Sender
|
54
src/pug/link.pug
Normal file
54
src/pug/link.pug
Normal file
@ -0,0 +1,54 @@
|
||||
extends _main
|
||||
|
||||
block script
|
||||
script.
|
||||
const requestLink = (publicID) => {
|
||||
const path = "/requestotl"
|
||||
fetch(path, {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ publicID: publicID }),
|
||||
}).then((res) => {
|
||||
res.json().then((value) => {
|
||||
window.location = '/read/' + value.otLinkID
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
block content
|
||||
div.center
|
||||
div.form
|
||||
h1 Accéder
|
||||
if link.senderName
|
||||
div.panel.left
|
||||
div.titlebox Expéditeur
|
||||
div.messagebox=link.senderName
|
||||
if link.subject
|
||||
div.panel.left
|
||||
div.titlebox Sujet
|
||||
div.messagebox=link.subject
|
||||
if link.message
|
||||
div.panel.left
|
||||
div.titlebox Message
|
||||
div.messagebox!=link.message.replaceAll('\n', '<br />')
|
||||
div.panel
|
||||
table.fullwidth
|
||||
tr
|
||||
td.left
|
||||
if link.timeLeft > 2820
|
||||
div.infotext Expires dans #{Math.round(link.timeLeft / 60 / 24)} jours
|
||||
else if link.timeLeft > 120
|
||||
div.infotext Expires dans #{Math.round(link.timeLeft / 60)} heures
|
||||
else
|
||||
div.infotext Expires dans #{link.timeLeft} min
|
||||
td.right
|
||||
if link.readsLeft > 0
|
||||
div.infotext #{link.readsLeft} affichage(s) restant(s)
|
||||
div.panel.controlpanel
|
||||
if link.emailOTL
|
||||
button(onclick="requestLink('" + link.publicID + "')").button.fullwidth Demmander
|
||||
else
|
||||
button(onclick="requestLink('" + link.publicID + "')").button.fullwidth Afficher
|
43
src/pug/newlink.pug
Normal file
43
src/pug/newlink.pug
Normal file
@ -0,0 +1,43 @@
|
||||
extends _main
|
||||
|
||||
block content
|
||||
div.center
|
||||
div.form
|
||||
h1 Nouveau lien
|
||||
div.panel
|
||||
div.messagebox.left.overflow.nowrap #{linkUrl}
|
||||
if link.senderName
|
||||
div.panel.left
|
||||
div.titlebox Expéditeur
|
||||
div.messagebox=link.senderName
|
||||
if link.subject
|
||||
div.panel.left
|
||||
div.titlebox Sujet
|
||||
div.messagebox=link.subject
|
||||
if link.message
|
||||
div.panel.left
|
||||
div.titlebox Message
|
||||
div.messagebox!=link.message.replaceAll('\n', '<br />')
|
||||
if link.emailNotify
|
||||
div.panel.left
|
||||
div.titlebox Notifier
|
||||
div.messagebox=link.emailNotify
|
||||
if link.emailOTL
|
||||
div.panel.left
|
||||
div.titlebox OTL
|
||||
div.messagebox=link.emailOTL
|
||||
div.panel
|
||||
table.fullwidth
|
||||
tr
|
||||
td.left
|
||||
if link.timeLeft > 2820
|
||||
div.infotext Expires dans #{Math.round(link.timeLeft / 60 / 24)} jours
|
||||
else if link.timeLeft > 120
|
||||
div.infotext Expires dans #{Math.round(link.timeLeft / 60)} heures
|
||||
else
|
||||
div.infotext Expires dans #{link.timeLeft} min
|
||||
td.right
|
||||
if link.readsLeft > 0
|
||||
div.infotext #{link.readsLeft} affichage(s) restant(s)
|
||||
div.panel.controlpanel
|
||||
button(onclick="navigator.clipboard.writeText('" + linkUrl + "')").button.fullwidth Copier le lien
|
37
src/pug/read.pug
Normal file
37
src/pug/read.pug
Normal file
@ -0,0 +1,37 @@
|
||||
extends _main
|
||||
|
||||
block content
|
||||
div.center
|
||||
div.form
|
||||
h1 Secret
|
||||
if secret.senderName
|
||||
div.panel.left
|
||||
div.titlebox Expéditeur
|
||||
div.messagebox=secret.senderName
|
||||
if secret.subject
|
||||
div.panel.left
|
||||
div.titlebox Sujet
|
||||
div.messagebox=secret.subject
|
||||
if secret.message
|
||||
div.panel.left
|
||||
div.titlebox Message
|
||||
div.messagebox!=secret.message.replaceAll('\n', '<br />')
|
||||
div.panel.left
|
||||
div.titlebox Secret
|
||||
div#Secret.messagebox.monospace!=secret.secret.replaceAll('\n', '<br />')
|
||||
if secret.readsLeft > 0 && secret.timeLeft > 0
|
||||
div.panel
|
||||
table.fullwidth
|
||||
tr
|
||||
td.left
|
||||
if secret.timeLeft > 2820
|
||||
div.infotext Expires dans #{Math.round(secret.timeLeft / 60 / 24)} jours
|
||||
else if secret.timeLeft > 120
|
||||
div.infotext Expires dans #{Math.round(secret.timeLeft / 60)} heures
|
||||
else
|
||||
div.infotext Expires dans #{secret.timeLeft} min
|
||||
td.right
|
||||
if secret.readsLeft > 0
|
||||
div.infotext #{secret.readsLeft} affichage(s) restant(s)
|
||||
div.panel.controlpanel
|
||||
button(onclick="navigator.clipboard.writeText(Secret.innerText)").button.fullwidth Copier le secret
|
44
test-api.sh
Executable file
44
test-api.sh
Executable file
@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
HTTPPORT=3081
|
||||
EMAILNOTIFY="${1}"
|
||||
EMAILOTP="${2}"
|
||||
|
||||
echo "Create secret ..."
|
||||
CREATEDATA='{'
|
||||
CREATEDATA+=' "senderName": "Me"'
|
||||
CREATEDATA+=', "subject": "Votre code"'
|
||||
CREATEDATA+=', "message": "Hello"'
|
||||
CREATEDATA+=', "secret": "My Secret"'
|
||||
if [ "${EMAILNOTIFY}" != "" ]; then
|
||||
CREATEDATA+=', "emailNotify": "'${EMAILNOTIFY}'"'
|
||||
fi
|
||||
if [ "${EMAILOTP}" != "" ]; then
|
||||
CREATEDATA+=', "emailOTP": "'${EMAILOTP}'"'
|
||||
fi
|
||||
CREATEDATA+=' }'
|
||||
echo "${CREATEDATA}"
|
||||
NEWSECRET=$(curl -s -X POST -H "Content-Type: application/json" \
|
||||
-d "${CREATEDATA}" \
|
||||
http://localhost:${HTTPPORT}/api/create)
|
||||
|
||||
echo "New secret :"
|
||||
echo "${NEWSECRET}" | jq
|
||||
|
||||
PUBLICID=$(echo "${NEWSECRET}" | jq -r .publicID)
|
||||
|
||||
echo ""
|
||||
echo "Get Link"
|
||||
curl -s http://localhost:${HTTPPORT}/api/getlink/"${PUBLICID}" | jq
|
||||
|
||||
echo ""
|
||||
echo "Create OTLink"
|
||||
OTLINKID=$(curl -s http://localhost:${HTTPPORT}/api/createOTLink/"${PUBLICID}" | jq -r .otLinkID)
|
||||
echo "${OTLINKID}"
|
||||
|
||||
echo ""
|
||||
echo "Read secret first time :"
|
||||
curl -s http://localhost:${HTTPPORT}/api/read/"${OTLINKID}" | jq
|
||||
|
||||
echo "Read secret second time :"
|
||||
curl -s http://localhost:${HTTPPORT}/api/read/"${OTLINKID}" | jq
|
Loading…
Reference in New Issue
Block a user