diff --git a/app.ar b/app.ar index bf0e87e..d6e4abf 100644 --- a/app.ar +++ b/app.ar @@ -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() \ No newline at end of file +app.run() +term.timeEnd("load") \ No newline at end of file diff --git a/database.ar b/database.ar new file mode 100644 index 0000000..15bc718 --- /dev/null +++ b/database.ar @@ -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")) \ No newline at end of file diff --git a/render.ar b/render.ar new file mode 100644 index 0000000..2c7a973 --- /dev/null +++ b/render.ar @@ -0,0 +1,17 @@ +import "bleach.ar" as bleach +import "database" as * + +let navbar(acc) = do + let links = [] + if (acc) do + links.append(''+bleach.clean(acc.username)+'') + else do + links.append('Login') + links.append('Register') + links.append('Github') + return '
' + +let middleware(req,res) = do + let acc = account.getByToken(req.cookies.get("token", "")) + res.templateValues.navbar = navbar(acc) + res.next() \ No newline at end of file diff --git a/routes/404.ar b/routes/404.ar index 95c27b2..41842c0 100644 --- a/routes/404.ar +++ b/routes/404.ar @@ -1,4 +1,5 @@ -import "../server" as * +import "server" as * +import "database" as * let err404(req, res) = do res.code = 404 diff --git a/routes/isotope/download.ar b/routes/isotope/download.ar index 6738ddc..3713424 100644 --- a/routes/isotope/download.ar +++ b/routes/isotope/download.ar @@ -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) diff --git a/routes/isotope/push.ar b/routes/isotope/push.ar index 49e977a..4b6b68f 100644 --- a/routes/isotope/push.ar +++ b/routes/isotope/push.ar @@ -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) \ No newline at end of file diff --git a/routes/isotope/search.ar b/routes/isotope/search.ar index c4f08ba..2216cb1 100644 --- a/routes/isotope/search.ar +++ b/routes/isotope/search.ar @@ -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 diff --git a/routes/website/account/account.ar b/routes/website/account/account.ar new file mode 100644 index 0000000..b35aa61 --- /dev/null +++ b/routes/website/account/account.ar @@ -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) \ No newline at end of file diff --git a/routes/website/account/login.ar b/routes/website/account/login.ar new file mode 100644 index 0000000..656a821 --- /dev/null +++ b/routes/website/account/login.ar @@ -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) \ No newline at end of file diff --git a/routes/website/account/logout.ar b/routes/website/account/logout.ar new file mode 100644 index 0000000..e745e32 --- /dev/null +++ b/routes/website/account/logout.ar @@ -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) \ No newline at end of file diff --git a/routes/website/home.ar b/routes/website/home.ar index 89b4c30..d42ccba 100644 --- a/routes/website/home.ar +++ b/routes/website/home.ar @@ -1,4 +1,5 @@ -import "../server" as * +import "server" as * +import "database" as * let home(req, res) = do res.renderTemplate("index.html", {}) diff --git a/routes/website/package.ar b/routes/website/package.ar index cd2ca16..4d451d1 100644 --- a/routes/website/package.ar +++ b/routes/website/package.ar @@ -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] diff --git a/routes/website/search.ar b/routes/website/search.ar index 4e0184d..d3684e0 100644 --- a/routes/website/search.ar +++ b/routes/website/search.ar @@ -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(""+bleach.clean(name)+"") 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) \ No newline at end of file diff --git a/save.ar b/save.ar deleted file mode 100644 index c3ecdea..0000000 --- a/save.ar +++ /dev/null @@ -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() \ No newline at end of file diff --git a/server.ar b/server.ar index 2701710..27daf87 100644 --- a/server.ar +++ b/server.ar @@ -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 \ No newline at end of file diff --git a/static/styles.css b/static/styles.css index 1b22f56..35982f7 100644 --- a/static/styles.css +++ b/static/styles.css @@ -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); } @@ -171,4 +179,46 @@ pre { -moz-user-select: none; -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; } \ No newline at end of file diff --git a/template.html b/template.html index f983d1b..a692e84 100644 --- a/template.html +++ b/template.html @@ -48,14 +48,7 @@