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
+471 -97

No files matched your search

+8 -3
View File
@@ -1,20 +1,25 @@
term.time("load")
import "server" as *
import "config" as *
import "save"
import "database"
# website
import "routes/website/home"
import "routes/website/search"
import "routes/website/package"
# account
import "routes/website/account/login"
import "routes/website/account/logout"
import "routes/website/account/account"
# isotope
import "routes/isotope/push"
import "routes/isotope/search"
import "routes/isotope/download"
# error pages
import "routes/404"
term.timeEnd("load")
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
res.code = 404
+3 -3
View File
@@ -1,6 +1,6 @@
import "../server" as *
import "../config" as *
import "../save" as *
import "server" as *
import "config" as *
import "database" as *
let search(req, res) = do
let name = req.query.get("name", null)
+4 -4
View File
@@ -1,6 +1,6 @@
import "../server" as *
import "../randstr" as *
import "../save" as *
import "server" as *
import "randstr" as *
import "database" as *
let push(req, res) = do
let name = req.formdata.get("name", null)
@@ -35,6 +35,6 @@ let push(req, res) = do
package.order[version] = time.now().unix()
if (latest == "true") do
package.versions['latest'] = {redirect: version}
changes.n = changes.n + 1
makeChange()
res.send("OK")
app.route("/isotope-push", ['POST'], push)
+3 -3
View File
@@ -1,6 +1,6 @@
import "../server" as *
import "../config" as config
import "../save" as *
import "server" as *
import "config" as config
import "database" as *
import "url.ar" as urllib
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
res.renderTemplate("index.html", {})
+16 -6
View File
@@ -1,29 +1,39 @@
import "../server" as *
import "../save" as *
import "server" as *
import "database" as *
import "url.ar" as url
import "bleach.ar" as bleach
import "shlex.ar" as shlex
import "render" as render
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 commandName = name
if (version != "latest") commandName = commandName + "@"+version
let command = shlex.join(["isotope", "install", commandName])
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.renderTemplate("404.html", {})
return
if ("order" not in package) do
package.order = {}
changes.n = changes.n + 1
if (version not in package.versions) do
makeChange()
if (version not in package.versions && version != "latest") do
res.code = 404
res.renderTemplate("404.html", {})
return
let versions = []
let keys = package.versions.keys()
keys.sort()
keys.sort(true, (v) = package.order.get(v, 0))
for (i from 0 to keys.length) do
let versioni = keys[i]
+5 -3
View File
@@ -1,9 +1,11 @@
import "../server" as *
import "../save" as *
import "server" as *
import "database" as *
import "url.ar" as url
import "bleach.ar" as bleach
import "render" as renderer
let search(req, res) = do
let acc = account.getByToken(req.cookies.get("token", ""))
let query = req.query.get("q", "")
let keys = packages.keys()
keys.sort()
@@ -15,5 +17,5 @@ let search(req, res) = do
if (query.lower() in name.lower()) do
render.append("<a href='/package?"+url.queryStr({name:name})+"'>"+bleach.clean(name)+"</a>")
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)
-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 "render" as render
let app = http.server()
app.use(http.logger)
app.use(http.cookie)
app.use(http.formdata)
app.use(render.middleware)
app.use(http.static("/","static"))
app.port = 7030
+53 -3
View File
@@ -49,15 +49,23 @@ h6 {
list-style: none;
}
.navbar li a {
font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
.navbar a {
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-weight: 700;
color: var(--accent);
margin-left: 1rem;
}
.navbar li a:hover {
.navbar .links {
overflow: auto;
}
.navbar .links a:hover {
color: var(--primary);
}
@@ -172,3 +180,45 @@ pre {
-ms-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>
<body>
<div class="content">
<ul class="navbar">
<li>
<a href="/"><div class="logo"></div></a>
</li>
<li>
<a href="https://github.com/open-argon/Isotope">Github</a>
</li>
</ul>
{{%navbar}}
</div>
</body>
</html>
+1 -8
View File
@@ -48,14 +48,7 @@
</head>
<body>
<div class="content">
<ul class="navbar">
<li>
<a href="/"><div class="logo"></div></a>
</li>
<li>
<a href="https://github.com/open-argon/Isotope">Github</a>
</li>
</ul>
{{%navbar}}
<div>
<h1>Error 404</h1>
<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>
<body>
<div class="content">
<ul class="navbar">
<li>
<a href="/"><div class="logo"></div></a>
</li>
<li>
<a href="https://github.com/open-argon/Isotope">Github</a>
</li>
</ul>
{{%navbar}}
<div class="welcome">
<h1>
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>
<body>
<div class="content">
<ul class="navbar">
<li>
<a href="/"><div class="logo"></div></a>
</li>
<li>
<a href="https://github.com/open-argon/Isotope">Github</a>
</li>
</ul>
{{%navbar}}
<div>
<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>
<div class="results">{{%versions}}</div>
</div>
+1 -8
View File
@@ -48,14 +48,7 @@
</head>
<body>
<div class="content">
<ul class="navbar">
<li>
<a href="/"><div class="logo"></div></a>
</li>
<li>
<a href="https://github.com/open-argon/Isotope">Github</a>
</li>
</ul>
{{%navbar}}
<div>
<form action="/search" method="GET" class="search">
<input