Tested and created all endpoints, now entering production beta

This commit is contained in:
Tracker-Friendly 2024-05-03 17:54:50 +01:00
parent c7bd5ad1f6
commit 6c46254988
7 changed files with 374 additions and 72 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
.idea
config.ini
tmp/

View File

@ -2,4 +2,4 @@
secretkey: supersecretkey
port: 8080
host: localhost
dblocation: /var/lib/maddy/credentials.db
dblocation: tmp/credentials.db

BIN
ctamail

Binary file not shown.

250
main.go
View File

@ -5,9 +5,13 @@ import (
"crypto/rand"
"database/sql"
"encoding/base64"
"errors"
"fmt"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"centrifuge.hectabit.org/HectaBit/captcha"
@ -19,7 +23,7 @@ import (
"golang.org/x/crypto/bcrypt"
)
const salt_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
const salt_chars = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ23456789"
var DBLOCATION = "/var/lib/maddy/credentials.db"
@ -51,11 +55,11 @@ func computeBcrypt(cost int, pass string) (string, error) {
if err != nil {
return "", err
}
return string(hash), nil
return "bcrypt:" + string(hash), nil
}
func verifyBcrypt(pass, hashSalt string) error {
return bcrypt.CompareHashAndPassword([]byte(hashSalt), []byte(pass))
func verifyBcrypt(hash, pass string) error {
return bcrypt.CompareHashAndPassword([]byte(strings.TrimPrefix(hash, "bcrypt:")), []byte(pass))
}
func main() {
@ -99,7 +103,7 @@ func main() {
c.Next()
})
router.GET("/login", func(c *gin.Context) {
router.GET("/signup", func(c *gin.Context) {
session := sessions.Default(c)
sessionid := genSalt(512)
session.Options(sessions.Options{
@ -107,7 +111,7 @@ func main() {
})
data, err := captcha.New(500, 100)
if err != nil {
fmt.Println("[ERROR] Failed to generate captcha at", time.Now().Unix(), err)
fmt.Println("[ERROR] Failed to generate captcha at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
c.String(500, "Failed to generate captcha")
return
}
@ -115,39 +119,54 @@ func main() {
session.Set("id", sessionid)
err = session.Save()
if err != nil {
fmt.Println("[ERROR] Failed to save session in /login at", time.Now().Unix(), err)
fmt.Println("[ERROR] Failed to save session in /login at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
c.String(500, "Failed to save session")
return
}
var b64bytes bytes.Buffer
err = data.WriteImage(&b64bytes)
if err != nil {
fmt.Println("[ERROR] Failed to encode captcha at", time.Now().Unix(), err)
fmt.Println("[ERROR] Failed to encode captcha at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
c.String(500, "Failed to encode captcha")
return
}
c.HTML(200, "main.html", gin.H{
c.HTML(200, "signup.html", gin.H{
"captcha_image": base64.StdEncoding.EncodeToString(b64bytes.Bytes()),
"unique_token": sessionid,
})
})
router.GET("/account", func(c *gin.Context) {
loggedin, err := c.Cookie("loggedin")
if errors.Is(err, http.ErrNoCookie) || loggedin != "true" {
c.HTML(200, "login.html", gin.H{})
return
} else {
c.HTML(200, "dashboard.html", gin.H{})
}
})
router.POST("/api/signup", func(c *gin.Context) {
err := c.Request.ParseForm()
var data map[string]interface{}
err := c.ShouldBindJSON(&data)
if err != nil {
c.String(400, "Failed to parse form")
c.JSON(400, gin.H{"error": "Invalid JSON"})
return
}
data := c.Request.Form
session := sessions.Default(c)
if data.Get("captcha") != session.Get("captcha") {
c.HTML(200, "badcaptcha.html", gin.H{})
if data["unique_token"].(string) != session.Get("id") {
c.HTML(403, "badtoken.html", gin.H{})
return
}
if data.Get("unique_token") != session.Get("id") {
c.HTML(200, "badtoken.html", gin.H{})
if data["captcha"].(string) != session.Get("captcha") {
c.HTML(400, "badcaptcha.html", gin.H{})
return
}
if !regexp.MustCompile(`^[a-zA-Z0-9.]+$`).MatchString(data["username"].(string)) {
c.String(402, "Invalid username")
return
}
@ -156,7 +175,7 @@ func main() {
err = session.Save()
if err != nil {
fmt.Println("[ERROR] Failed to save session in /api/signup at", time.Now().Unix(), err)
fmt.Println("[ERROR] Failed to save session in /api/signup at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
c.String(500, "Failed to save session")
return
}
@ -165,26 +184,211 @@ func main() {
defer func(conn *sql.DB) {
err := conn.Close()
if err != nil {
fmt.Println("[ERROR] Failed to defer database connection at", time.Now().Unix(), err)
fmt.Println("[ERROR] Failed to defer database connection in /api/signup at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
c.String(500, "Failed to defer database connection")
return
}
}(conn)
hashedpass, err := computeBcrypt(10, data.Get("password"))
hashedpass, err := computeBcrypt(10, data["password"].(string))
if err != nil {
fmt.Println("[ERROR] Failed to hash password connection at", time.Now().Unix(), err)
fmt.Println("[ERROR] Failed to hash password in /api/signup at", time.Now().Unix(), err)
c.String(500, "Failed to hash password")
return
}
_, err = conn.Exec("INSERT INTO passwords (key, value) VALUES (?, ?)", data.Get("username"), hashedpass)
_, err = conn.Exec("INSERT INTO passwords (key, value) VALUES (?, ?)", data["username"].(string), hashedpass)
if err != nil {
c.HTML(500, "taken.html", gin.H{})
c.String(501, "Username taken")
return
}
c.HTML(200, "postsignup.html", gin.H{})
c.String(200, "Success")
})
router.POST("/api/login", func(c *gin.Context) {
var data map[string]interface{}
err := c.ShouldBindJSON(&data)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid JSON"})
return
}
conn := get_db_connection()
defer func(conn *sql.DB) {
err := conn.Close()
if err != nil {
fmt.Println("[ERROR] Failed to defer database connection at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
c.String(500, "Failed to defer database connection")
return
}
}(conn)
var rows string
err = conn.QueryRow("SELECT value FROM passwords WHERE key = ?", data["username"].(string)).Scan(&rows)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
c.String(401, "Invalid username")
return
} else {
fmt.Println("[ERROR] Failed to query database in /api/login at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
c.String(500, "Failed to query database")
return
}
}
err = verifyBcrypt(rows, data["password"].(string))
if err != nil {
c.String(403, "Password is incorrect")
return
}
c.JSON(200, gin.H{"password": rows})
})
router.POST("/api/deleteacct", func(c *gin.Context) {
var data map[string]interface{}
err := c.ShouldBindJSON(&data)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid JSON"})
return
}
conn := get_db_connection()
defer func(conn *sql.DB) {
err := conn.Close()
if err != nil {
fmt.Println("[ERROR] Failed to defer database connection in /api/deleteacct at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
c.String(500, "Failed to defer database connection")
return
}
}(conn)
result, err := conn.Exec("DELETE FROM passwords WHERE key = ? AND value = ?", data["username"].(string), data["password"].(string))
if err != nil {
fmt.Println("[ERROR] Failed to query database in /api/deleteacct at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
c.String(500, "Failed to query database")
} else {
rowsaffected, err := result.RowsAffected()
if err != nil {
fmt.Println("[ERROR] Failed to get rows affected in /api/deleteacct at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
c.String(500, "Failed to get rows affected")
} else {
if rowsaffected == int64(0) {
c.String(401, "Invalid username or password")
} else {
c.String(200, "Success")
}
}
}
})
router.POST("/api/changepass", func(c *gin.Context) {
var data map[string]interface{}
err := c.ShouldBindJSON(&data)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid JSON"})
return
}
conn := get_db_connection()
defer func(conn *sql.DB) {
err := conn.Close()
if err != nil {
fmt.Println("[ERROR] Failed to defer database connection in /api/changepass at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
c.String(500, "Failed to defer database connection")
return
}
}(conn)
newhash, err := computeBcrypt(10, data["newpass"].(string))
if err != nil {
fmt.Println("[ERROR] Failed to hash password in /api/changepass at", time.Now().Unix(), err)
c.String(500, "Failed to hash password")
return
}
result, err := conn.Exec("UPDATE passwords SET value = ? WHERE key = ? AND value = ?", newhash, data["username"].(string), data["password"].(string))
if err != nil {
fmt.Println("[ERROR] Failed to query database in /api/changepass at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
c.String(500, "Failed to query database")
} else {
rowsaffected, err := result.RowsAffected()
if err != nil {
fmt.Println("[ERROR] Failed to get rows affected in /api/changepass at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
c.String(500, "Failed to get rows affected")
} else {
if rowsaffected == int64(0) {
c.String(401, "Invalid username or password")
} else {
c.JSON(200, gin.H{"password": newhash})
}
}
}
})
router.GET("/account/logout", func(c *gin.Context) {
c.HTML(200, "logout.html", gin.H{})
})
router.GET("/account/deleteacct", func(c *gin.Context) {
c.HTML(200, "deleteacct.html", gin.H{})
})
router.GET("/account/changepass", func(c *gin.Context) {
c.HTML(200, "changepass.html", gin.H{})
})
router.GET("/usererror", func(c *gin.Context) {
c.HTML(200, "usererror.html", gin.H{})
})
router.GET("/accounterror", func(c *gin.Context) {
c.HTML(200, "accounterror.html", gin.H{})
})
router.GET("/badcaptcha", func(c *gin.Context) {
c.HTML(200, "badcaptcha.html", gin.H{})
})
router.GET("/signuperror", func(c *gin.Context) {
c.HTML(200, "signuperror.html", gin.H{})
})
router.GET("/loginerror", func(c *gin.Context) {
c.HTML(200, "loginerror.html", gin.H{})
})
router.GET("/invalidtoken", func(c *gin.Context) {
c.HTML(200, "invalidtoken.html", gin.H{})
})
router.GET("/invaliduser", func(c *gin.Context) {
c.HTML(200, "invaliduser.html", gin.H{})
})
router.GET("/badpassword", func(c *gin.Context) {
c.HTML(200, "badpassword.html", gin.H{})
})
router.GET("/baduser", func(c *gin.Context) {
c.HTML(200, "baduser.html", gin.H{})
})
router.GET("/success", func(c *gin.Context) {
c.HTML(200, "success.html", gin.H{})
})
router.GET("/taken", func(c *gin.Context) {
c.HTML(200, "taken.html", gin.H{})
})
router.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{})
})
router.GET("/cta", func(c *gin.Context) {
c.HTML(200, "cta.html", gin.H{})
})
fmt.Println("[INFO] Server started at", time.Now().Unix())

View File

@ -20,8 +20,6 @@ body.darkmode {
input {
padding: 10px;
background-color: var(--button);
color: white;
border-style: none;
border-radius: 5px;
margin-top: 5px;
@ -29,6 +27,11 @@ input {
}
button {
border-style: none;
}
.button {
text-decoration: none;
padding: 10px;
background-color: var(--button);
border-style: none;
@ -38,6 +41,19 @@ button {
color: #ffffff;
}
.logout {
display: block;
margin-top: 15px;
width: 65px;
margin-left: calc(50% - 45px);
z-index: 0;
position: absolute;
}
.button:hover {
background-color: var(--header);
}
.pswdbox {
margin-top: 5px;
}

View File

@ -2,6 +2,18 @@ addEventListener("DOMContentLoaded", function () {
if (getCookie("darkMode") === "true") {
applyDarkMode();
}
if (document.getElementById("username")) {
document.getElementById("username").innerText = localStorage.getItem("user")
}
if (document.getElementById("logout")) {
document.getElementById("logout").innerText = "Currently logging out " + localStorage.getItem("user") + ", please be patient..."
localStorage.removeItem("user")
localStorage.removeItem("hash")
setCookie("loggedin", "false", 1, "Strict")
window.location.href = "/account"
}
})
function toggleDarkMode() {
@ -47,4 +59,118 @@ function getCookie(name) {
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function signup() {
fetch("/api/signup", {
method: "POST",
body: JSON.stringify({
captcha: document.getElementById("captcha").value,
unique_token: document.getElementById("unique_token").value,
username: document.getElementById("username").value,
password: document.getElementById("password").value
})
})
.then((response) => response)
.then((response) => {
async function doStuff() {
console.log(response.status)
if (response.status === 200) {
window.location.href = "/success"
} else if (response.status === 501) {
window.location.href = "/taken"
} else if (response.status === 400) {
window.location.href = "/badcaptcha"
} else if (response.status === 402) {
window.location.href = "/invaliduser"
} else if (response.status === 403) {
window.location.href = "/invalidtoken"
} else {
window.location.href = "/signuperror"
}
}
doStuff()
})
}
function login() {
let username = document.getElementById("username").value
fetch("/api/login", {
method: "POST",
body: JSON.stringify({
username: username,
password: document.getElementById("password").value
})
})
.then((response) => response)
.then((response) => {
async function doStuff() {
console.log(response.status)
if (response.status === 200) {
setCookie("loggedin", "true", 30, "Strict")
const data = await response.json();
localStorage.setItem("user", username)
localStorage.setItem("hash", data.password)
window.location.reload()
} else if (response.status === 401) {
window.location.href = "/baduser"
} else if (response.status === 403) {
window.location.href = "/badpassword"
} else {
window.location.href = "/loginerror"
}
}
doStuff()
})
}
function deleteacct() {
fetch("/api/deleteacct", {
method: "POST",
body: JSON.stringify({
username: localStorage.getItem("user"),
password: localStorage.getItem("hash")
})
})
.then((response) => response)
.then((response) => {
async function doStuff() {
console.log(response.status)
if (response.status === 200) {
window.location.href = "/logout"
} else if (response.status === 401) {
window.location.href = "/usererror"
} else {
window.location.href = "/accounterror"
}
}
doStuff()
})
}
function changepass() {
fetch("/api/changepass", {
method: "POST",
body: JSON.stringify({
username: localStorage.getItem("user"),
password: localStorage.getItem("hash"),
newpass: document.getElementById("newpass").value
})
})
.then((response) => response)
.then((response) => {
async function doStuff() {
console.log(response.status)
if (response.status === 200) {
const data = await response.json();
localStorage.setItem("hash", data.password)
window.location.href = "/account"
} else if (response.status === 401) {
window.location.href = "/usererror"
} else {
window.location.href = "/accounterror"
}
}
doStuff()
})
}

View File

@ -1,46 +0,0 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Sign Up</title>
<link rel="stylesheet" href="/static/css/main.css">
<script src="/static/js/main.js"></script>
</head>
<body>
<div class="headerbar">
<a class="a" href="/">CtaMail</a>
<a class="main a" href="/register">Sign up</a>
<a class="a" href="/account">Account</a>
<button class="a right" onclick="toggleDarkMode()">Toggle Theme</button>
</div>
<div class="content">
<h1>Register an Email Account</h1>
<form method="POST" action="/api/signup">
<label>Username</label>
<div class="spacer">
<input type="text" name="username" required="">
</div>
<br>
<div class="Password">
<label>Password</label>
<div class="spacer">
<input type="password" name="password" required="">
</div>
</div>
<br>
<div class="spacer">
<label>CAPTCHA</label>
<div class="spacer">
<img src="data:image/png;base64,{{ .captcha_image }}" alt="Captcha">
</div>
<div class="spacer">
<input required="" name="captcha" type="text">
</div>
</div>
<br>
<input type="hidden" name="unique_token" value="{{ .unique_token }}">
<input type="submit" value="Register">
</form>
</div>
</body>
</html>