Add admin-route & dropdown-menu. Add clear-cache & create-code. Adjustments for activation-code and user models. Add sweet-alert for admin-tools.

This commit is contained in:
MaxJa4
2024-01-20 16:47:22 +01:00
parent f2ab72ba1e
commit e5d13f2270
15 changed files with 281 additions and 55 deletions

View File

@@ -7,5 +7,7 @@
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="bootstrap" level="application" /> <orderEntry type="library" name="bootstrap" level="application" />
<orderEntry type="library" name="bootstrap-icons" level="application" /> <orderEntry type="library" name="bootstrap-icons" level="application" />
<orderEntry type="library" name="sweetalert2" level="application" />
<orderEntry type="library" name="@sweetalert2/theme-dark" level="application" />
</component> </component>
</module> </module>

View File

@@ -4,12 +4,13 @@
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues"> <option name="myValues">
<value> <value>
<list size="5"> <list size="6">
<item index="0" class="java.lang.String" itemvalue="hx-get" /> <item index="0" class="java.lang.String" itemvalue="hx-get" />
<item index="1" class="java.lang.String" itemvalue="hx-target" /> <item index="1" class="java.lang.String" itemvalue="hx-target" />
<item index="2" class="java.lang.String" itemvalue="hx-vals" /> <item index="2" class="java.lang.String" itemvalue="hx-vals" />
<item index="3" class="java.lang.String" itemvalue="hx-post" /> <item index="3" class="java.lang.String" itemvalue="hx-post" />
<item index="4" class="java.lang.String" itemvalue="hx-trigger" /> <item index="4" class="java.lang.String" itemvalue="hx-trigger" />
<item index="5" class="java.lang.String" itemvalue="hx-swap" />
</list> </list>
</value> </value>
</option> </option>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="JavaScriptLibraryMappings"> <component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{bootstrap, bootstrap-icons}" /> <file url="PROJECT" libraries="{@sweetalert2/theme-dark, bootstrap, bootstrap-icons, sweetalert2}" />
</component> </component>
</project> </project>

61
auth.go
View File

@@ -50,37 +50,56 @@ func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
session, _ := utils.Store.Get(c.Request, utils.LoginSessionName) session, _ := utils.Store.Get(c.Request, utils.LoginSessionName)
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth || !controllers.IsUserEnabled(session.Values["username"].(string)) { if auth, ok := session.Values["authenticated"].(bool); !ok || !auth || !controllers.IsUserEnabled(session.Values["username"].(string)) {
session.Options.MaxAge = -1 redirectToLogin(c)
err := session.Save(c.Request, c.Writer)
if err != nil {
log.Fatal(err)
}
c.Redirect(http.StatusFound, "/login")
c.Abort()
return return
} }
c.Next() c.Next()
} }
} }
func AdminAuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
session, _ := utils.Store.Get(c.Request, utils.LoginSessionName)
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
redirectToLogin(c)
return
}
username, ok := session.Values["username"].(string)
if !ok || !controllers.IsUserEnabled(username) || !controllers.IsUserAdmin(username) {
redirectToLogin(c)
return
}
c.Next()
}
}
func isUserAdmin(c *gin.Context) bool {
session, _ := utils.Store.Get(c.Request, utils.LoginSessionName)
username, ok := session.Values["username"].(string)
if !ok {
return false
}
return controllers.IsUserAdmin(username)
}
func redirectToLogin(c *gin.Context) {
session, _ := utils.Store.Get(c.Request, utils.LoginSessionName)
session.Options.MaxAge = -1
err := session.Save(c.Request, c.Writer)
if err != nil {
log.Fatal(err)
}
c.Redirect(http.StatusFound, "/login")
c.Abort()
}
func isValidCode(code string) bool { func isValidCode(code string) bool {
var activationCode models.ActivationCode var activationCode models.ActivationCode
if err := models.DB.Where("code = ?", code).First(&activationCode).Error; err != nil { if err := models.DB.Where("code = ?", code).First(&activationCode).Error; err != nil {
return false return false
} }
if activationCode.Code == code && !activationCode.Used { return activationCode.Code == code && activationCode.UsedForUsername == ""
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

@@ -5,6 +5,7 @@ import (
"InfantrySkillCalculator/utils" "InfantrySkillCalculator/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
"net/http" "net/http"
"time" "time"
) )
@@ -49,8 +50,6 @@ func AddCache(c *gin.Context) {
cache := models.PlayerCache{CacheDate: input.CacheDate, PlayerID: input.PlayerID, Score: input.Score, Game: input.Game} cache := models.PlayerCache{CacheDate: input.CacheDate, PlayerID: input.PlayerID, Score: input.Score, Game: input.Game}
//CreateOrUpdateCache(cache)
c.JSON(http.StatusOK, cache) c.JSON(http.StatusOK, cache)
} }
@@ -68,15 +67,11 @@ func UpdateCacheByPlayerID(c *gin.Context) {
return return
} }
//log.Println("Updating cache for player #" + utils.UintToString(cache.PlayerID))
models.DB.Model(&cache).Updates(map[string]interface{}{ models.DB.Model(&cache).Updates(map[string]interface{}{
"CacheDate": input.CacheDate, "CacheDate": input.CacheDate,
"Score": input.Score, "Score": input.Score,
}) })
//UpdateCacheTimestamp()
c.JSON(http.StatusOK, cache) c.JSON(http.StatusOK, cache)
} }
@@ -90,11 +85,21 @@ func DeleteCacheByPlayerID(c *gin.Context) {
models.DB.Delete(&cache) models.DB.Delete(&cache)
//UpdateCacheTimestamp()
c.JSON(http.StatusOK, true) c.JSON(http.StatusOK, true)
}
//log.Println("Deleted cache for player #" + utils.UintToString(cache.PlayerID)) // DeleteAllCaches DELETE /cache
func DeleteAllCaches(c *gin.Context) {
var caches []models.PlayerCache
if err := models.DB.
Session(&gorm.Session{AllowGlobalUpdate: true}).
Clauses(clause.Returning{}).
Delete(&caches).Error; err != nil {
c.String(http.StatusBadRequest, "Purge failed! Error: "+err.Error())
return
}
c.String(http.StatusOK, "Purged "+utils.UintToString(uint(len(caches)))+" caches!")
} }
func FindCacheGin(out interface{}, c *gin.Context) *gorm.DB { func FindCacheGin(out interface{}, c *gin.Context) *gorm.DB {

View File

@@ -0,0 +1,37 @@
package controllers
import (
"InfantrySkillCalculator/models"
"InfantrySkillCalculator/utils"
"github.com/gin-gonic/gin"
"net/http"
)
// CreateCode POST /code
func CreateCode(c *gin.Context) {
userRole, ok := c.GetPostForm("user_role")
if !ok {
c.String(http.StatusBadRequest, "Missing user role")
return
}
var role models.Role
switch userRole {
case "ADMIN":
role = models.AdminRole
case "AUTHOR":
role = models.AuthorRole
case "READER":
role = models.ReaderRole
default:
c.String(http.StatusInternalServerError, "Invalid user role: "+userRole)
}
newCode := utils.GenerateActivationCode()
newCodeObj := models.ActivationCode{Code: newCode, UserRole: role}
if err := models.DB.Create(&newCodeObj).Error; err != nil {
c.String(http.StatusInternalServerError, "Failed to create new code: "+err.Error())
return
}
c.String(http.StatusOK, "Activation code for role '"+string(role)+"': "+newCode)
}

View File

@@ -7,17 +7,39 @@ import (
func CreateUser(username string, hashedPassword string, enabled bool, usedCode string) { func CreateUser(username string, hashedPassword string, enabled bool, usedCode string) {
user := models.User{Username: username, Password: hashedPassword, Enabled: enabled} user := models.User{Username: username, Password: hashedPassword, Enabled: enabled}
models.DB.Create(&user) err := models.DB.Create(&user).Error
err := models.DB.Model(&models.ActivationCode{}).
Where("code = ?", usedCode).
Update("Used", true).Error
if err != nil { if err != nil {
log.Fatal(err) log.Fatalf("Error while creating user: %v", err)
}
var code models.ActivationCode
err = models.DB.
Model(&models.ActivationCode{}).
Where("code = ?", usedCode).
First(&code).Error
if err != nil {
log.Fatalf("Error while getting activation code: %v", err)
}
code.UsedForUsername = username
err = models.DB.Save(&code).Error
if err != nil {
log.Fatalf("Error while updating activation code: %v", err)
}
user.UserRole = code.UserRole
err = models.DB.Save(&user).Error
if err != nil {
log.Fatalf("Error while updating user role: %v", err)
} }
var bf2042 models.Game var bf2042 models.Game
models.DB.Where("tag = ?", "BF2042").First(&bf2042) err = models.DB.
Where("tag = ?", "BF2042").
First(&bf2042).Error
if err != nil {
log.Fatalf("Error while getting game: %v", err)
}
userSettings := models.UserSettings{ userSettings := models.UserSettings{
Username: username, Username: username,
ActiveGameID: bf2042.ID, ActiveGameID: bf2042.ID,
@@ -33,3 +55,13 @@ func IsUserEnabled(username string) bool {
models.DB.Where("username = ?", username).First(&user) models.DB.Where("username = ?", username).First(&user)
return user.Enabled return user.Enabled
} }
func IsUserAdmin(username string) bool {
var user models.User
err := models.DB.Where("username = ?", username).First(&user).Error
if err != nil {
log.Fatal(err)
return false
}
return user.UserRole == models.AdminRole
}

View File

@@ -22,6 +22,8 @@ func main() {
router.LoadHTMLGlob("templates/**/*") router.LoadHTMLGlob("templates/**/*")
protected := router.Group("/") protected := router.Group("/")
protected.Use(AuthRequired()) protected.Use(AuthRequired())
admin := router.Group("/admin")
admin.Use(AdminAuthRequired())
models.ConnectDatabase() models.ConnectDatabase()
@@ -35,6 +37,10 @@ func main() {
gin.LoggerWithWriter(utils.GinWriter), gin.LoggerWithWriter(utils.GinWriter),
gin.Recovery(), gin.Recovery(),
) )
admin.Use(
gin.LoggerWithWriter(utils.GinWriter),
gin.Recovery(),
)
router.Static("/static", "./static") router.Static("/static", "./static")
@@ -67,6 +73,9 @@ func main() {
protected.GET("/settings", controllers.GetSettings) protected.GET("/settings", controllers.GetSettings)
protected.PATCH("/settings", controllers.UpdateSettings) protected.PATCH("/settings", controllers.UpdateSettings)
admin.GET("/clear_cache", controllers.DeleteAllCaches)
admin.POST("/create_code", controllers.CreateCode)
log.Println("Running on 8000...") log.Println("Running on 8000...")
log.Fatal(router.Run(":8000")) log.Fatal(router.Run(":8000"))
} }

View File

@@ -1,6 +1,8 @@
package models package models
type ActivationCode struct { type ActivationCode struct {
Code string Code string `gorm:"primary_key"`
Used bool UserRole Role
UsedForUsername string
UsedFor User `gorm:"foreignKey:UsedForUsername"`
} }

View File

@@ -44,8 +44,8 @@ func ConnectDatabase() {
var code ActivationCode var code ActivationCode
if err := database.First(&code).Error; err != nil { if err := database.First(&code).Error; err != nil {
firstCode := utils.GenerateActivationCode() firstCode := utils.GenerateActivationCode()
database.Create(&ActivationCode{Code: firstCode, Used: false}) database.Create(&ActivationCode{Code: firstCode, UserRole: AdminRole})
log.Println("Created first activation code: " + firstCode) log.Println("Created first activation code with ADMIN role:\n" + firstCode)
} }
} }
err = database.AutoMigrate(&Game{}) err = database.AutoMigrate(&Game{})

View File

@@ -4,4 +4,13 @@ type User struct {
Username string `json:"username" gorm:"primary_key"` Username string `json:"username" gorm:"primary_key"`
Password string `json:"password"` Password string `json:"password"`
Enabled bool `json:"enabled" default:"1"` Enabled bool `json:"enabled" default:"1"`
UserRole Role `json:"user_role" default:"READER"`
} }
type Role string
const (
AdminRole Role = "ADMIN"
AuthorRole Role = "AUTHOR"
ReaderRole Role = "READER"
)

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"InfantrySkillCalculator/controllers" "InfantrySkillCalculator/controllers"
"InfantrySkillCalculator/models"
"InfantrySkillCalculator/utils" "InfantrySkillCalculator/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"html/template" "html/template"
@@ -32,11 +31,8 @@ func mainPage(c *gin.Context) {
log.Fatal(err) log.Fatal(err)
} }
var clans []models.Clan
models.DB.Find(&clans)
data := map[string]interface{}{ data := map[string]interface{}{
"clans": clans, "isAdmin": isUserAdmin(c),
} }
err = tmpl.Execute(c.Writer, data) err = tmpl.Execute(c.Writer, data)

View File

@@ -144,3 +144,39 @@ function deselectAllPlayers(playerListId) {
checkCounter.innerText = 0; checkCounter.innerText = 0;
} }
function confirmAndTrigger(btn) {
Swal.fire({
title: btn.innerText,
text: 'Do you want to continue?',
confirmButtonText: 'Yes',
confirmButtonColor: '#dd6b55',
denyButtonColor: '#3085d6',
icon: 'warning',
showDenyButton: true
}).then((result) => {
if (result.isConfirmed) {
htmx.trigger(btn, 'confirmed');
}
});
}
function createCodeDialog(btn) {
Swal.fire({
title: btn.innerText,
input: 'select',
inputOptions: {
'READER': 'Reader',
'AUTHOR': 'Author',
'ADMIN': 'Admin'
},
inputPlaceholder: 'Select a role',
confirmButtonText: 'Generate',
showCancelButton: true
}).then((result) => {
if (result.isConfirmed) {
btn.setAttribute('hx-vals', '{"user_role": "' + result.value + '"}');
htmx.trigger(btn, 'confirmed');
}
});
}

View File

@@ -2,13 +2,77 @@
<div class="row justify-content-between border-top pt-4 position-relative mb-5"> <div class="row justify-content-between border-top pt-4 position-relative mb-5">
<div class="col-auto position-absolute start-0"> <div class="col-auto position-absolute start-0">
<a class="btn btn-lg btn-outline-secondary text-secondary-emphasis" href="/logout" data-bs-toggle="tooltip" data-bs-title="Abmelden"> <div class="btn-toolbar" role="toolbar">
<i class="bi bi-door-closed"></i> <a class="btn btn-lg btn-outline-secondary text-secondary-emphasis me-2" href="/logout" data-bs-toggle="tooltip" data-bs-title="Abmelden">
</a> <i class="bi bi-door-closed"></i>
<button class="btn btn-lg btn-outline-secondary text-secondary-emphasis" data-bs-toggle="modal" data-bs-target="#settingsModal"> </a>
<i class="bi bi-gear-fill me-2"></i> <button class="btn btn-lg btn-outline-secondary text-secondary-emphasis me-2" data-bs-toggle="modal" data-bs-target="#settingsModal">
Einstellungen <i class="bi bi-gear-fill"></i>
</button> </button>
{{ if .isAdmin }}
<div class="dropdown">
<button class="btn btn-lg btn-outline-secondary text-secondary-emphasis dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-tools"></i>
</button>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">ACCOUNTS</h6></li>
<li>
<button class="dropdown-item fs-5 text-info-emphasis fst-italic"
hx-post="/admin/create_code" hx-trigger="confirmed" hx-swap="none"
onclick="createCodeDialog(this)">
Create Code
</button>
</li>
<li>
<button class="dropdown-item fs-5 text-info-emphasis fst-italic"
hx-get="/admin/reset_password" hx-trigger="confirmed" hx-swap="none"
onclick="confirmAndTrigger(this)">
Reset Password
</button>
</li>
<li>
<button class="dropdown-item fs-5 text-info-emphasis fst-italic"
hx-get="/admin/disable_account" hx-trigger="confirmed" hx-swap="none"
onclick="confirmAndTrigger(this)">
Disable Account
</button>
</li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">TOOLS</h6></li>
<li>
<button class="dropdown-item fs-5 text-warning-emphasis"
hx-get="/admin/clear_cache" hx-trigger="confirmed" hx-swap="none"
onclick="confirmAndTrigger(this)">
Clear Cache
</button>
</li>
<li>
<button class="dropdown-item fs-5 text-warning-emphasis fst-italic"
hx-get="/admin/update_cache" hx-trigger="confirmed" hx-swap="none"
onclick="confirmAndTrigger(this)">
Update Cache
</button>
</li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">DANGER ZONE</h6></li>
<li>
<button class="dropdown-item fs-5 text-danger-emphasis fst-italic"
hx-get="/admin/purge_players" hx-trigger="confirmed" hx-swap="none"
onclick="confirmAndTrigger(this)">
Purge Players
</button>
</li>
<li>
<button class="dropdown-item fs-5 text-danger-emphasis fst-italic"
hx-get="/admin/purge_cache" hx-trigger="confirmed" hx-swap="none"
onclick="confirmAndTrigger(this)">
Purge Clans
</button>
</li>
</ul>
</div>
{{ end }}
</div>
</div> </div>
<div class="col-auto position-absolute start-50 translate-middle-x"> <div class="col-auto position-absolute start-50 translate-middle-x">
<button class="btn btn-lg btn-outline-primary"> <button class="btn btn-lg btn-outline-primary">
@@ -24,4 +88,16 @@
</div> </div>
</div> </div>
<script>
document.body.addEventListener('htmx:afterRequest', function (event) {
if (event.detail.pathInfo.requestPath.startsWith("/admin/")) {
Swal.fire({
title: 'Action executed',
text: event.detail.xhr.response,
icon: event.detail.xhr.status === 200 ? 'success' : 'error'
});
}
});
</script>
{{ end }} {{ end }}

View File

@@ -9,6 +9,8 @@
<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> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/@sweetalert2/theme-dark@4/dark.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.js"></script>
<link rel="stylesheet" href="../static/index.css"> <link rel="stylesheet" href="../static/index.css">
{{ end }} {{ end }}