add registration link logic and registration html
This commit is contained in:
parent
22cbc94da6
commit
0af10cd17e
17 changed files with 291 additions and 10 deletions
2
go.mod
2
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
6
internal/communication/classes.go
Normal file
6
internal/communication/classes.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package communication
|
||||
|
||||
type registrationTemplateVars struct {
|
||||
BaseUrl string
|
||||
RegistrationSecret string
|
||||
}
|
53
internal/communication/mail.go
Normal file
53
internal/communication/mail.go
Normal 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
|
||||
}
|
11
internal/communication/templates/registration.plain.tmpl
Normal file
11
internal/communication/templates/registration.plain.tmpl
Normal 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
|
|
@ -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"))
|
||||
|
|
|
@ -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
|
||||
|
|
23
internal/handlers/user-email-confirm.go
Normal file
23
internal/handlers/user-email-confirm.go
Normal 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
|
||||
}
|
|
@ -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(), ®istration)
|
||||
err := c.BodyParser(®istration)
|
||||
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
30
internal/shared/random-string.go
Normal file
30
internal/shared/random-string.go
Normal 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
5
static/css/pico.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/pico.min.css.map
Normal file
1
static/css/pico.min.css.map
Normal file
File diff suppressed because one or more lines are too long
59
static/index.html
Normal file
59
static/index.html
Normal 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
14
static/js/register.js
Normal 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")
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in a new issue