start adding account features

This commit is contained in:
William Bell committed 2023-07-31 20:10:19 +02:00
1 parent 7b29ce72ea
commit 30ca8f4fcd
23 files changed
+472 -98

No files matched your search

+9 -4
View File
@@ -1,20 +1,25 @@
term.time("load") term.time("load")
import "server" as * import "server" as *
import "config" as * import "config" as *
import "save" import "database"
# website # website
import "routes/website/home" import "routes/website/home"
import "routes/website/search" import "routes/website/search"
import "routes/website/package" import "routes/website/package"
# account
import "routes/website/account/login"
import "routes/website/account/logout"
import "routes/website/account/account"
# isotope # isotope
import "routes/isotope/push" import "routes/isotope/push"
import "routes/isotope/search" import "routes/isotope/search"
import "routes/isotope/download" import "routes/isotope/download"
# error pages
import "routes/404" import "routes/404"
term.timeEnd("load") app.run()
app.run() term.timeEnd("load")
+176
View File
@@ -0,0 +1,176 @@
let packages = null
let accounts = null
let indexes = {username: {}, email: {}, id: {}}
let savePath = path.join([program.cwd, "packages-db.json"])
let changes = {n: 0, last: 0}
let makeChange() = changes.n = changes.n + 1
let randstr(length) = do
let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
let str = []
for (i from 0 to length) do
str.append(chars[random.int(0, chars.length-1)])
return str.join("")
let load() = do
if (path.exists(savePath)) do
let file = file.read(savePath)
data = file.json()
if ("packages" not in data) do
packages = data
accounts = []
makeChange()
else do
packages = data.packages
accounts = data.accounts
file.close()
let forceSave() = do
makeChange()
save()
let save() = do
if (changes.n == changes.last) return
changes.last = changes.n
let file = file.write(savePath)
file.json({packages: packages, accounts: accounts})
file.close()
let saveThread() = do
forever do
try do
save()
catch (e) do
term.log(e)
time.snooze(10)
let account = {}
account.addToIndexes(acc, index) = do
indexes.username[acc.username] = index
indexes.email[acc.email] = index
indexes.id[acc.id] = index
account.makeId() = do
let id = "u-"+randstr(32)
if (account.getById(id) != null) return account.makeId()
return id
account.getById(id) = do
if (id in indexes.id) return accounts[indexes.id[id]]
for (i from 0 to accounts.length) do
if (accounts[i].id == id) do
account.addToIndexes(accounts[i], i)
return accounts[i]
return null
account.getByUsername(username) = do
if (username in indexes.username) return accounts[indexes.username[username]]
for (i from 0 to accounts.length) do
if (accounts[i].username == username) do
account.addToIndexes(accounts[i], i)
return accounts[i]
return null
account.getByEmail(email) = do
if (email in indexes.email) return accounts[indexes.email[email]]
for (i from 0 to accounts.length) do
if (accounts[i].email == email) do
account.addToIndexes(accounts[i], i)
return accounts[i]
return null
account.getByToken(token) = do
let split = token.splitN("|", 2)
if (split.length != 2) return null
let id = split[0]
let token = split[1]
let acc = account.getById(id)
if (acc == null) return null
if (token not in acc.tokens) return null
return acc
account.deleteToken(token) = do
let split = token.split(":")
if (split.length != 2) return false
let id = split[0]
let token = split[1]
let acc = account.getById(id)
if (acc == null) return false
if (token not in acc.tokens) return false
acc.tokens = acc.tokens.filter((t) = t != token)
forceSave()
return true
account.makeToken(acc) = do
let token = randstr(100)
acc.tokens.append(token)
makeChange()
return acc.id+"|"+token
account.saltHash(password, salt) = sha256(password+salt)
account.validPassword(password) = do
if (password.length < 8) return "Password must be at least 8 characters"
# include at least one number
let hasNumber = false
for (i from 0 to password.length) do
if (password[i] in "0123456789") do
hasNumber = true
break
if (not hasNumber) return "Password must include at least one number"
# include at least one uppercase letter
if (password.lower() == password) return "Password must include at least one uppercase letter"
# include at least one lowercase letter
if (password.upper() == password) return "Password must include at least one lowercase letter"
return null
account.validEmail(email) = do
let split = email.split("@")
if (split.length != 2) return false
if (split[0].length == 0) return false
if (" " in split[0]) return false
if (split[1].length == 0) return false
if (" " in split[1]) return false
if (split[1].split(".").length < 2) return false
return true
account.validUsername(username) = do
if (username.length < 3) return false
if (username.length > 32) return false
for (i from 0 to username.length) do
if (username[i] not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_") return false
return true
account.create(username, email, password) = do
if (not account.validUsername(username)) return {error: "Invalid username"}
if (not account.validEmail(email)) return {error: "Invalid email"}
if (account.getByUsername(username) != null) return {error: "Username already exists"}
if (account.getByEmail(email) != null) return {error: "Email already exists"}
if ((let error = account.validPassword(password)) != null) return {error: error}
let id = account.makeId()
let salt = randstr(100)
let hash = account.saltHash(password, salt)
let acc = {id: id, username: username, email:email, password: hash, salt: salt, tokens: []}
accounts.append(acc)
forceSave()
return acc
account.login(user, password) = do
let acc = account.getByUsername(user)
if (acc == null) do
acc = account.getByEmail(user)
if (acc == null) return {error: "Icorrect username or email"}
let hash = account.saltHash(password, acc.salt)
if (hash != acc.password) return {error: "Incorrect password"}
return {token: account.makeToken(acc)}
load()
t = thread(saveThread)
t.start()
term.log(account.create("test1", "sussy@gmail.com", "Password123"))
+17
View File
@@ -0,0 +1,17 @@
import "bleach.ar" as bleach
import "database" as *
let navbar(acc) = do
let links = []
if (acc) do
links.append('<a href="/account">'+bleach.clean(acc.username)+'</a>')
else do
links.append('<a href="/login">Login</a>')
links.append('<a href="/register">Register</a>')
links.append('<a href="https://github.com/open-argon/Isotope">Github</a>')
return '<div class="navbar"><a href="/"><div class="logo"></div></a><div class="links">'+links.join("")+'</div></div>'
let middleware(req,res) = do
let acc = account.getByToken(req.cookies.get("token", ""))
res.templateValues.navbar = navbar(acc)
res.next()
+2 -1
View File
@@ -1,4 +1,5 @@
import "../server" as * import "server" as *
import "database" as *
let err404(req, res) = do let err404(req, res) = do
res.code = 404 res.code = 404
+3 -3
View File
@@ -1,6 +1,6 @@
import "../server" as * import "server" as *
import "../config" as * import "config" as *
import "../save" as * import "database" as *
let search(req, res) = do let search(req, res) = do
let name = req.query.get("name", null) let name = req.query.get("name", null)
+4 -4
View File
@@ -1,6 +1,6 @@
import "../server" as * import "server" as *
import "../randstr" as * import "randstr" as *
import "../save" as * import "database" as *
let push(req, res) = do let push(req, res) = do
let name = req.formdata.get("name", null) let name = req.formdata.get("name", null)
@@ -35,6 +35,6 @@ let push(req, res) = do
package.order[version] = time.now().unix() package.order[version] = time.now().unix()
if (latest == "true") do if (latest == "true") do
package.versions['latest'] = {redirect: version} package.versions['latest'] = {redirect: version}
changes.n = changes.n + 1 makeChange()
res.send("OK") res.send("OK")
app.route("/isotope-push", ['POST'], push) app.route("/isotope-push", ['POST'], push)
+3 -3
View File
@@ -1,6 +1,6 @@
import "../server" as * import "server" as *
import "../config" as config import "config" as config
import "../save" as * import "database" as *
import "url.ar" as urllib import "url.ar" as urllib
let search(req, res) = do let search(req, res) = do
+9
View File
@@ -0,0 +1,9 @@
import "server" as *
import "database" as *
import "url.ar" as url
let account(req, res) = do
let acc = account.getByToken(req.cookies.get("token", ""))
if (not acc) return res.redirect("/login?"+url.queryStr({to:req.path}))
res.renderTemplate("account.html", {})
app.route("/account", null, account)
+22
View File
@@ -0,0 +1,22 @@
import "server" as *
import "database" as *
let login(req, res) = do
let redirect = req.query.get("to", "/")
if (redirect == "/login") redirect = "/"
let acc = account.getByToken(req.cookies.get("token", ""))
if (acc) do
return res.redirect(redirect)
else if (req.method == "POST") do
let user = req.formdata.get("user")
let password = req.formdata.get("password")
let errResp(err) = res.renderTemplate("login.html", {errorMsg:err, user:user})
if (not user) return errResp("Missing user")
else if (not password) return errResp("Missing password")
let acc = account.login(user, password)
if ("error" in acc) return errResp(acc.error)
res.setCookie("token", {value:acc.token, path:"/"})
res.redirect(redirect)
return
res.renderTemplate("login.html", {})
app.route("/login", ["GET", "POST"], login)
+10
View File
@@ -0,0 +1,10 @@
import "server" as *
import "database" as *
let logout(req, res) = do
let redirect = req.query.get("to", "/login")
if (redirect == "/logout") redirect = "/login"
account.deleteToken(req.cookies.get("token", ""))
res.deleteCookie("token")
res.redirect(redirect)
app.route("/logout", null, logout)
+2 -1
View File
@@ -1,4 +1,5 @@
import "../server" as * import "server" as *
import "database" as *
let home(req, res) = do let home(req, res) = do
res.renderTemplate("index.html", {}) res.renderTemplate("index.html", {})
+16 -6
View File
@@ -1,29 +1,39 @@
import "../server" as * import "server" as *
import "../save" as * import "database" as *
import "url.ar" as url import "url.ar" as url
import "bleach.ar" as bleach import "bleach.ar" as bleach
import "shlex.ar" as shlex import "shlex.ar" as shlex
import "render" as render
let package(req, res) = do let package(req, res) = do
let name = req.query.get("name", null) let acc = account.getByToken(req.cookies.get("token", ""))
let name = req.query.get("name")
if (not name) do
res.code = 404
res.renderTemplate("404.html", {})
return
let version = req.query.get("version", "latest") let version = req.query.get("version", "latest")
let commandName = name let commandName = name
if (version != "latest") commandName = commandName + "@"+version if (version != "latest") commandName = commandName + "@"+version
let command = shlex.join(["isotope", "install", commandName]) let command = shlex.join(["isotope", "install", commandName])
let package = packages.get(name) let package = packages.get(name)
if (package == null) do if (package == null && version == "latest") do
package = {versions:{}, order:{}, name:name}
else if (package == null) do
res.code = 404 res.code = 404
res.renderTemplate("404.html", {}) res.renderTemplate("404.html", {})
return return
if ("order" not in package) do if ("order" not in package) do
package.order = {} package.order = {}
changes.n = changes.n + 1 makeChange()
if (version not in package.versions) do if (version not in package.versions && version != "latest") do
res.code = 404 res.code = 404
res.renderTemplate("404.html", {}) res.renderTemplate("404.html", {})
return return
let versions = [] let versions = []
let keys = package.versions.keys() let keys = package.versions.keys()
keys.sort()
keys.sort(true, (v) = package.order.get(v, 0)) keys.sort(true, (v) = package.order.get(v, 0))
for (i from 0 to keys.length) do for (i from 0 to keys.length) do
let versioni = keys[i] let versioni = keys[i]
+5 -3
View File
@@ -1,9 +1,11 @@
import "../server" as * import "server" as *
import "../save" as * import "database" as *
import "url.ar" as url import "url.ar" as url
import "bleach.ar" as bleach import "bleach.ar" as bleach
import "render" as renderer
let search(req, res) = do let search(req, res) = do
let acc = account.getByToken(req.cookies.get("token", ""))
let query = req.query.get("q", "") let query = req.query.get("q", "")
let keys = packages.keys() let keys = packages.keys()
keys.sort() keys.sort()
@@ -15,5 +17,5 @@ let search(req, res) = do
if (query.lower() in name.lower()) do if (query.lower() in name.lower()) do
render.append("<a href='/package?"+url.queryStr({name:name})+"'>"+bleach.clean(name)+"</a>") render.append("<a href='/package?"+url.queryStr({name:name})+"'>"+bleach.clean(name)+"</a>")
results = results + 1 results = results + 1
res.renderTemplate("search.html", {query:query, numOfResults:results, results:render.join("")}) res.renderTemplate("search.html", {query:query, numOfResults:results, results:render.join(""), navbar:renderer.navbar(acc)})
app.route("/search", null, search) app.route("/search", null, search)
-29
View File
@@ -1,29 +0,0 @@
let packages = {}
let savePath = path.join([program.cwd, "packages-db.json"])
let changes = {n: 0, last: 0}
let load() = do
if (path.exists(savePath)) do
let file = file.read(savePath)
packages = file.json()
file.close()
let save() = do
if (changes.n == changes.last) return
changes.last = changes.n
let file = file.write(savePath)
file.json(packages)
file.close()
let saveThread() = do
forever do
try do
save()
catch (e) do
term.log(e)
time.snooze(10)
load()
t = thread(saveThread)
t.start()
+3
View File
@@ -1,8 +1,11 @@
import "http.ar" as http import "http.ar" as http
import "render" as render
let app = http.server() let app = http.server()
app.use(http.logger) app.use(http.logger)
app.use(http.cookie)
app.use(http.formdata) app.use(http.formdata)
app.use(render.middleware)
app.use(http.static("/","static")) app.use(http.static("/","static"))
app.port = 7030 app.port = 7030
+53 -3
View File
@@ -49,15 +49,23 @@ h6 {
list-style: none; list-style: none;
} }
.navbar li a { .navbar a {
font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
text-decoration: none; text-decoration: none;
}
.navbar .links a {
font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
color: var(--accent); color: var(--accent);
margin-left: 1rem;
} }
.navbar li a:hover { .navbar .links {
overflow: auto;
}
.navbar .links a:hover {
color: var(--primary); color: var(--primary);
} }
@@ -171,4 +179,46 @@ pre {
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
}
.authForm {
width: 100%;
max-width: 500px;
margin: auto;
text-align: center;
}
.authForm .input {
width: 100%;
height: 50px;
margin: 1rem 0;
box-sizing: border-box;
border-radius: 25px;
color: var(--text);
background-color: var(--secondary);
border: 1px solid var(--primary);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
}
.authForm .submit {
width: 100px;
border-radius: 25px;
margin: auto;
color: var(--text);
background-color: var(--primary);
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 1rem;
}
.error {
color: red;
font-size: 0.8rem;
margin: 0.5rem 0;
} }
+1 -8
View File
@@ -48,14 +48,7 @@
</head> </head>
<body> <body>
<div class="content"> <div class="content">
<ul class="navbar"> {{%navbar}}
<li>
<a href="/"><div class="logo"></div></a>
</li>
<li>
<a href="https://github.com/open-argon/Isotope">Github</a>
</li>
</ul>
</div> </div>
</body> </body>
</html> </html>
+1 -8
View File
@@ -48,14 +48,7 @@
</head> </head>
<body> <body>
<div class="content"> <div class="content">
<ul class="navbar"> {{%navbar}}
<li>
<a href="/"><div class="logo"></div></a>
</li>
<li>
<a href="https://github.com/open-argon/Isotope">Github</a>
</li>
</ul>
<div> <div>
<h1>Error 404</h1> <h1>Error 404</h1>
<p>Page not found</p> <p>Page not found</p>
+55
View File
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Isotope</title>
<link rel="stylesheet" type="text/css" href="/styles.css" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="194x194"
href="/favicon-194x194.png"
/>
<link
rel="icon"
type="image/png"
sizes="192x192"
href="/android-chrome-192x192.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#0b1d28" />
<meta name="apple-mobile-web-app-title" content="Isotope" />
<meta name="application-name" content="Isotope" />
<meta name="msapplication-TileColor" content="#0b1d28" />
<meta name="msapplication-TileImage" content="/mstile-144x144.png" />
<meta name="theme-color" content="#0b1d28" />
</head>
<body>
<div class="content">
{{%navbar}}
</div>
</body>
</html>
+1 -8
View File
@@ -48,14 +48,7 @@
</head> </head>
<body> <body>
<div class="content"> <div class="content">
<ul class="navbar"> {{%navbar}}
<li>
<a href="/"><div class="logo"></div></a>
</li>
<li>
<a href="https://github.com/open-argon/Isotope">Github</a>
</li>
</ul>
<div class="welcome"> <div class="welcome">
<h1> <h1>
The official package manager for The official package manager for
+74
View File
@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Login - Isotope</title>
<link rel="stylesheet" type="text/css" href="/styles.css" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="194x194"
href="/favicon-194x194.png"
/>
<link
rel="icon"
type="image/png"
sizes="192x192"
href="/android-chrome-192x192.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#0b1d28" />
<meta name="apple-mobile-web-app-title" content="Isotope" />
<meta name="application-name" content="Isotope" />
<meta name="msapplication-TileColor" content="#0b1d28" />
<meta name="msapplication-TileImage" content="/mstile-144x144.png" />
<meta name="theme-color" content="#0b1d28" />
</head>
<body>
<div class="content">
{{%navbar}}
<form class="authForm" method="post" action="/login">
<h1>Login</h1>
<input
type="text"
name="user"
placeholder="Username or email address"
value="{{user}}"
required
class="input"
/>
<input
type="password"
name="password"
placeholder="Password"
required
class="input"
/>
<p class="error">{{errorMsg}}</p>
<input type="submit" value="Login" class="submit" />
</form>
</div>
</body>
</html>
+5 -9
View File
@@ -60,17 +60,13 @@
</head> </head>
<body> <body>
<div class="content"> <div class="content">
<ul class="navbar"> {{%navbar}}
<li>
<a href="/"><div class="logo"></div></a>
</li>
<li>
<a href="https://github.com/open-argon/Isotope">Github</a>
</li>
</ul>
<div> <div>
<h1>{{name}}@{{version}}</h1> <h1>{{name}}@{{version}}</h1>
<pre onclick="copyToClipboard(event.target.dataset.command)" data-command="{{command}}">{{command}}<div style="float: right;margin-left: 1rem;">📋 Click to Copy</div></pre> <pre
onclick="copyToClipboard(event.target.dataset.command)"
data-command="{{command}}"
>{{command}}<div style="float: right;margin-left: 1rem;">📋 Click to Copy</div></pre>
<h2>Versions</h2> <h2>Versions</h2>
<div class="results">{{%versions}}</div> <div class="results">{{%versions}}</div>
</div> </div>
+1 -8
View File
@@ -48,14 +48,7 @@
</head> </head>
<body> <body>
<div class="content"> <div class="content">
<ul class="navbar"> {{%navbar}}
<li>
<a href="/"><div class="logo"></div></a>
</li>
<li>
<a href="https://github.com/open-argon/Isotope">Github</a>
</li>
</ul>
<div> <div>
<form action="/search" method="GET" class="search"> <form action="/search" method="GET" class="search">
<input <input