add registration link logic and registration html

This commit is contained in:
Adora Laura Kalb 2023-12-18 07:48:25 +01:00
parent 22cbc94da6
commit 0af10cd17e
Signed by: adoralaura
GPG key ID: 7A4552166FC8C056
17 changed files with 291 additions and 10 deletions

2
go.mod
View file

@ -13,6 +13,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/glebarez/sqlite v1.10.0 // indirect github.com/glebarez/sqlite v1.10.0 // indirect
github.com/go-mail/mail v2.3.1+incompatible // indirect
github.com/google/uuid v1.4.0 // indirect github.com/google/uuid v1.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
@ -27,6 +28,7 @@ require (
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/crypto v0.16.0 // indirect golang.org/x/crypto v0.16.0 // indirect
golang.org/x/sys v0.15.0 // indirect golang.org/x/sys v0.15.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
modernc.org/libc v1.22.5 // indirect modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect

4
go.sum
View file

@ -7,6 +7,8 @@ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9g
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc= github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA= github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM=
github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ= github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U= github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U=
@ -48,6 +50,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=

View file

@ -25,9 +25,9 @@ func InitializeDB() error {
models.DB = db models.DB = db
err = models.DB.AutoMigrate(&models.User{}) err = models.DB.AutoMigrate(&models.User{}, &models.EmailConfirmation{})
if err != nil { if err != nil {
slog.Error("Can't do DB Migration", "Model", "User") slog.Error("Can't do DB Migration")
return err return err
} }

View file

@ -57,10 +57,13 @@ func SetupFiber() error {
//fiberapp.Static("/admin/", "./web") //fiberapp.Static("/admin/", "./web")
fiberapp.Add("POST", "/api/users/register", handlers.POSTUserRegister) fiberapp.Add("POST", "/api/users/register", handlers.POSTUserRegister)
fiberapp.Add("POST", "/api/users/login", handlers.POSTUserLogin) fiberapp.Add("POST", "/api/users/login", handlers.POSTUserLogin)
fiberapp.Add("GET", "/confirm/email", handlers.GETUserEmailConfirm)
v1 := fiberapp.Group("/api/v1") v1 := fiberapp.Group("/api/v1")
v1.Use(cors.New(cors.Config{AllowOrigins: "*"})) v1.Use(cors.New(cors.Config{AllowOrigins: "*"}))
fiberapp.Static("/", "./static")
listenerr := fiberapp.Listen(ip + ":" + port) listenerr := fiberapp.Listen(ip + ":" + port)
if listenerr != nil { if listenerr != nil {
return listenerr return listenerr

View file

@ -0,0 +1,6 @@
package communication
type registrationTemplateVars struct {
BaseUrl string
RegistrationSecret string
}

View file

@ -0,0 +1,53 @@
package communication
import (
"code.lila.network/lauralani/tlm-login-server/internal/config"
"code.lila.network/lauralani/tlm-login-server/internal/models"
"crypto/tls"
"github.com/go-mail/mail"
"log/slog"
"strings"
"text/template"
)
func SendRegistrationNotification(user models.User, registrationsecet string) error {
message := mail.NewMessage()
message.SetHeader("From", config.SMTPFrom)
message.SetHeader("To", user.Email)
message.SetHeader("Subject", "Your Registration for TLM Nation")
var data = registrationTemplateVars{
BaseUrl: config.BasePath,
RegistrationSecret: registrationsecet,
}
plainstring := new(strings.Builder)
texttemplate, err := template.New("registration.plain.tmpl").ParseFiles("internal/communication/templates/registration.plain.tmpl")
if err != nil {
slog.Error("Can't load template", "Template", "internal/communication/templates/registration.plain.tmpl")
return err
}
err = texttemplate.Execute(plainstring, data)
if err != nil {
slog.Error("Can't execute template", "Template", "internal/communication/templates/registration.plain.tmpl")
return err
}
message.SetBody("text/plain", plainstring.String())
slog.Debug("got requested to send mail:", "FROM", config.SMTPFrom, "TO", user.Email)
mailer := mail.NewDialer(config.SMTPServer, config.SMTPPort, config.SMTPUser, config.SMTPPassword)
if !config.SMTPVerifySSL {
mailer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
err = mailer.DialAndSend(message)
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,11 @@
Thank you for your rgeistration for TLM Nation!
There is only one more step until you can start competing and posting Highscores:
Please confirm your email address by clicking on the following link:
{{ .BaseUrl }}/confirm/email?cs={{ .RegistrationSecret }}
This link is valid for four hours!
Thank you so much~
Laura

View file

@ -26,6 +26,24 @@ func InitializeConfig() error {
return errors.New("required env var is empty: TLM_SMTP_SERVER") return errors.New("required env var is empty: TLM_SMTP_SERVER")
} }
if os.Getenv("TLM_SMTP_USER") != "" {
SMTPUser = os.Getenv("TLM_SMTP_USER")
} else {
return errors.New("required env var is empty: TLM_SMTP_USER")
}
if os.Getenv("TLM_SMTP_PASSWORD") != "" {
SMTPPassword = os.Getenv("TLM_SMTP_PASSWORD")
} else {
return errors.New("required env var is empty: TLM_SMTP_PASSWORD")
}
if os.Getenv("TLM_BASEPATH") != "" {
BasePath = os.Getenv("TLM_BASEPATH")
} else {
return errors.New("required env var is empty: TLM_BASEPATH")
}
if os.Getenv("TLM_SMTP_VERIFYSSL") != "" { if os.Getenv("TLM_SMTP_VERIFYSSL") != "" {
var err error var err error
SMTPVerifySSL, err = strconv.ParseBool(os.Getenv("TLM_SMTP_VERIFYSSL")) SMTPVerifySSL, err = strconv.ParseBool(os.Getenv("TLM_SMTP_VERIFYSSL"))

View file

@ -1,9 +1,12 @@
package config package config
var SMTPPort int var SMTPPort int
var SMTPHost string
var SMTPFrom string var SMTPFrom string
var SMTPServer string var SMTPServer string
var SMTPUser string
var SMTPPassword string
var SMTPVerifySSL bool var SMTPVerifySSL bool
var SQLitePath string var SQLitePath string
// BasePath is the http path without trailing slash
var BasePath string

View file

@ -0,0 +1,23 @@
package handlers
import (
"code.lila.network/lauralani/tlm-login-server/internal/models"
"github.com/gofiber/fiber/v2"
)
func GETUserEmailConfirm(c *fiber.Ctx) error {
confirmationsecret := c.Query("cs")
if confirmationsecret == "" {
return fiber.NewError(fiber.StatusBadRequest, "400 Bad Request")
}
confirmation := models.EmailConfirmation{ConfirmationCode: confirmationsecret}
models.DB.First(&confirmation)
//fmt.Printf("UserID of Confirmation: %v\n", confirmation.UserID)
//fmt.Printf("Username of Confirmation: %v\n", confirmation.User.Username)
return nil
}

View file

@ -1,12 +1,12 @@
package handlers package handlers
import ( import (
"code.lila.network/lauralani/tlm-login-server/internal/communication"
"code.lila.network/lauralani/tlm-login-server/internal/models" "code.lila.network/lauralani/tlm-login-server/internal/models"
"code.lila.network/lauralani/tlm-login-server/internal/shared" "code.lila.network/lauralani/tlm-login-server/internal/shared"
"encoding/json"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"log"
"log/slog" "log/slog"
"time"
) )
func POSTUserRegister(c *fiber.Ctx) error { func POSTUserRegister(c *fiber.Ctx) error {
@ -14,9 +14,8 @@ func POSTUserRegister(c *fiber.Ctx) error {
var user models.User var user models.User
var queryuser models.User var queryuser models.User
err := json.Unmarshal(c.Body(), &registration) err := c.BodyParser(&registration)
if err != nil { if err != nil {
log.Println(err.Error())
return fiber.NewError(fiber.StatusBadRequest, "400 Bad Request") return fiber.NewError(fiber.StatusBadRequest, "400 Bad Request")
} }
@ -28,7 +27,7 @@ func POSTUserRegister(c *fiber.Ctx) error {
Message: "Username is already taken!", Message: "Username is already taken!",
}) })
if err != nil { if err != nil {
log.Println(err) slog.Error(err.Error())
} }
return nil return nil
} }
@ -40,6 +39,48 @@ func POSTUserRegister(c *fiber.Ctx) error {
dbop := models.DB.Create(&user) dbop := models.DB.Create(&user)
if dbop.Error != nil { if dbop.Error != nil {
slog.Error("Error creating user", "Username", registration.Username, "Error", err.Error()) slog.Error("Error creating user", "Username", registration.Username, "Error", err.Error())
return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error")
}
confirmation := models.EmailConfirmation{
User: user,
ConfirmationCode: shared.RandomString(64),
ExpiresAt: time.Now().Add(time.Hour * 4),
}
dbop = models.DB.Create(&confirmation)
if dbop.Error != nil {
slog.Error("Error creating EmailConfirmation", "Username", registration.Username, "Error", err.Error())
return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error")
}
emailerr := communication.SendRegistrationNotification(user, confirmation.ConfirmationCode)
if emailerr != nil {
dbdelete := models.DB.Delete(&user)
if dbdelete.Error != nil {
slog.Error("Error deleting user", "Username", registration.Username, "Error", err.Error())
return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error")
}
dbdelete = models.DB.Delete(&confirmation)
if dbdelete.Error != nil {
slog.Error("Error deleting confirmation", "Username", registration.Username, "Error", err.Error())
return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error")
}
reply := models.ErrorReply{
Status: "error",
Message: "Invalid email",
}
slog.Error("Error sending mail", "Error", emailerr.Error())
c.Status(fiber.StatusBadRequest)
err = c.JSON(reply)
if err != nil {
slog.Error(err.Error())
}
return nil
} }
var httpreply models.RegistrationReply var httpreply models.RegistrationReply
@ -51,7 +92,7 @@ func POSTUserRegister(c *fiber.Ctx) error {
c.Status(fiber.StatusCreated) c.Status(fiber.StatusCreated)
err = c.JSON(httpreply) err = c.JSON(httpreply)
if err != nil { if err != nil {
log.Println(err) slog.Error(err.Error())
} }
return nil return nil

View file

@ -14,3 +14,11 @@ type User struct {
PasswordHash []byte `gorm:"size:60"` PasswordHash []byte `gorm:"size:60"`
LastLoginAt time.Time LastLoginAt time.Time
} }
type EmailConfirmation struct {
gorm.Model
UserID uint
User User
ConfirmationCode string
ExpiresAt time.Time
}

View file

@ -0,0 +1,30 @@
package shared
import "math/rand"
// see https://stackoverflow.com/a/31832326
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
func RandomString(n int) string {
b := make([]byte, n)
// A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
for i, cache, remain := n-1, rand.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = rand.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}

5
static/css/pico.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

59
static/index.html Normal file
View file

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>register</title>
<link rel="stylesheet" href="css/pico.min.css">
</head>
<body>
<main>
<div class="container">
<h1>TLM Nation - User Registration</h1>
<form action="/api/users/register" method="post">
<label for="username">
Username (a-zA-Z0-9-_):
<input type="text" name="username" id="username" placeholder="Username" aria-label="Username"
required />
</label>
<label for="email">
Email:
<input type="email" name="email" id="email" placeholder="Email address"
aria-label="Email address" required />
</label>
<div class="grid">
<fieldset>
<label for="password">
Password:
<input onchange="ValidatePasswordFields()" type="password" name="password" id="password" placeholder="********"
aria-label="Password" required />
</label>
</fieldset>
<fieldset>
<label for="password-repeat">
Repeat Password:
<input onchange="ValidatePasswordFields()" type="password" id="password-repeat" placeholder="********"
aria-label="Repeat Password" required />
</label>
</fieldset>
</div>
<fieldset>
<label for="terms">
<input type="checkbox" role="switch" id="terms" name="terms"/>
I agree to the <a href="#" onclick="event.preventDefault()">Privacy Policy</a>
</label>
</fieldset>
<button type="submit" id="submit">Register</button>
</form>
</div>
</main>
<script src="js/register.js" defer></script>
</body>
</html>

14
static/js/register.js Normal file
View file

@ -0,0 +1,14 @@
function ValidatePasswordFields() {
var password = document.getElementById("password");
var passwordrepeat = document.getElementById("password-repeat");
if (password.value === passwordrepeat.value) {
passwordrepeat.setCustomValidity("");
console.log("passwords match")
} else {
passwordrepeat.setCustomValidity("Passwords don't match!");
console.log("passwords mööp")
}
}