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/glebarez/go-sqlite v1.21.2 // 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/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
@ -27,6 +28,7 @@ require (
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/crypto v0.16.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/mathutil 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/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
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/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
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/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=

View file

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

View file

@ -57,10 +57,13 @@ func SetupFiber() error {
//fiberapp.Static("/admin/", "./web")
fiberapp.Add("POST", "/api/users/register", handlers.POSTUserRegister)
fiberapp.Add("POST", "/api/users/login", handlers.POSTUserLogin)
fiberapp.Add("GET", "/confirm/email", handlers.GETUserEmailConfirm)
v1 := fiberapp.Group("/api/v1")
v1.Use(cors.New(cors.Config{AllowOrigins: "*"}))
fiberapp.Static("/", "./static")
listenerr := fiberapp.Listen(ip + ":" + port)
if listenerr != nil {
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")
}
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") != "" {
var err error
SMTPVerifySSL, err = strconv.ParseBool(os.Getenv("TLM_SMTP_VERIFYSSL"))

View file

@ -1,9 +1,12 @@
package config
var SMTPPort int
var SMTPHost string
var SMTPFrom string
var SMTPServer string
var SMTPUser string
var SMTPPassword string
var SMTPVerifySSL bool
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
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/shared"
"encoding/json"
"github.com/gofiber/fiber/v2"
"log"
"log/slog"
"time"
)
func POSTUserRegister(c *fiber.Ctx) error {
@ -14,9 +14,8 @@ func POSTUserRegister(c *fiber.Ctx) error {
var user models.User
var queryuser models.User
err := json.Unmarshal(c.Body(), &registration)
err := c.BodyParser(&registration)
if err != nil {
log.Println(err.Error())
return fiber.NewError(fiber.StatusBadRequest, "400 Bad Request")
}
@ -28,7 +27,7 @@ func POSTUserRegister(c *fiber.Ctx) error {
Message: "Username is already taken!",
})
if err != nil {
log.Println(err)
slog.Error(err.Error())
}
return nil
}
@ -40,6 +39,48 @@ func POSTUserRegister(c *fiber.Ctx) error {
dbop := models.DB.Create(&user)
if dbop.Error != nil {
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
@ -51,7 +92,7 @@ func POSTUserRegister(c *fiber.Ctx) error {
c.Status(fiber.StatusCreated)
err = c.JSON(httpreply)
if err != nil {
log.Println(err)
slog.Error(err.Error())
}
return nil

View file

@ -14,3 +14,11 @@ type User struct {
PasswordHash []byte `gorm:"size:60"`
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")
}
}