Add activation-codes and registration. Added tooltips. Added player-score-cache-display in mainpage.

This commit is contained in:
MaxJa4
2024-01-17 12:21:48 +01:00
parent 2a8c53ab56
commit 7e3c02e37e
18 changed files with 234 additions and 36 deletions

29
auth.go
View File

@@ -2,9 +2,12 @@ package main
import ( import (
"InfrantrySkillCalculator/models" "InfrantrySkillCalculator/models"
"InfrantrySkillCalculator/utils"
"errors"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"log" "log"
"net/http" "net/http"
) )
@@ -25,7 +28,9 @@ func getUserPassword(username string) (string, error) {
var user models.User var user models.User
if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil { if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil {
log.Fatal(err) if !errors.Is(err, gorm.ErrRecordNotFound) {
log.Fatal(err)
}
return "", err return "", err
} }
@@ -58,3 +63,25 @@ func AuthRequired() gin.HandlerFunc {
c.Next() c.Next()
} }
} }
func isValidCode(code string) bool {
var activationCode models.ActivationCode
if err := models.DB.Where("code = ?", code).First(&activationCode).Error; err != nil {
return false
}
if activationCode.Code == code && !activationCode.Used {
models.DB.Model(&activationCode).Updates(map[string]interface{}{
"Code": code,
"Used": true,
})
newCode := utils.GenerateActivationCode()
newCodeObj := models.ActivationCode{Code: newCode, Used: false}
models.DB.Create(&newCodeObj)
return true
}
return false
}

View File

@@ -46,7 +46,7 @@ func GetPlayersByClanHTML(c *gin.Context) {
var htmlOptions string var htmlOptions string
for _, player := range players { for _, player := range players {
htmlOptions += fmt.Sprintf(playerItem, player.Name, player.ID, player.ID) htmlOptions += fmt.Sprintf(playerItem, player.Name, "----", player.ID, player.ID)
} }
c.Header("Content-Type", "text/html") c.Header("Content-Type", "text/html")

View File

@@ -20,6 +20,10 @@ func main() {
//gin.SetMode(gin.ReleaseMode) // uncomment upon release //gin.SetMode(gin.ReleaseMode) // uncomment upon release
router := gin.New() router := gin.New()
err := router.SetTrustedProxies([]string{"127.0.0.1"})
if err != nil {
log.Fatal(err)
}
router.LoadHTMLGlob("templates/**/*") router.LoadHTMLGlob("templates/**/*")
protected := router.Group("/") protected := router.Group("/")
protected.Use(AuthRequired()) protected.Use(AuthRequired())
@@ -38,6 +42,8 @@ func main() {
router.GET("/login", loginPage) router.GET("/login", loginPage)
router.POST("/login", loginPost) router.POST("/login", loginPost)
router.GET("/logout", logout) router.GET("/logout", logout)
router.GET("/register", registerPage)
router.POST("/register", registerPost)
protected.GET("/", mainPage) protected.GET("/", mainPage)

6
models/code.go Normal file
View File

@@ -0,0 +1,6 @@
package models
type ActivationCode struct {
Code string
Used bool
}

View File

@@ -1,6 +1,7 @@
package models package models
import ( import (
"InfrantrySkillCalculator/utils"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
@@ -29,7 +30,21 @@ func ConnectDatabase() {
log.Fatal(err) log.Fatal(err)
} }
//database.AutoMigrate(&PlayerCache{}) //database.AutoMigrate(&PlayerCache{})
//database.AutoMigrate(&User{}) err = database.AutoMigrate(&User{})
if err != nil {
log.Fatal(err)
}
err = database.AutoMigrate(&ActivationCode{})
if err != nil {
log.Fatal(err)
} else {
var code ActivationCode
if err := database.First(&code).Error; err != nil {
firstCode := utils.GenerateActivationCode()
database.Create(&ActivationCode{Code: firstCode, Used: false})
log.Println("Created first activation code: " + firstCode)
}
}
//database.AutoMigrate(&Game{}) //database.AutoMigrate(&Game{})
//database.AutoMigrate(&MetricSettings{}) //database.AutoMigrate(&MetricSettings{})

View File

@@ -94,3 +94,48 @@ func logout(c *gin.Context) {
c.Redirect(http.StatusFound, "/login") c.Redirect(http.StatusFound, "/login")
} }
func registerPage(c *gin.Context) {
session, _ := store.Get(c.Request, LoginSessionName)
if auth, ok := session.Values["authenticated"].(bool); ok && auth {
c.Redirect(http.StatusFound, "/")
return
}
tmpl, err := template.ParseFiles([]string{"./templates/register.html", "./templates/components/header.html"}...)
if err != nil {
log.Fatal(err)
}
err = tmpl.Execute(c.Writer, nil)
if err != nil {
log.Fatal(err)
}
}
func registerPost(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
code := c.PostForm("code")
if !isValidCode(code) {
c.HTML(http.StatusOK, "login_error.html", gin.H{"message": "Ungültiger Aktivierungscode!"})
return
}
user := models.User{Username: username, Password: password, Enabled: true}
models.DB.Create(&user)
session, _ := store.Get(c.Request, LoginSessionName)
session.Values["authenticated"] = true
session.Values["username"] = username
err := session.Save(c.Request, c.Writer)
if err != nil {
c.JSON(http.StatusInternalServerError, nil)
return
}
c.Header("HX-Redirect", "/")
c.String(http.StatusOK, "")
}

View File

@@ -1,3 +1,10 @@
document.addEventListener('DOMContentLoaded', function() {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach((elem) => {
new bootstrap.Tooltip(elem);
});
});
function setupClanButtons(dropdownId, delBtnId, editBtnId) { function setupClanButtons(dropdownId, delBtnId, editBtnId) {
const dropdown = document.getElementById(dropdownId); const dropdown = document.getElementById(dropdownId);
const deleteButton = document.getElementById(delBtnId); const deleteButton = document.getElementById(delBtnId);

View File

@@ -1,14 +1,26 @@
{{ define "bottom_controls" }} {{ define "bottom_controls" }}
<div class="row justify-content-between border-top pt-4"> <div class="row justify-content-between border-top pt-4 position-relative mb-5">
<div class="col-md-auto"> <div class="col-auto position-absolute start-0">
<button class="btn btn-lg btn-outline-secondary text-secondary-emphasis w-100"><i class="bi bi-gear-fill me-2"></i>Einstellungen</button> <a class="btn btn-lg btn-outline-secondary text-secondary-emphasis" href="/logout" data-bs-toggle="tooltip" data-bs-title="Abmelden">
<i class="bi bi-door-closed"></i>
</a>
<button class="btn btn-lg btn-outline-secondary text-secondary-emphasis">
<i class="bi bi-gear-fill me-2"></i>
Einstellungen
</button>
</div> </div>
<div class="col-md-auto"> <div class="col-auto position-absolute start-50 translate-middle-x">
<button class="btn btn-lg btn-outline-primary w-100"><i class="bi bi-calculator-fill me-2"></i>Berechnen</button> <button class="btn btn-lg btn-outline-primary">
<i class="bi bi-calculator-fill me-2"></i>
Berechnen
</button>
</div> </div>
<div class="col-md-auto"> <div class="col-auto position-absolute end-0">
<button class="btn btn-lg btn-outline-secondary text-secondary-emphasis w-100"><i class="bi bi-person me-2"></i>Einzel-Abfrage</button> <button class="btn btn-lg btn-outline-secondary text-secondary-emphasis">
<i class="bi bi-person me-2"></i>
Einzel-Abfrage
</button>
</div> </div>
</div> </div>

View File

@@ -8,6 +8,7 @@
<!-- Bootstrap 5 CSS --> <!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="../static/index.css"> <link rel="stylesheet" href="../static/index.css">
{{ end }} {{ end }}

View File

@@ -11,13 +11,13 @@
<!-- Options will be loaded dynamically --> <!-- Options will be loaded dynamically -->
</select> </select>
<button class="btn btn-lg btn-outline-secondary text-danger bg-secondary-subtle" type="button" id="home-delete" data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#deleteClanModal" disabled> <button class="btn btn-lg btn-outline-secondary text-danger bg-secondary-subtle" type="button" id="home-delete" data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#deleteClanModal" disabled>
<i class="bi bi-trash3"></i> <i class="bi bi-trash3" data-bs-toggle="tooltip" data-bs-title="Clan löschen"></i>
</button> </button>
<button class="btn btn-lg btn-outline-secondary text-primary bg-secondary-subtle" type="button" data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#editClanModal" id="home-edit" disabled> <button class="btn btn-lg btn-outline-secondary text-primary bg-secondary-subtle" type="button" data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#editClanModal" id="home-edit" disabled>
<i class="bi bi-pencil-fill"></i> <i class="bi bi-pencil-fill" data-bs-toggle="tooltip" data-bs-title="Clan bearbeiten"></i>
</button> </button>
<button class="btn btn-lg btn-outline-secondary text-success" type="button" data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#addClanModal" id="home-add"> <button class="btn btn-lg btn-outline-secondary text-success" type="button" data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#addClanModal" id="home-add">
<i class="bi bi-plus-lg"></i> <i class="bi bi-plus-lg" data-bs-toggle="tooltip" data-bs-title="Clan hinzufügen"></i>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -2,21 +2,25 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div id="home-player-list" class="border rounded p-1 overflow-auto" style="height: 47vh;"> <div id="home-player-list" class="border rounded p-1 overflow-auto" style="height: 47vh;" oninput="updateTooltips(this)">
</div> </div>
</div> </div>
<div class="col-auto ps-0"> <div class="col-auto ps-0">
<div class="btn-group-vertical btn-group-lg" role="group"> <div class="btn-group-vertical btn-group-lg" role="group">
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="selectAllPlayers('home-player-list')"><i class="bi bi-check-square fs-4"></i></button> <button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="selectAllPlayers('home-player-list')">
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="deselectAllPlayers('home-player-list')"><i class="bi bi-square fs-4"></i></button> <i class="bi bi-check-square fs-4" data-bs-toggle="tooltip" data-bs-title="Alle auswählen"></i>
</button>
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="deselectAllPlayers('home-player-list')">
<i class="bi bi-square fs-4" data-bs-toggle="tooltip" data-bs-title="Nichts auswählen"></i>
</button>
</div> </div>
<br> <br>
<button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="home-player-add" data-bs-toggle="modal" data-bs-list="#home-player-list" data-bs-target="#addPlayerModal" disabled> <button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="home-player-add" data-bs-toggle="modal" data-bs-list="#home-player-list" data-bs-target="#addPlayerModal" disabled>
<i class="bi bi-person-add fs-4"></i> <i class="bi bi-person-add fs-4" data-bs-toggle="tooltip" data-bs-title="Spieler hinzufügen"></i>
</button> </button>
<br> <br>
<div class="vstack text-center border border-secondary rounded mt-2"> <div class="vstack text-center border border-secondary rounded mt-2" data-bs-toggle="tooltip" data-bs-title="Ausgwählte Spieler">
<i class="bi bi-ui-checks fs-4 mt-2 mb-1"></i> <i class="bi bi-ui-checks fs-4 mt-2 mb-1"></i>
<span class="badge fs-5 mb-2" id="home-player-selected">0</span> <span class="badge fs-5 mb-2" id="home-player-selected">0</span>
</div> </div>

View File

@@ -10,13 +10,13 @@
<option disabled selected value>Auswählen...</option> <option disabled selected value>Auswählen...</option>
</select> </select>
<button class="btn btn-lg btn-outline-secondary text-danger bg-secondary-subtle" type="button" id="opponent-delete" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#deleteClanModal" disabled> <button class="btn btn-lg btn-outline-secondary text-danger bg-secondary-subtle" type="button" id="opponent-delete" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#deleteClanModal" disabled>
<i class="bi bi-trash3"></i> <i class="bi bi-trash3" data-bs-toggle="tooltip" data-bs-title="Clan löschen"></i>
</button> </button>
<button class="btn btn-lg btn-outline-secondary text-primary bg-secondary-subtle" type="button" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#editClanModal" id="opponent-edit" disabled> <button class="btn btn-lg btn-outline-secondary text-primary bg-secondary-subtle" type="button" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#editClanModal" id="opponent-edit" disabled>
<i class="bi bi-pencil-fill"></i> <i class="bi bi-pencil-fill" data-bs-toggle="tooltip" data-bs-title="Clan bearbeiten"></i>
</button> </button>
<button class="btn btn-lg btn-outline-secondary text-success" type="button" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#addClanModal" id="opponent-add"> <button class="btn btn-lg btn-outline-secondary text-success" type="button" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#addClanModal" id="opponent-add">
<i class="bi bi-plus-lg"></i> <i class="bi bi-plus-lg" data-bs-toggle="tooltip" data-bs-title="Clan hinzufügen"></i>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -8,15 +8,19 @@
</div> </div>
<div class="col-auto ps-0"> <div class="col-auto ps-0">
<div class="btn-group-vertical btn-group-lg" role="group"> <div class="btn-group-vertical btn-group-lg" role="group">
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="selectAllPlayers('opponent-player-list')"><i class="bi bi-check-square fs-4"></i></button> <button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="selectAllPlayers('opponent-player-list')">
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="deselectAllPlayers('opponent-player-list')"><i class="bi bi-square fs-4"></i></button> <i class="bi bi-check-square fs-4" data-bs-toggle="tooltip" data-bs-title="Alle auswählen"></i>
</button>
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="deselectAllPlayers('opponent-player-list')">
<i class="bi bi-square fs-4" data-bs-toggle="tooltip" data-bs-title="Nichts auswählen"></i>
</button>
</div> </div>
<br> <br>
<button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="opponent-player-add" data-bs-toggle="modal" data-bs-list="#opponent-player-list" data-bs-target="#addPlayerModal" disabled> <button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="opponent-player-add" data-bs-toggle="modal" data-bs-list="#opponent-player-list" data-bs-target="#addPlayerModal" disabled>
<i class="bi bi-person-add fs-4"></i> <i class="bi bi-person-add fs-4" data-bs-toggle="tooltip" data-bs-title="Spieler hinzufügen"></i>
</button> </button>
<br> <br>
<div class="vstack text-center border border-secondary rounded mt-2"> <div class="vstack text-center border border-secondary rounded mt-2" data-bs-toggle="tooltip" data-bs-title="Spieler hinzufügen">
<i class="bi bi-ui-checks fs-4 mt-2 mb-1"></i> <i class="bi bi-ui-checks fs-4 mt-2 mb-1"></i>
<span class="badge fs-5 mb-2" id="opponent-player-selected">0</span> <span class="badge fs-5 mb-2" id="opponent-player-selected">0</span>
</div> </div>

View File

@@ -8,7 +8,7 @@
<div class="row"> <div class="row">
<!-- Home-Clan Column --> <!-- Home-Clan Column -->
<div class="col-md-6 d-flex flex-column border-end px-3 pb-4"> <div class="col-md-6 d-flex flex-column border-end px-3 pb-4">
<h4 class="text-center mt-2 pb-3 mb-3 border-bottom">Heim-Team</h4> <h4 class="text-center mt-2 pb-3 mb-3 border-bottom"><i class="bi bi-people me-3"></i>Heim-Team</h4>
<!-- Clan Selection --> <!-- Clan Selection -->
{{ template "home_clan_bar" . }} {{ template "home_clan_bar" . }}
@@ -18,7 +18,7 @@
<!-- Opponent-Clan Column --> <!-- Opponent-Clan Column -->
<div class="col-md-6 d-flex flex-column px-3 pb-4"> <div class="col-md-6 d-flex flex-column px-3 pb-4">
<h4 class="text-center mt-2 pb-3 mb-3 border-bottom">Gegner-Team</h4> <h4 class="text-center mt-2 pb-3 mb-3 border-bottom"><i class="bi bi-people me-3"></i>Gegner-Team</h4>
<!-- Clan Selection --> <!-- Clan Selection -->
{{ template "opp_clan_bar" . }} {{ template "opp_clan_bar" . }}
@@ -48,11 +48,8 @@
<script lang="javascript"> <script lang="javascript">
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
loadClans(); loadClans();
}); });
</script> </script>
<!-- Bootstrap 5 JS Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>

View File

@@ -7,7 +7,7 @@
<div class="container-fluid bg-dark mt-5 p-4 rounded-3 text-light text-center" style="max-width: 500px;"> <div class="container-fluid bg-dark mt-5 p-4 rounded-3 text-light text-center" style="max-width: 500px;">
<h3>Login</h3> <h3>Login</h3>
<hr> <hr>
<form hx-post="/login" hx-target="#login-result"> <form hx-post="/login" hx-target="#login-result" class="position-relative">
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input class="form-control form-control-lg" type="text" id="username" name="username" placeholder="Username" required> <input class="form-control form-control-lg" type="text" id="username" name="username" placeholder="Username" required>
<label for="username">Username</label> <label for="username">Username</label>
@@ -16,9 +16,10 @@
<input class="form-control form-control-lg" type="password" id="password" name="password" placeholder="Passwort" required> <input class="form-control form-control-lg" type="password" id="password" name="password" placeholder="Passwort" required>
<label for="password">Passwort</label> <label for="password">Passwort</label>
</div> </div>
<button class="btn btn-lg btn-primary mt-3" type="submit">Anmelden</button> <button class="btn btn-lg btn-primary mt-4" type="submit"><i class="bi bi-door-open me-3"></i>Anmelden</button>
<a class="btn btn-lg btn-outline-primary mt-4 ms-2 position-absolute end-0" href="/register" data-bs-toggle="tooltip" data-bs-title="Zur Registrierung"><i class="bi bi-key"></i></a>
</form> </form>
<div id="login-result" class="mt-4 fs-4"></div> <div id="login-result" class="fs-4 mt-2"></div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -1,8 +1,11 @@
<div class="input-group input-group-lg mb-1"> <div class="input-group input-group-lg mb-1">
<div class="input-group-text py-1 px-2"> <div class="input-group-text py-1 px-2">
<input class="form-check-input fs-4 border-secondary mt-0" type="checkbox" value="" onchange="updateSelectedPlayers(this)"> <input class="form-check-input fs-4 border-secondary mt-0" type="checkbox" value="" onchange="updateSelectedPlayers(this)" data-bs-toggle="tooltip" data-bs-title="In Berechnung einbeziehen">
</div> </div>
<span class="form-control py-1 px-2">%s</span> <span class="form-control py-2 px-3 w-50">%s</span>
<span class="form-control text-center px-1 text-secondary">
<i class="bi bi-trophy me-2"></i>%s
</span>
<button class="btn btn-outline-secondary text-secondary-emphasis dropdown-toggle py-1" type="button" data-bs-toggle="dropdown"></button> <button class="btn btn-outline-secondary text-secondary-emphasis dropdown-toggle py-1" type="button" data-bs-toggle="dropdown"></button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item text-primary fs-5" href="#" data-bs-toggle="modal" data-bs-id="%d" data-bs-target="#editPlayerModal"> <li><a class="dropdown-item text-primary fs-5" href="#" data-bs-toggle="modal" data-bs-id="%d" data-bs-target="#editPlayerModal">

56
templates/register.html Normal file
View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="de">
<head>
{{ template "header" . }}
</head>
<body data-bs-theme="dark" class="h-auto">
<div class="container-fluid bg-dark mt-5 p-4 rounded-3 text-light text-center" style="max-width: 500px;">
<h3>Registrierung</h3>
<hr>
<form hx-post="/register" hx-target="#register-result" class="position-relative needs-validation" novalidate>
<div class="form-floating mb-3">
<input class="form-control form-control-lg" type="text" minlength="4" id="username" name="username" placeholder="Username" required>
<label for="username">Username</label>
<div class="invalid-feedback">
Mindestlänge: 4 Zeichen
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control form-control-lg" type="password" minlength="8" id="password" name="password" placeholder="Passwort" required>
<label for="password">Passwort</label>
<div class="invalid-feedback">
Mindestlänge: 8 Zeichen
</div>
</div>
<div class="form-floating">
<input class="form-control form-control-lg" type="password" minlength="32" maxlength="32" id="code" name="code" placeholder="Aktivierungscode" required>
<label for="code">Aktivierungscode</label>
<div class="invalid-feedback">
Ungültiges Format
</div>
</div>
<button class="btn btn-lg btn-primary mt-4" type="submit"><i class="bi bi-key me-3"></i>Registrieren</button>
<a class="btn btn-lg btn-outline-primary mt-4 ms-2 position-absolute end-0" href="/login" data-bs-toggle="tooltip" data-bs-title="Zurück zur Anmeldung"><i class="bi bi-door-open"></i></a>
</form>
<div id="register-result" class="fs-4 mt-2"></div>
</div>
</body>
<script>
(() => {
'use strict'
const forms = document.querySelectorAll('.needs-validation')
Array.from(forms).forEach(form => {
form.addEventListener('submit', event => {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
</html>

14
utils/misc.go Normal file
View File

@@ -0,0 +1,14 @@
package utils
import (
"crypto/rand"
"encoding/hex"
)
func GenerateActivationCode() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
panic(err) // Handle the error appropriately in production
}
return hex.EncodeToString(bytes)
}