15 Commits

Author SHA1 Message Date
bcbb091dfb add invites to admin header
All checks were successful
Go / build (push) Successful in 12m15s
2025-04-15 00:36:51 +02:00
adfb3df283 update route permissions
Some checks failed
Go / build (push) Has been cancelled
2025-04-15 00:31:01 +02:00
1525f44687 show some sections only to admin user 2025-04-15 00:26:54 +02:00
3955d8626a add/rm tokens, register with token 2025-04-15 00:12:34 +02:00
6d63e53200 Add token repositorie 2025-04-14 23:29:24 +02:00
cca0b2775c disable /register if a user exist 2025-04-14 22:55:36 +02:00
98c75c111f update css 2025-04-14 22:42:25 +02:00
b2735e178f hide elements when not logged in 2025-04-14 22:42:07 +02:00
1c9fc230b1 remove rest api routes
currently not used at all
2025-04-14 22:41:43 +02:00
6c2b3964fe rm unused code 2025-04-14 22:41:26 +02:00
19ce41aca7 update styling
All checks were successful
Go / build (push) Successful in 12m16s
2025-04-14 12:22:53 +02:00
763bb35a45 set item amount at cart
All checks were successful
Go / build (push) Successful in 12m21s
2025-04-14 11:29:27 +02:00
6130843aa7 update printername
All checks were successful
Go / build (push) Successful in 12m15s
2025-04-14 10:59:22 +02:00
667c3eba13 add image to edititem view
All checks were successful
Go / build (push) Successful in 12m18s
2025-04-14 01:36:54 +02:00
e22cc0b243 dont clear tags on edit 2025-04-14 01:36:35 +02:00
32 changed files with 539 additions and 282 deletions

View File

@@ -1,8 +1,6 @@
package controllers package controllers
import ( import (
"crypto/rand"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@@ -15,6 +13,7 @@ import (
"git.dynamicdiscord.de/kalipso/zineshop/models" "git.dynamicdiscord.de/kalipso/zineshop/models"
//"git.dynamicdiscord.de/kalipso/zineshop/services" //"git.dynamicdiscord.de/kalipso/zineshop/services"
"git.dynamicdiscord.de/kalipso/zineshop/repositories" "git.dynamicdiscord.de/kalipso/zineshop/repositories"
"git.dynamicdiscord.de/kalipso/zineshop/utils"
) )
type CartItemController interface { type CartItemController interface {
@@ -53,15 +52,6 @@ func getSetCookieValue(c *gin.Context, cookieName string) string {
return "" // Return empty string if cookie is not found return "" // Return empty string if cookie is not found
} }
func generateSessionId(length int) string {
bytes := make([]byte, length) // 16 bytes = 128 bits
_, err := rand.Read(bytes)
if err != nil {
panic("failed to generate session ID")
}
return hex.EncodeToString(bytes)
}
func GetSessionId(ctx *gin.Context) string { func GetSessionId(ctx *gin.Context) string {
sessionId, err := ctx.Cookie("session_id") sessionId, err := ctx.Cookie("session_id")
@@ -73,17 +63,13 @@ func GetSessionId(ctx *gin.Context) string {
return responseCookie return responseCookie
} }
sessionId = generateSessionId(16) sessionId = utils.GenerateSessionId(16)
ctx.SetCookie("session_id", sessionId, 3600, "/", "", false, true) ctx.SetCookie("session_id", sessionId, 3600, "/", "", false, true)
} }
return sessionId return sessionId
} }
func GenerateToken() string {
return generateSessionId(16)
}
func (rc *cartItemController) NewCartItemFromForm(ctx *gin.Context) (models.CartItem, error) { func (rc *cartItemController) NewCartItemFromForm(ctx *gin.Context) (models.CartItem, error) {
sessionId := GetSessionId(ctx) sessionId := GetSessionId(ctx)
shopItemIdStr := ctx.PostForm("ShopItemId") shopItemIdStr := ctx.PostForm("ShopItemId")
@@ -162,7 +148,7 @@ func (rc *cartItemController) NewAddressFromForm(ctx *gin.Context) (models.Addre
func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order, error) { func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order, error) {
sessionId := GetSessionId(ctx) sessionId := GetSessionId(ctx)
status := models.OrderStatus("AwaitingConfirmation") status := models.OrderStatus("AwaitingConfirmation")
token := GenerateToken() token := utils.GenerateToken()
email := ctx.PostForm("email") email := ctx.PostForm("email")
comment := ctx.PostForm("comment") comment := ctx.PostForm("comment")
firstName := ctx.PostForm("firstName") firstName := ctx.PostForm("firstName")
@@ -337,6 +323,27 @@ func (rc *cartItemController) EditItemHandler(c *gin.Context) {
action := c.PostForm("action") action := c.PostForm("action")
if action == "setAmount" {
amountStr := c.PostForm("amount")
amount, err := strconv.Atoi(amountStr)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
if amount < 0 {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "amount cant be negative"})
return
}
if amount > 500 {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "amount cant be over 500"})
return
}
cartItem.Quantity = amount
}
if action == "increase" { if action == "increase" {
cartItem.Quantity += 1 cartItem.Quantity += 1
} }

View File

@@ -456,7 +456,10 @@ func (rc *shopItemController) EditItemHandler(c *gin.Context) {
newShopItem.Variants = shopItem.Variants newShopItem.Variants = shopItem.Variants
newShopItem.BasePrice = shopItem.BasePrice newShopItem.BasePrice = shopItem.BasePrice
newShopItem.IsPublic = shopItem.IsPublic newShopItem.IsPublic = shopItem.IsPublic
newShopItem.Tags = shopItem.Tags
if len(shopItem.Tags) != 0 {
newShopItem.Tags = shopItem.Tags
}
if shopItem.Image != "static/img/zine.jpg" { if shopItem.Image != "static/img/zine.jpg" {
newShopItem.Image = shopItem.Image newShopItem.Image = shopItem.Image

View File

@@ -1,11 +1,12 @@
package controllers package controllers
import ( import (
"errors"
"fmt" "fmt"
"math/rand"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm"
"git.dynamicdiscord.de/kalipso/zineshop/models" "git.dynamicdiscord.de/kalipso/zineshop/models"
"git.dynamicdiscord.de/kalipso/zineshop/repositories" "git.dynamicdiscord.de/kalipso/zineshop/repositories"
@@ -36,7 +37,7 @@ func (uc *UserController) Register(c *gin.Context) {
return return
} }
_, err = services.Users.Register(body.Name, body.Email, body.Password) _, err = services.Users.Register(body.Name, body.Email, body.Password, false)
if err != nil { if err != nil {
fmt.Println("Error: ", err) fmt.Println("Error: ", err)
@@ -140,10 +141,12 @@ func (rc *UserController) LoginHandler(c *gin.Context) {
} }
func CreateSessionData(c *gin.Context, extra any) gin.H { func CreateSessionData(c *gin.Context, extra any) gin.H {
_, exists := c.Get("user") user, exists := c.Get("user")
userImpl, _ := user.(models.User)
return gin.H{ return gin.H{
"loggedIn": exists, "loggedIn": exists,
"isAdmin": userImpl.IsAdmin,
"data": extra, "data": extra,
} }
} }
@@ -153,11 +156,45 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
email := c.PostForm("email") email := c.PostForm("email")
password := c.PostForm("password") password := c.PostForm("password")
_, err := services.Users.Register(name, email, password) //first registered user is admin
isEmpty, _ := repositories.Users.IsEmpty()
if isEmpty {
_, err := services.Users.Register(name, email, password, true)
if err != nil {
data := gin.H{
"error": "Registering Failed.",
"success": "",
}
c.HTML(http.StatusOK, "register.html", data)
return
}
if err != nil {
data := gin.H{ data := gin.H{
"error": "Registering Failed.", "error": "",
"success": "You successfully registered as Admin. Try logging in.",
}
c.HTML(http.StatusOK, "register.html", data)
return
}
//for any other user token is required
token := c.PostForm("token")
if token == "" {
data := gin.H{
"error": "No token. No register.",
"success": "",
}
c.HTML(http.StatusOK, "register.html", data)
}
tokenExists, err := repositories.Tokens.Exists(token)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
data := gin.H{
"error": err,
"success": "", "success": "",
} }
@@ -165,6 +202,31 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
return return
} }
if !tokenExists {
data := gin.H{
"error": "Invalid Token.",
"success": "",
}
c.HTML(http.StatusOK, "register.html", data)
return
}
_, err = services.Users.Register(name, email, password, false)
if err != nil {
data := gin.H{
"error": "Registering Failed.",
"success": "",
}
c.HTML(http.StatusOK, "register.html", data)
return
}
err = repositories.Tokens.Delete(token)
if err != nil {
fmt.Println("Could not delete RegisterToken: ", err)
}
data := gin.H{ data := gin.H{
"error": "", "error": "",
"success": "You successfully registered. Try logging in.", "success": "You successfully registered. Try logging in.",
@@ -174,6 +236,39 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
} }
func (rc *UserController) RegisterView(c *gin.Context) { func (rc *UserController) RegisterView(c *gin.Context) {
data := gin.H{
"error": "",
"success": "",
"token": c.Param("token"),
}
c.HTML(http.StatusOK, "registertoken.html", data)
}
func (rc *UserController) InitAdmin(c *gin.Context) {
isEmpty, err := repositories.Users.IsEmpty()
if err != nil {
data := gin.H{
"error": err,
"success": "",
}
c.HTML(http.StatusInternalServerError, "error.html", data)
return
}
if !isEmpty {
data := gin.H{
"error": "Registration is closed",
"success": "",
}
c.HTML(http.StatusInternalServerError, "error.html", data)
return
}
data := gin.H{ data := gin.H{
"error": "", "error": "",
"success": "", "success": "",
@@ -204,6 +299,38 @@ func (rc *UserController) ResetHandler(c *gin.Context) {
c.HTML(http.StatusOK, "passwordreset.html", data) c.HTML(http.StatusOK, "passwordreset.html", data)
} }
func (rc *UserController) InviteView(c *gin.Context) {
tokens, _ := repositories.Tokens.GetAll()
fmt.Println(tokens)
data := gin.H{
"tokens": tokens,
}
c.HTML(http.StatusOK, "invites.html", data)
}
func (rc *UserController) InviteHandler(c *gin.Context) {
action := c.PostForm("action")
if action == "create" {
_, err := repositories.Tokens.Create()
if err != nil {
fmt.Println(err)
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
}
if action == "delete" {
token := c.PostForm("token")
repositories.Tokens.Delete(token)
}
rc.InviteView(c)
}
func (rc *UserController) MainView(c *gin.Context) { func (rc *UserController) MainView(c *gin.Context) {
shopItems, _ := repositories.ShopItems.GetAll() shopItems, _ := repositories.ShopItems.GetAll()
fmt.Println(len(shopItems)) fmt.Println(len(shopItems))
@@ -227,68 +354,6 @@ func (rc *UserController) TagView(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", data) c.HTML(http.StatusOK, "index.html", data)
} }
type booking struct {
Booked bool
}
type calendarbooking struct {
Time string
Bookings []booking
}
func (rc *UserController) CalendarView(c *gin.Context) {
shopItems, _ := repositories.ShopItems.GetAll()
fmt.Println(len(shopItems))
generateBookings := func(amountShopItems int) []calendarbooking {
var result []calendarbooking
time := 6
for _ = range 18 {
book := calendarbooking{
Time: fmt.Sprintf("%d:00", time),
Bookings: []booking{},
}
for _ = range amountShopItems {
book.Bookings = append(book.Bookings, booking{Booked: rand.Float32() < 0.5})
}
time += 1
result = append(result, book)
}
return result
}
bookings := gin.H{
"head": []string{
"malobeo",
"hole of fame",
"BK",
"AZ Conni",
},
"bookings": generateBookings(4),
//"bookings": []calendarbooking{
// {
// Time: "10:00",
// Bookings: []booking{
// { Booked: true },
// { Booked: false },
// },
// },
//},
}
data := CreateSessionData(c, gin.H{
"title": "shopItem Page",
"bookings": bookings,
"shopItemcount": len(bookings["head"].([]string)) + 1,
})
fmt.Println(data)
c.HTML(http.StatusOK, "calendar.html", data)
}
func (rc *UserController) Logout(c *gin.Context) { func (rc *UserController) Logout(c *gin.Context) {
c.SetCookie("Authorization", "", -1, "", "", false, true) c.SetCookie("Authorization", "", -1, "", "", false, true)
c.HTML(http.StatusOK, "index.html", gin.H{}) c.HTML(http.StatusOK, "index.html", gin.H{})

76
main.go
View File

@@ -55,66 +55,50 @@ func main() {
server.Static("/static", os.Getenv("STATIC")) server.Static("/static", os.Getenv("STATIC"))
server.LoadHTMLGlob(fmt.Sprintf("%s/*.html", os.Getenv("VIEWS"))) server.LoadHTMLGlob(fmt.Sprintf("%s/*.html", os.Getenv("VIEWS")))
apiRoutes := server.Group("/api")
//apiRoutes.Use(middlewares.BasicAuth())
{
apiRoutes.POST("/tags", authValidator.RequireAuth, shopItemController.CreateTag)
apiRoutes.GET("/tags", authValidator.OptionalAuth, shopItemController.GetAllTags)
apiRoutes.POST("/shopitems", authValidator.RequireAuth, shopItemController.Create)
apiRoutes.GET("/shopitems", authValidator.OptionalAuth, shopItemController.GetAll)
apiRoutes.GET("/shopitems/:id", authValidator.OptionalAuth, shopItemController.GetById)
apiRoutes.PUT("/shopitems/:id", authValidator.RequireAuth, shopItemController.Update)
apiRoutes.DELETE("/shopitems/:id", authValidator.RequireAuth, shopItemController.Delete)
//apiRoutes.GET("/rooms/:id/users", authValidator.RequireAuth, authValidator.RequireRoomAdmin, shopItemController.GetUsers)
//apiRoutes.POST("/rooms/:id/users", authValidator.RequireAuth, shopItemController.AddUser)
apiRoutes.POST("/users/register", userController.Register)
apiRoutes.POST("/users/login", userController.Login)
apiRoutes.GET("/users/validate", authValidator.OptionalAuth, userController.Validate)
}
viewRoutes := server.Group("/", authValidator.OptionalAuth) viewRoutes := server.Group("/", authValidator.OptionalAuth)
{ {
viewRoutes.GET("/", userController.MainView) viewRoutes.GET("/", userController.MainView)
viewRoutes.GET("/shopitems/:id", shopItemController.ShopItemView) viewRoutes.GET("/shopitems/:id", shopItemController.ShopItemView)
viewRoutes.GET("/shopitems/:id/edit", authValidator.RequireAuth, shopItemController.EditItemView) viewRoutes.GET("/shopitems/:id/edit", authValidator.RequireAdmin, shopItemController.EditItemView)
viewRoutes.POST("/shopitems/:id/edit", authValidator.RequireAuth, shopItemController.EditItemHandler) viewRoutes.POST("/shopitems/:id/edit", authValidator.RequireAdmin, shopItemController.EditItemHandler)
viewRoutes.GET("/shopitems/:id/delete", authValidator.RequireAuth, shopItemController.DeleteItemView) viewRoutes.GET("/shopitems/:id/delete", authValidator.RequireAdmin, shopItemController.DeleteItemView)
viewRoutes.POST("/shopitems/:id/delete", authValidator.RequireAuth, shopItemController.DeleteItemHandler) viewRoutes.POST("/shopitems/:id/delete", authValidator.RequireAdmin, shopItemController.DeleteItemHandler)
viewRoutes.GET("/variant/:id/print", authValidator.RequireAuth, printController.PrintVariantView) viewRoutes.GET("/variant/:id/print", authValidator.RequireAdmin, printController.PrintVariantView)
viewRoutes.GET("/cart/print", authValidator.RequireAuth, printController.PrintCartView) viewRoutes.GET("/cart/print", authValidator.RequireAdmin, printController.PrintCartView)
viewRoutes.POST("/print", authValidator.RequireAuth, printController.PrintHandler) viewRoutes.POST("/print", authValidator.RequireAdmin, printController.PrintHandler)
viewRoutes.GET("/tags", authValidator.RequireAuth, shopItemController.TagView) viewRoutes.GET("/tags", authValidator.RequireAdmin, shopItemController.TagView)
viewRoutes.POST("/tags/:id", authValidator.RequireAuth, shopItemController.TagHandler) viewRoutes.POST("/tags/:id", authValidator.RequireAdmin, shopItemController.TagHandler)
viewRoutes.GET("/tags/:id", userController.TagView) viewRoutes.GET("/tags/:id", userController.TagView)
viewRoutes.POST("/tags", authValidator.RequireAuth, shopItemController.AddTagHandler) viewRoutes.POST("/tags", authValidator.RequireAdmin, shopItemController.AddTagHandler)
viewRoutes.GET("/cart", cartItemController.CartItemView) viewRoutes.GET("/cart", authValidator.RequireAuth, cartItemController.CartItemView)
viewRoutes.POST("/cart", cartItemController.AddItemHandler) viewRoutes.POST("/cart", authValidator.RequireAuth, cartItemController.AddItemHandler)
viewRoutes.POST("/cart/delete", cartItemController.DeleteItemHandler) viewRoutes.POST("/cart/delete", authValidator.RequireAuth, cartItemController.DeleteItemHandler)
viewRoutes.POST("/cart/edit", cartItemController.EditItemHandler) viewRoutes.POST("/cart/edit", authValidator.RequireAuth, cartItemController.EditItemHandler)
viewRoutes.GET("/checkout", cartItemController.CheckoutView) viewRoutes.GET("/checkout", authValidator.RequireAuth, cartItemController.CheckoutView)
viewRoutes.POST("/checkout", cartItemController.CheckoutHandler) viewRoutes.POST("/checkout", authValidator.RequireAuth, cartItemController.CheckoutHandler)
viewRoutes.POST("/order", cartItemController.OrderHandler) viewRoutes.POST("/order", authValidator.RequireAuth, cartItemController.OrderHandler)
viewRoutes.GET("/order/:token", cartItemController.OrderView) viewRoutes.GET("/order/:token", authValidator.RequireAuth, cartItemController.OrderView)
viewRoutes.GET("/order/:token/print", authValidator.RequireAuth, printController.PrintOrderView) viewRoutes.GET("/order/:token/print", authValidator.RequireAuth, printController.PrintOrderView)
viewRoutes.GET("/orders", authValidator.RequireAuth, cartItemController.OrdersView) viewRoutes.GET("/orders", authValidator.RequireAdmin, cartItemController.OrdersView)
viewRoutes.POST("/order/:token/edit", authValidator.RequireAuth, cartItemController.OrdersHandler) viewRoutes.POST("/order/:token/edit", authValidator.RequireAdmin, cartItemController.OrdersHandler)
//write middleware that redirects to homescreen on register/login/reset for logged in users //write middleware that redirects to homescreen on register/login/reset for logged in users
viewRoutes.GET("/login", userController.LoginView) viewRoutes.GET("/login", userController.LoginView)
viewRoutes.GET("/logout", userController.Logout) viewRoutes.GET("/logout", userController.Logout)
viewRoutes.GET("/register", userController.RegisterView) viewRoutes.GET("/register", userController.InitAdmin)
viewRoutes.GET("/passwordreset", userController.ResetView) viewRoutes.GET("/register/:token", userController.RegisterView)
viewRoutes.GET("/additem", authValidator.RequireAuth, shopItemController.AddItemView) viewRoutes.GET("/invites", authValidator.RequireAdmin, userController.InviteView)
viewRoutes.GET("/batchupload", authValidator.RequireAuth, shopItemController.AddItemsView) viewRoutes.POST("/invites", authValidator.RequireAdmin, userController.InviteHandler)
viewRoutes.GET("/passwordreset", authValidator.RequireAuth, userController.ResetView)
viewRoutes.GET("/additem", authValidator.RequireAdmin, shopItemController.AddItemView)
viewRoutes.GET("/batchupload", authValidator.RequireAdmin, shopItemController.AddItemsView)
viewRoutes.POST("/login", userController.LoginHandler) viewRoutes.POST("/login", userController.LoginHandler)
viewRoutes.POST("/register", userController.RegisterHandler) viewRoutes.POST("/register", userController.RegisterHandler)
viewRoutes.POST("/additem", authValidator.RequireAuth, shopItemController.AddItemHandler) viewRoutes.POST("/additem", authValidator.RequireAdmin, shopItemController.AddItemHandler)
viewRoutes.POST("/batchupload", authValidator.RequireAuth, shopItemController.AddItemsHandler) viewRoutes.POST("/batchupload", authValidator.RequireAdmin, shopItemController.AddItemsHandler)
viewRoutes.POST("/passwordreset", userController.ResetHandler) viewRoutes.POST("/passwordreset", authValidator.RequireAuth, userController.ResetHandler)
} }
server.Run(":" + os.Getenv("PORT")) server.Run(":" + os.Getenv("PORT"))

View File

@@ -1,13 +1,13 @@
package middlewares package middlewares
import( import (
"os"
"fmt" "fmt"
"os"
"time" "time"
//"strconv" //"strconv"
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"net/http"
//"git.dynamicdiscord.de/kalipso/zineshop/models" //"git.dynamicdiscord.de/kalipso/zineshop/models"
"git.dynamicdiscord.de/kalipso/zineshop/repositories" "git.dynamicdiscord.de/kalipso/zineshop/repositories"
@@ -98,6 +98,63 @@ func (av *AuthValidator) RequireAuth(c *gin.Context) {
c.AbortWithStatus(http.StatusUnauthorized) c.AbortWithStatus(http.StatusUnauthorized)
} }
func (av *AuthValidator) RequireAdmin(c *gin.Context) {
// Get Cookie
tokenString, err := c.Cookie("Authorization")
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
//Validate
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
return []byte(os.Getenv("SECRET")), nil
})
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
//Check Expiration
if float64(time.Now().Unix()) > claims["exp"].(float64) {
//expired
c.AbortWithStatus(http.StatusUnauthorized)
return
}
//Find user
user, err := repositories.Users.GetById(claims["sub"])
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if !user.IsAdmin {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
//Attach to req
c.Set("user", user)
// Coninue
c.Next()
return
}
c.AbortWithStatus(http.StatusUnauthorized)
}
func (av *AuthValidator) OptionalAuth(c *gin.Context) { func (av *AuthValidator) OptionalAuth(c *gin.Context) {
defer c.Next() defer c.Next()

View File

@@ -62,7 +62,7 @@ func NewPrintJob(shopItem ShopItem, variant ItemVariant, coverPage bool, amount
} }
func (p *PrintJob) Execute() error { func (p *PrintJob) Execute() error {
baseCommand := "lp -d KONICA_MINOLTA_KONICA_MINOLTA_bizhub_C258/BookletPrint" baseCommand := "lp -d KonicaBooklet"
baseCommand += fmt.Sprintf(" -n %v ", p.Amount) baseCommand += fmt.Sprintf(" -n %v ", p.Amount)
for _, option := range p.Options { for _, option := range p.Options {

View File

@@ -4,9 +4,15 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type RegisterToken struct {
gorm.Model
Token string `json:"token" binding:"required" gorm:"unique;not null"`
}
type User struct { type User struct {
gorm.Model gorm.Model
Name string `json:"name" binding:"required" gorm:"unique;not null"` Name string `json:"name" binding:"required" gorm:"unique;not null"`
Password string `json:"password" binding:"required" gorm:"not null"` Password string `json:"password" binding:"required" gorm:"not null"`
Email string `json:"email" binding:"required,email" gorm:"unique;not null"` Email string `json:"email" binding:"required,email" gorm:"unique;not null"`
IsAdmin bool `json:"isAdmin" gorm:"default:false;not null"`
} }

View File

@@ -0,0 +1,86 @@
package repositories
import (
"errors"
"fmt"
"gorm.io/gorm"
"git.dynamicdiscord.de/kalipso/zineshop/models"
"git.dynamicdiscord.de/kalipso/zineshop/utils"
)
type RegisterTokenRepository interface {
Create() (models.RegisterToken, error)
GetAll() ([]models.RegisterToken, error)
Exists(string) (bool, error)
Delete(string) error
}
type GORMRegisterTokenRepository struct {
DB *gorm.DB
}
func NewGORMRegisterTokenRepository(db *gorm.DB) RegisterTokenRepository {
return &GORMRegisterTokenRepository{
DB: db,
}
}
func (t *GORMRegisterTokenRepository) Create() (models.RegisterToken, error) {
token := utils.GenerateToken()
exists, err := t.Exists(token)
if err != nil {
return models.RegisterToken{}, err
}
if exists {
return t.Create()
}
newToken := models.RegisterToken{
Token: token,
}
result := t.DB.Create(&newToken)
if result.Error != nil {
return models.RegisterToken{}, result.Error
}
return newToken, nil
}
func (t *GORMRegisterTokenRepository) GetAll() ([]models.RegisterToken, error) {
var tokens []models.RegisterToken
result := t.DB.Find(&tokens)
return tokens, result.Error
}
func (t *GORMRegisterTokenRepository) Exists(tokenString string) (bool, error) {
var token models.RegisterToken
result := t.DB.First(&token, "token = ?", tokenString)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return false, nil
}
return false, result.Error
}
return true, nil
}
func (t *GORMRegisterTokenRepository) Delete(token string) error {
result := t.DB.Where("token = ?", token).Delete(&models.RegisterToken{})
if result.Error != nil {
return result.Error
} else if result.RowsAffected == 0 {
return fmt.Errorf("Token not found, could not be deleted")
}
return nil
}

View File

@@ -14,6 +14,7 @@ var (
Tags TagRepository Tags TagRepository
CartItems CartItemRepository CartItems CartItemRepository
Orders OrderRepository Orders OrderRepository
Tokens RegisterTokenRepository
) )
func InitRepositories() { func InitRepositories() {
@@ -27,7 +28,8 @@ func InitRepositories() {
&models.User{}, &models.User{},
&models.Tag{}, &models.Tag{},
&models.CartItem{}, &models.CartItem{},
&models.Order{}) &models.Order{},
&models.RegisterToken{})
if err != nil { if err != nil {
panic("failed to migrate database") panic("failed to migrate database")
@@ -38,4 +40,5 @@ func InitRepositories() {
Tags = NewGORMTagRepository(db) Tags = NewGORMTagRepository(db)
CartItems = NewGORMCartItemRepository(db) CartItems = NewGORMCartItemRepository(db)
Orders = NewGORMOrderRepository(db) Orders = NewGORMOrderRepository(db)
Tokens = NewGORMRegisterTokenRepository(db)
} }

View File

@@ -1,6 +1,6 @@
package repositories package repositories
import( import (
"gorm.io/gorm" "gorm.io/gorm"
"git.dynamicdiscord.de/kalipso/zineshop/models" "git.dynamicdiscord.de/kalipso/zineshop/models"
@@ -10,6 +10,7 @@ type UserRepository interface {
Create(models.User) (models.User, error) Create(models.User) (models.User, error)
GetByEmail(string) (models.User, error) GetByEmail(string) (models.User, error)
GetById(interface{}) (models.User, error) GetById(interface{}) (models.User, error)
IsEmpty() (bool, error)
} }
type GORMUserRepository struct { type GORMUserRepository struct {
@@ -22,7 +23,7 @@ func NewGORMUserRepository(db *gorm.DB) UserRepository {
} }
} }
func (u *GORMUserRepository) Create(user models.User) (models.User, error) { func (u *GORMUserRepository) Create(user models.User) (models.User, error) {
result := u.DB.Create(&user) result := u.DB.Create(&user)
if result.Error != nil { if result.Error != nil {
@@ -53,3 +54,18 @@ func (u *GORMUserRepository) GetById(id interface{}) (models.User, error) {
return user, nil return user, nil
} }
func (u *GORMUserRepository) IsEmpty() (bool, error) {
var user models.User
result := u.DB.First(&user)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return true, nil
} else {
return false, result.Error
}
}
return false, nil
}

View File

@@ -1,9 +1,9 @@
package services package services
import( import (
"golang.org/x/crypto/bcrypt"
"os" "os"
"time" "time"
"golang.org/x/crypto/bcrypt"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
@@ -11,13 +11,13 @@ import(
"git.dynamicdiscord.de/kalipso/zineshop/repositories" "git.dynamicdiscord.de/kalipso/zineshop/repositories"
) )
var( var (
Users UserService = UserService{} Users UserService = UserService{}
) )
type UserService struct {} type UserService struct{}
func (u *UserService) Register(name string, email string, password string) (models.User, error) { func (u *UserService) Register(name string, email string, password string, isAdmin bool) (models.User, error) {
//hash pw //hash pw
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
@@ -25,7 +25,7 @@ func (u *UserService) Register(name string, email string, password string) (mode
return models.User{}, err return models.User{}, err
} }
user := models.User{Name: name, Email: email, Password: string(hash)} user := models.User{Name: name, Email: email, Password: string(hash), IsAdmin: isAdmin}
_, err = repositories.Users.Create(user) _, err = repositories.Users.Create(user)
if err != nil { if err != nil {
@@ -35,7 +35,7 @@ func (u *UserService) Register(name string, email string, password string) (mode
return user, nil return user, nil
} }
//return jwt tokenstring on success // return jwt tokenstring on success
func (u *UserService) Login(email string, password string) (string, error) { func (u *UserService) Login(email string, password string) (string, error) {
//lookup requested user //lookup requested user
user, err := repositories.Users.GetByEmail(email) user, err := repositories.Users.GetByEmail(email)

BIN
static/img/logo-black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
static/img/logo-white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -650,10 +650,6 @@ video {
margin-right: 1rem; margin-right: 1rem;
} }
.ms-2 {
margin-inline-start: 0.5rem;
}
.mt-1 { .mt-1 {
margin-top: 0.25rem; margin-top: 0.25rem;
} }
@@ -738,10 +734,6 @@ video {
width: 33.333333%; width: 33.333333%;
} }
.w-10 {
width: 2.5rem;
}
.w-12 { .w-12 {
width: 3rem; width: 3rem;
} }
@@ -790,6 +782,10 @@ video {
flex: 1 1 0%; flex: 1 1 0%;
} }
.flex-shrink-0 {
flex-shrink: 0;
}
.shrink-0 { .shrink-0 {
flex-shrink: 0; flex-shrink: 0;
} }
@@ -814,10 +810,6 @@ video {
flex-direction: column; flex-direction: column;
} }
.items-start {
align-items: flex-start;
}
.items-center { .items-center {
align-items: center; align-items: center;
} }
@@ -908,10 +900,6 @@ video {
border-radius: 1.5rem; border-radius: 1.5rem;
} }
.rounded-\[50px\] {
border-radius: 50px;
}
.rounded-full { .rounded-full {
border-radius: 9999px; border-radius: 9999px;
} }
@@ -1068,6 +1056,11 @@ video {
background-color: rgb(224 231 255 / var(--tw-bg-opacity, 1)); background-color: rgb(224 231 255 / var(--tw-bg-opacity, 1));
} }
.bg-indigo-500 {
--tw-bg-opacity: 1;
background-color: rgb(99 102 241 / var(--tw-bg-opacity, 1));
}
.bg-indigo-600 { .bg-indigo-600 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(79 70 229 / var(--tw-bg-opacity, 1)); background-color: rgb(79 70 229 / var(--tw-bg-opacity, 1));
@@ -1232,10 +1225,6 @@ video {
fill: #fef2f2; fill: #fef2f2;
} }
.stroke-gray-900 {
stroke: #111827;
}
.stroke-red-500 { .stroke-red-500 {
stroke: #ef4444; stroke: #ef4444;
} }
@@ -1720,10 +1709,6 @@ video {
color: rgb(39 39 42 / var(--tw-text-opacity, 1)); color: rgb(39 39 42 / var(--tw-text-opacity, 1));
} }
.underline {
text-decoration-line: underline;
}
.antialiased { .antialiased {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@@ -1735,11 +1720,6 @@ video {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
} }
.shadow-transparent {
--tw-shadow-color: transparent;
--tw-shadow: var(--tw-shadow-colored);
}
.outline { .outline {
outline-style: solid; outline-style: solid;
} }
@@ -1785,10 +1765,6 @@ video {
outline-offset: 2px; outline-offset: 2px;
} }
.focus-within\:outline-gray-300:focus-within {
outline-color: #d1d5db;
}
.focus-within\:outline-red-500:focus-within { .focus-within\:outline-red-500:focus-within {
outline-color: #ef4444; outline-color: #ef4444;
} }
@@ -1808,11 +1784,6 @@ video {
--tw-ring-offset-width: 2px; --tw-ring-offset-width: 2px;
} }
.hover\:border-gray-300:hover {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.hover\:bg-blue-700:hover { .hover\:bg-blue-700:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1)); background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
@@ -1828,11 +1799,6 @@ video {
background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1)); background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1));
} }
.hover\:bg-gray-50:hover {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-700:hover { .hover\:bg-gray-700:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1)); background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
@@ -1877,15 +1843,6 @@ video {
text-decoration-line: underline; text-decoration-line: underline;
} }
.hover\:no-underline:hover {
text-decoration-line: none;
}
.hover\:shadow-gray-200:hover {
--tw-shadow-color: #e5e7eb;
--tw-shadow: var(--tw-shadow-colored);
}
.focus\:z-10:focus { .focus\:z-10:focus {
z-index: 10; z-index: 10;
} }
@@ -1922,12 +1879,6 @@ video {
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
} }
.focus\:ring-2:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-4:focus { .focus\:ring-4:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@@ -1964,10 +1915,6 @@ video {
fill: #f87171; fill: #f87171;
} }
.group:hover .group-hover\:stroke-black {
stroke: #000;
}
.group:hover .group-hover\:stroke-white { .group:hover .group-hover\:stroke-white {
stroke: #fff; stroke: #fff;
} }
@@ -2043,10 +1990,6 @@ video {
max-width: 24rem; max-width: 24rem;
} }
.sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sm\:items-center { .sm\:items-center {
align-items: center; align-items: center;
} }
@@ -2138,10 +2081,6 @@ video {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
.lg\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.lg\:p-8 { .lg\:p-8 {
padding: 2rem; padding: 2rem;
} }
@@ -2165,11 +2104,6 @@ video {
.xl\:grid-cols-4 { .xl\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
} }
.xl\:gap-x-8 {
-moz-column-gap: 2rem;
column-gap: 2rem;
}
} }
@media (min-width: 1536px) { @media (min-width: 1536px) {
@@ -2259,10 +2193,6 @@ video {
color: rgb(156 163 175 / var(--tw-placeholder-opacity, 1)); color: rgb(156 163 175 / var(--tw-placeholder-opacity, 1));
} }
.dark\:ring-offset-gray-800 {
--tw-ring-offset-color: #1f2937;
}
.dark\:hover\:bg-gray-600:hover { .dark\:hover\:bg-gray-600:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(75 85 99 / var(--tw-bg-opacity, 1)); background-color: rgb(75 85 99 / var(--tw-bg-opacity, 1));

19
utils/utils.go Normal file
View File

@@ -0,0 +1,19 @@
package utils
import (
"crypto/rand"
"encoding/hex"
)
func GenerateSessionId(length int) string {
bytes := make([]byte, length) // 16 bytes = 128 bits
_, err := rand.Read(bytes)
if err != nil {
panic("failed to generate session ID")
}
return hex.EncodeToString(bytes)
}
func GenerateToken() string {
return GenerateSessionId(16)
}

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8"> <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm"> <div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company"> <img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Add an Item</h2> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Add an Item</h2>
</div> </div>

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8"> <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm"> <div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company"> <img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Add Tag</h2> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Add Tag</h2>
</div> </div>

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8"> <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm"> <div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company"> <img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Batch Upload</h2> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Batch Upload</h2>
Select multiple files on the upload. For each a new Shop Item will be created with the Filename as Name, Abstract Select multiple files on the upload. For each a new Shop Item will be created with the Filename as Name, Abstract
and Description.<br> and Description.<br>

View File

@@ -33,28 +33,14 @@
</p> </p>
<form action="/cart/edit" method="POST"> <form action="/cart/edit" method="POST">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="flex items-center gap-4">
<input type="hidden" id="{{ .ID }}" name="id" value="{{ .ID }}"> <input type="hidden" id="{{ .ID }}" name="id" value="{{ .ID }}">
<button type="submit" name="action" value="decrease" <div class="flex items-center gap-4">
class="group rounded-[50px] border border-gray-200 shadow-sm shadow-transparent p-2.5 flex items-center justify-center bg-white transition-all duration-500 hover:shadow-gray-200 hover:bg-gray-50 hover:border-gray-300 focus-within:outline-gray-300"> <input type="number" name="amount" value="{{ .Quantity }}" step="1" min="0" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
<svg class="stroke-gray-900 transition-all duration-500 group-hover:stroke-black" <button type="submit" name="action" value="setAmount"
width="18" height="19" viewBox="0 0 18 19" fill="none" class="block w-full bg-indigo-500 rounded-md px-3 py-1.5 text-base text-white placeholder:text-gray-400 sm:text-sm/6 duration-500 hover:bg-indigo-700">
xmlns="http://www.w3.org/2000/svg"> Set Amount
<path d="M4.5 9.5H13.5" stroke="" stroke-width="1.6" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button> </button>
<p class="border border-gray-200 text-center rounded-full w-10 text-gray-900 font-semibold text-sm py-1.5 px-3 bg-gray-100 text-center">{{ .Quantity }}</p> </div>
<button type="submit" name="action" value="increase"
class="group rounded-[50px] border border-gray-200 shadow-sm shadow-transparent p-2.5 flex items-center justify-center bg-white transition-all duration-500 hover:shadow-gray-200 hover:bg-gray-50 hover:border-gray-300 focus-within:outline-gray-300">
<svg class="stroke-gray-900 transition-all duration-500 group-hover:stroke-black"
width="18" height="19" viewBox="0 0 18 19" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M3.75 9.5H14.25M9 14.75V4.25" stroke="" stroke-width="1.6"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</div>
<h6 class="text-indigo-600 font-manrope font-bold text-2xl leading-9 text-right">{{ .Quantity}} x {{ .ItemVariant.Price }}€</h6> <h6 class="text-indigo-600 font-manrope font-bold text-2xl leading-9 text-right">{{ .Quantity}} x {{ .ItemVariant.Price }}€</h6>
</div> </div>
</form> </form>
@@ -88,7 +74,7 @@
<div class="max-lg:max-w-lg max-lg:mx-auto"> <div class="max-lg:max-w-lg max-lg:mx-auto">
<p class="font-normal text-base leading-7 text-gray-500 text-center mb-5 mt-6">Shipping calculated at checkout</p> <p class="font-normal text-base leading-7 text-gray-500 text-center mb-5 mt-6">Shipping calculated at checkout</p>
<button type="submit" <button type="submit"
class="rounded-full py-4 px-6 bg-indigo-600 text-white font-semibold text-lg w-full text-center transition-all duration-500 hover:bg-indigo-700 ">Checkout</button> class="rounded py-4 px-6 bg-indigo-500 text-white font-semibold text-lg w-full text-center transition-all duration-500 hover:bg-indigo-700 ">Checkout</button>
</div> </div>
</form> </form>

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8"> <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm"> <div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Logo"> <img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Logo">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Checkout</h2> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Checkout</h2>
</div> </div>

View File

@@ -5,7 +5,7 @@
<h3 class="text-lg">{{ .data.shopItem.Name }}</h3> <h3 class="text-lg">{{ .data.shopItem.Name }}</h3>
<i class="text-xs">{{ .data.shopItem.Description }}</i> <i class="text-xs">{{ .data.shopItem.Description }}</i>
<p class="">Price: {{ .data.shopItem.BasePrice }}</p> <p class="">Price: {{ .data.shopItem.BasePrice }}</p>
{{ if .loggedIn }} {{ if .isAdmin }}
<p class="mt-10 text-center text-sm/6 text-red-500"> <p class="mt-10 text-center text-sm/6 text-red-500">
Do you really want to delete this item?? Do you really want to delete this item??

View File

@@ -2,8 +2,12 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8"> <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm"> <div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company"> <img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit Item</h2> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit Item</h2>
<a href="/shopitems/{{ .data.shopItem.ID }}">
<img src="/{{ .data.shopItem.Image }}" alt="Product Image" class="aspect-4/5 mx-auto rounded-md bg-gray-200 object-cover group-hover:opacity-75 lg:aspect-auto lg:h-80">
</a>
</div> </div>

View File

@@ -3,7 +3,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8"> <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm"> <div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company"> <img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit Orders</h2> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit Orders</h2>
</div> </div>

View File

@@ -12,8 +12,8 @@
<div class="mx-auto max-w-7xl px-4 sm:px-8 lg:px-8"> <div class="mx-auto max-w-7xl px-4 sm:px-8 lg:px-8">
<div class="relative flex h-16 items-center justify-between"> <div class="relative flex h-16 items-center justify-between">
<div class="flex flex-1 items-center"> <div class="flex flex-1 items-center">
<a href="/"><div class="flex shrink-0"> <a href="/"><div class="flex-shrink-0 w-full h-full">
<img class="h-8 w-auto" src="/static/img/circlea.png" alt="Your Company"> <img class="h-8 w-auto" src="/static/img/logo-white.png" alt="Your Company">
</div></a> </div></a>
<!-- <!--
{{ if .loggedIn }} {{ if .loggedIn }}
@@ -25,7 +25,7 @@
{{ end }} {{ end }}
--> -->
</div> </div>
{{ if .loggedIn }} {{ if .isAdmin }}
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8"> <div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8">
<a href="/additem" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Add Item</a> <a href="/additem" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Add Item</a>
<a href="/batchupload" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 <a href="/batchupload" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300
@@ -33,13 +33,15 @@
<a href="/orders" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 <a href="/orders" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300
hover:bg-gray-700 hover:text-white">Orders</a> hover:bg-gray-700 hover:text-white">Orders</a>
<a href="/tags" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Tags</a> <a href="/tags" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Tags</a>
<a href="/invites" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Invites</a>
<a href="/cart/print" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 <a href="/cart/print" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700
hover:text-white">Print</a> hover:text-white">Print</a>
<a href="/cart" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 <a href="/cart" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700
hover:text-white">Cart</a> hover:text-white">Cart</a>
<a href="/logout" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-red-300 hover:bg-gray-700 hover:text-white">Logout</a> <a href="/logout" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-red-300 hover:bg-gray-700 hover:text-white">Logout</a>
</div> </div>
{{ else }} {{ end }}
{{ if .loggedIn }}
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8"> <div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8">
<a href="/cart" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">&#128722; Cart</a> <a href="/cart" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">&#128722; Cart</a>
</div> </div>

28
views/invites.html Normal file
View File

@@ -0,0 +1,28 @@
{{ template "header.html" . }}
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Create/Delete Invites</h2>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
{{ range .tokens }}
<form action="/invites" method="POST">
<div class="max-w-md mx-auto mt-4">
<div class="flex">
<input type="text" id="token" name="token" value="{{ .Token }}" readonly="readonly" class="flex-grow border border-gray-300 rounded-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
<button type="submit" name="action" value="delete" class="bg-red-800 text-white rounded px-4 hover:bg-red-900">Delete</button>
</div>
</form>
{{ end }}
<form action="/invites" method="POST">
<div class="max-w-md mx-auto mt-4">
<div class="flex">
<button type="submit" name="action" value="create" class="bg-green-600 text-white ml-4 mr-4 rounded px-4 hover:bg-green-700">Create</button>
</div>
</div>
</form>
</div>
{{ template "footer.html" . }}

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8"> <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm"> <div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Logo"> <img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Logo">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Login to your account</h2> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Login to your account</h2>
</div> </div>

View File

@@ -74,11 +74,6 @@
</dl> </dl>
</div> </div>
<div class="flex items-start sm:items-center">
<input id="terms-checkbox-2" type="checkbox" value="" class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-primary-600 focus:ring-2 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-primary-600" />
<label for="terms-checkbox-2" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300"> I agree with the <a href="#" title="" class="text-primary-700 underline hover:no-underline dark:text-primary-500">Terms and Conditions</a> of use of the Flowbite marketplace </label>
</div>
<div class="gap-4 sm:flex sm:items-center"> <div class="gap-4 sm:flex sm:items-center">
<button type="button" class="w-full rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"><a href="/">Return to Shopping</a></button> <button type="button" class="w-full rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"><a href="/">Return to Shopping</a></button>

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8"> <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm"> <div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Logo"> <img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Logo">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Register your account</h2> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Register your account</h2>
</div> </div>

59
views/registertoken.html Normal file
View File

@@ -0,0 +1,59 @@
{{ template "header.html" . }}
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Logo">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Register your account</h2>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" action="/register" method="POST">
<div>
<label for="token" class="block text-sm/6 font-medium text-gray-900">Token</label>
<div class="mt-2">
<input type="text" name="token" id="token" value="{{ .token }}" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="name" class="block text-sm/6 font-medium text-gray-900">Username</label>
<div class="mt-2">
<input type="text" name="name" id="name" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="email" class="block text-sm/6 font-medium text-gray-900">Email address</label>
<div class="mt-2">
<input type="email" name="email" id="email" autocomplete="email" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<div class="flex items-center justify-between">
<label for="password" class="block text-sm/6 font-medium text-gray-900">Password</label>
</div>
<div class="mt-2">
<input type="password" name="password" id="password" autocomplete="current-password" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<p class="mt-10 text-center text-sm/6 text-red-500">
{{ .error }}
</p>
<p class="mt-10 text-center text-sm/6 text-green-500">
{{ .success }}
</p>
<div>
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Register</button>
</div>
</form>
</div>
</div>
{{ template "footer.html" . }}

View File

@@ -9,15 +9,18 @@
<img class="w-full h-full object-cover" src="/{{ .data.shopItem.Image}}" alt="Product Image"> <img class="w-full h-full object-cover" src="/{{ .data.shopItem.Image}}" alt="Product Image">
</div> </div>
<div class="flex -mx-2 mb-4"> <div class="flex -mx-2 mb-4">
{{ if .loggedIn }}
<input type="hidden" id="{{ .data.shopItem.ID}}" name="ShopItemId" value="{{ .data.shopItem.ID }}"> <input type="hidden" id="{{ .data.shopItem.ID}}" name="ShopItemId" value="{{ .data.shopItem.ID }}">
<div class="w-1/3 px-2"> <div class="w-1/3 px-2">
<button type="submit" class="w-full bg-gray-900 dark:bg-gray-600 text-white py-2 px-4 rounded-lg font-bold hover:bg-gray-800 dark:hover:bg-gray-700">Add to Cart</button> <button type="submit" class="w-full bg-gray-900 dark:bg-gray-600 text-white py-2 px-4 rounded-lg font-bold hover:bg-gray-800 dark:hover:bg-gray-700">Add to Cart</button>
</div> </div>
{{ end }}
<div class="w-1/3 px-2"> <div class="w-1/3 px-2">
<button type="button" class="w-full bg-blue-900 dark:bg-gray-600 text-white py-2 px-4 rounded-lg font-bold hover:bg-gray-800 dark:hover:bg-gray-700"><a href="/{{ .data.shopItem.Pdf }}">View</a></button> <button type="button" class="w-full bg-blue-900 dark:bg-gray-600 text-white py-2 px-4 rounded-lg font-bold hover:bg-gray-800 dark:hover:bg-gray-700"><a href="/{{ .data.shopItem.Pdf }}">View</a></button>
</div> </div>
{{ if .loggedIn }}
{{ if .isAdmin }}
<div class="w-1/3 px-2"> <div class="w-1/3 px-2">
<button type="button" class="w-full bg-blue-900 dark:bg-gray-600 text-white py-2 px-4 rounded-lg font-bold hover:bg-gray-800 dark:hover:bg-gray-700"><a href="{{ .data.shopItem.ID }}/edit">Edit</a></button> <button type="button" class="w-full bg-blue-900 dark:bg-gray-600 text-white py-2 px-4 rounded-lg font-bold hover:bg-gray-800 dark:hover:bg-gray-700"><a href="{{ .data.shopItem.ID }}/edit">Edit</a></button>
</div> </div>
@@ -34,6 +37,7 @@
{{ .data.shopItem.Abstract }} {{ .data.shopItem.Abstract }}
</p> </p>
{{ if .loggedIn }}
<div class="flex mb-4"> <div class="flex mb-4">
<label for="ItemVariantId" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"></label> <label for="ItemVariantId" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"></label>
<select name="ItemVariantId" id="ItemVariantId" required class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"> <select name="ItemVariantId" id="ItemVariantId" required class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
@@ -43,6 +47,7 @@
{{ end }} {{ end }}
</select> </select>
</div> </div>
{{ end }}
<div class="flex mb-4"> <div class="flex mb-4">
<div class="mr-4"> <div class="mr-4">

View File

@@ -1,14 +1,14 @@
<div class="bg-white"> <div class="bg-white">
<div class="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:max-w-7xl lg:px-8"> <div class="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:max-w-7xl">
<h2 class="text-2xl font-bold tracking-tight text-gray-900">Available Zines</h2> <h2 class="text-2xl font-bold tracking-tight text-gray-900">Available Zines</h2>
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for names.."> <input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for names..">
<div class="mt-6 grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-4 xl:gap-x-8"> <div class="mt-6 grid grid-cols-1 gap-x-6 gap-y-10 md:grid-cols-2 xl:grid-cols-4">
{{ range .data.shopItems }} {{ range .data.shopItems }}
<div class="myClass group relative"> <div class="myClass group relative">
<a href="/shopitems/{{ .ID }}"> <a href="/shopitems/{{ .ID }}">
<img src="/{{ .Image }}" alt="Product Image" class="aspect-4/5 w-full rounded-md bg-gray-200 object-cover group-hover:opacity-75 lg:aspect-auto lg:h-80"> <img src="/{{ .Image }}" alt="Product Image" class="aspect-4/5 mx-auto rounded bg-gray-200 object-cover group-hover:opacity-75 lg:aspect-auto lg:h-80">
</a> </a>
<div class="mt-4 flex justify-between"> <div class="mt-4 flex justify-between">
<div> <div>
@@ -25,7 +25,9 @@
{{ end }} {{ end }}
</p> </p>
</div> </div>
<p class="text-sm font-medium text-gray-900">{{ .BasePrice }}€</p> {{ if $.loggedIn }}
<p class="text-sm font-medium text-gray-900">{{ .BasePrice }}€</p>
{{ end }}
</div> </div>
</div> </div>

View File

@@ -3,7 +3,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8"> <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm"> <div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company"> <img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit Tags</h2> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit Tags</h2>
</div> </div>