From cac2986b59344292c5d0c4118d66c37be7dde423 Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Sat, 14 Dec 2024 22:38:06 -0500 Subject: [PATCH 01/15] authority system start --- models/permission.go | 16 ++ models/role-permissions.go | 16 ++ models/roles.go | 16 ++ models/user-roles.go | 16 ++ run-auto-migrations.go | 4 + utils/authority.go | 408 +++++++++++++++++++++++++++++++++++++ 6 files changed, 476 insertions(+) create mode 100644 models/permission.go create mode 100644 models/role-permissions.go create mode 100644 models/roles.go create mode 100644 models/user-roles.go create mode 100644 utils/authority.go diff --git a/models/permission.go b/models/permission.go new file mode 100644 index 0000000..933b16d --- /dev/null +++ b/models/permission.go @@ -0,0 +1,16 @@ +// Copyright (c) 2025 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package models + +type Permission struct { + BaseModel + Name string + Slug string +} + +// TableName sets the table name +func (Permission) TableName() string { + return "permissions" +} diff --git a/models/role-permissions.go b/models/role-permissions.go new file mode 100644 index 0000000..99cf3fe --- /dev/null +++ b/models/role-permissions.go @@ -0,0 +1,16 @@ +// Copyright (c) 2025 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package models + +type RolePermission struct { + BaseModel + RoleID uint // Role id + PermissionID uint // Permission id +} + +// TableName sets the table name +func (RolePermission) TableName() string { + return "role_permissions" +} diff --git a/models/roles.go b/models/roles.go new file mode 100644 index 0000000..decd675 --- /dev/null +++ b/models/roles.go @@ -0,0 +1,16 @@ +// Copyright (c) 2025 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package models + +type Role struct { + BaseModel + Name string // The name of the role + Slug string // String based unique identifier of the role, (use hyphen seperated role name '-', instead of space) +} + +// TableName sets the table name +func (Role) TableName() string { + return "roles" +} diff --git a/models/user-roles.go b/models/user-roles.go new file mode 100644 index 0000000..9c69ba7 --- /dev/null +++ b/models/user-roles.go @@ -0,0 +1,16 @@ +// Copyright (c) 2025 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package models + +type UserRole struct { + BaseModel + UserID string // The user id + RoleID uint // The role id +} + +// TableName sets the table name +func (UserRole) TableName() string { + return "user_roles" +} diff --git a/run-auto-migrations.go b/run-auto-migrations.go index 8410637..119a44c 100644 --- a/run-auto-migrations.go +++ b/run-auto-migrations.go @@ -18,4 +18,8 @@ func RunAutoMigrations() { // Add auto migrations for your models here... db.AutoMigrate(&models.User{}) + db.AutoMigrate(&models.UserRole{}) + db.AutoMigrate(&models.Role{}) + db.AutoMigrate(&models.RolePermission{}) + db.AutoMigrate(&models.Permission{}) } diff --git a/utils/authority.go b/utils/authority.go new file mode 100644 index 0000000..401f36b --- /dev/null +++ b/utils/authority.go @@ -0,0 +1,408 @@ +// Copyright (c) 2025 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package utils + +import ( + "errors" + "fmt" + + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/cup/models" + "gorm.io/gorm" +) + +type Authority struct{} + +var auth *Authority + +var ( + ErrPermissionInUse = errors.New("cannot delete assigned permission") + ErrPermissionNotFound = errors.New("permission not found") + ErrRoleInUse = errors.New("cannot delete assigned role") + ErrRoleNotFound = errors.New("role not found") +) + +// Add a new role to the database +func (a *Authority) CreateRole(c *core.Context, r models.Role) error { + roleSlug := r.Slug + var dbRole models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).First(&dbRole) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + // create + createRes := c.GetGorm().Create(&r) + if createRes.Error != nil { + return createRes.Error + } + return nil + } + return res.Error + } + + return errors.New(fmt.Sprintf("role '%v' already exists", roleSlug)) +} + +// Add a new permission to the database +func (a *Authority) CreatePermission(c *core.Context, p models.Permission) error { + permSlug := p.Slug + var dbPerm models.Permission + res := c.GetGorm().Where("slug = ?", permSlug).First(&dbPerm) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + // create + createRes := c.GetGorm().Create(&p) + if createRes.Error != nil { + return createRes.Error + } + return nil + } + return res.Error + } + + return errors.New(fmt.Sprintf("permission '%v' already exists", permSlug)) +} + +// Assigns a group of permissions to a given role +func (a *Authority) AssignPermissionsToRole(c *core.Context, roleSlug string, permSlugs []string) error { + var role models.Role + rRes := c.GetGorm().Where("slug = ?", roleSlug).First(&role) + if rRes.Error != nil { + if errors.Is(rRes.Error, gorm.ErrRecordNotFound) { + return ErrRoleNotFound + } + return rRes.Error + } + var perms []models.Permission + for _, permSlug := range permSlugs { + var perm models.Permission + pRes := c.GetGorm().Where("slug = ?", permSlug).First(&perm) + if pRes.Error != nil { + if errors.Is(pRes.Error, gorm.ErrRecordNotFound) { + return ErrPermissionNotFound + } + return pRes.Error + } + perms = append(perms, perm) + } + tx := c.GetGorm().Begin() + for _, perm := range perms { + var rolePerm models.RolePermission + res := c.GetGorm().Where("role_id = ?", role.ID).Where("permission_id =?", perm.ID).First(&rolePerm) + if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { + cRes := tx.Create(&models.RolePermission{RoleID: role.ID, PermissionID: perm.ID}) + if cRes.Error != nil { + tx.Rollback() + return cRes.Error + } + } + if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { + tx.Rollback() + return res.Error + } + if rolePerm != (models.RolePermission{}) { + tx.Rollback() + return errors.New(fmt.Sprintf("permission '%v' is aleady assigned to the role '%v'", perm.Name, role.Name)) + } + rolePerm = models.RolePermission{} + } + return tx.Commit().Error +} + +// Assigns a role to a given user +func (a *Authority) AssignRoleToUser(c *core.Context, userID interface{}, roleSlug string) error { + userIDStr := fmt.Sprintf("%v", userID) + var role models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).First(&role) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return ErrRoleNotFound + } + return res.Error + } + var userRole models.UserRole + res = c.GetGorm().Where("user_id = ?", userIDStr).Where("role_id = ?", role.ID).First(&userRole) + if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { + c.GetGorm().Create(&models.UserRole{UserID: userIDStr, RoleID: role.ID}) + return nil + } + if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { + return res.Error + } + + return errors.New(fmt.Sprintf("this role '%v' is aleady assigned to the user", roleSlug)) +} + +// Checks if a role is assigned to a user +func (a *Authority) CheckUserRole(c *core.Context, userID interface{}, roleSlug string) (bool, error) { + userIDStr := fmt.Sprintf("%v", userID) + // find the role + var role models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).First(&role) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, ErrRoleNotFound + } + return false, res.Error + } + + // check if the role is a assigned + var userRole models.UserRole + res = c.GetGorm().Where("user_id = ?", userIDStr).Where("role_id = ?", role.ID).First(&userRole) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, nil + } + return false, res.Error + } + + return true, nil +} + +// Checks if a permission is assigned to a user +func (a *Authority) CheckUserPermission(c *core.Context, userID interface{}, permSlug string) (bool, error) { + userIDStr := fmt.Sprintf("%v", userID) + // the user role + var userRoles []models.UserRole + res := c.GetGorm().Where("user_id = ?", userIDStr).Find(&userRoles) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, nil + } + return false, res.Error + } + + //prepare an array of role ids + var roleIDs []interface{} + for _, r := range userRoles { + roleIDs = append(roleIDs, r.RoleID) + } + + // find the permission + var perm models.Permission + res = c.GetGorm().Where("slug = ?", permSlug).First(&perm) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, ErrPermissionNotFound + } + return false, res.Error + } + + // find the role permission + var rolePermission models.RolePermission + res = c.GetGorm().Where("role_id IN (?)", roleIDs).Where("permission_id = ?", perm.ID).First(&rolePermission) + if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, nil + } + if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, res.Error + } + + return true, nil +} + +// Checks if a permission is assigned to a role +func (a *Authority) CheckRolePermission(c *core.Context, roleSlug string, permSlug string) (bool, error) { + // find the role + var role models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).First(&role) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, ErrRoleNotFound + } + return false, res.Error + } + + // find the permission + var perm models.Permission + res = c.GetGorm().Where("slug = ?", permSlug).First(&perm) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, ErrPermissionNotFound + } + return false, res.Error + } + + // find the rolePermission + var rolePermission models.RolePermission + res = c.GetGorm().Where("role_id = ?", role.ID).Where("permission_id = ?", perm.ID).First(&rolePermission) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return false, nil + } + return false, res.Error + } + + return true, nil +} + +// Revokes a roles's permission +func (a *Authority) RevokeRolePermission(c *core.Context, roleSlug string, permSlug string) error { + // find the role + var role models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).First(&role) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return ErrRoleNotFound + } + return res.Error + } + + // find the permission + var perm models.Permission + res = c.GetGorm().Where("slug = ?", permSlug).First(&perm) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return ErrPermissionNotFound + } + return res.Error + } + + // revoke the permission + rRes := c.GetGorm().Where("role_id = ?", role.ID).Where("permission_id = ?", perm.ID).Delete(models.RolePermission{}) + if rRes.Error != nil { + return rRes.Error + } + + return nil +} + +// Returns all stored roles +func (a *Authority) GetAllRoles(c *core.Context) ([]models.Role, error) { + var roles []models.Role + res := c.GetGorm().Find(&roles) + if res.Error != nil { + return nil, res.Error + } + + return roles, nil +} + +// Returns all user assigned roles +func (a *Authority) GetUserRoles(c *core.Context, userID interface{}) ([]models.Role, error) { + userIDStr := fmt.Sprintf("%v", userID) + var userRoles []models.UserRole + res := c.GetGorm().Where("user_id = ?", userIDStr).Find(&userRoles) + if res.Error != nil { + return nil, res.Error + } + + var roleIDs []interface{} + for _, r := range userRoles { + roleIDs = append(roleIDs, r.RoleID) + } + + var roles []models.Role + res = c.GetGorm().Where("id IN (?)", roleIDs).Find(&roles) + if res.Error != nil { + return nil, res.Error + } + + return roles, nil +} + +// Returns all role assigned permissions +func (a *Authority) GetRolePermissions(c *core.Context, roleSlug string) ([]models.Permission, error) { + var role models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).Find(&role) + if res.Error != nil { + return nil, res.Error + } + + var rolePerms []models.RolePermission + res = c.GetGorm().Where("role_id = ?", role.ID).Find(&rolePerms) + if res.Error != nil { + return nil, res.Error + } + var permIDs []interface{} + for _, rolePerm := range rolePerms { + permIDs = append(permIDs, rolePerm.PermissionID) + } + + var perms []models.Permission + res = c.GetGorm().Where("id IN (?)", permIDs).Find(&perms) + if res.Error != nil { + return nil, res.Error + } + + return perms, nil +} + +// Returns all stored permissions +func (a *Authority) GetAllPermissions(c *core.Context) ([]models.Permission, error) { + var perms []models.Permission + res := c.GetGorm().Find(&perms) + if res.Error != nil { + return nil, res.Error + } + + return perms, nil +} + +// Deletes a given role even if it's has assigned permissions +func (a *Authority) DeleteRole(c *core.Context, roleSlug string) error { + // find the role + var role models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).First(&role) + if res.Error != nil { + return res.Error + } + + // check if the role is assigned to a user + var ca int64 + res = c.GetGorm().Model(models.UserRole{}).Where("role_id = ?", role.ID).Count(&ca) + if res.Error != nil { + return res.Error + } + + if ca != 0 { + // role is assigned + return ErrRoleInUse + } + tx := c.GetGorm().Begin() + // revoke the assignment of permissions before deleting the role + dRes := tx.Where("role_id = ?", role.ID).Delete(models.RolePermission{}) + if dRes.Error != nil { + tx.Rollback() + return dRes.Error + } + + // delete the role + dRes = c.GetGorm().Where("slug = ?", roleSlug).Delete(models.Role{}) + if dRes.Error != nil { + tx.Rollback() + return dRes.Error + } + + return tx.Commit().Error +} + +// Deletes a given permission +func (a *Authority) DeletePermission(c *core.Context, permSlug string) error { + // find the permission + var perm models.Permission + res := c.GetGorm().Where("slug = ?", permSlug).First(&perm) + if res.Error != nil { + return res.Error + } + + // check if the permission is assigned to a role + var rolePermission models.RolePermission + res = c.GetGorm().Where("permission_id = ?", perm.ID).First(&rolePermission) + if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { + return res.Error + } + + if res.Error == nil { + return ErrPermissionInUse + } + + // delete the permission + dRes := c.GetGorm().Where("slug = ?", permSlug).Delete(models.Permission{}) + if dRes.Error != nil { + return dRes.Error + } + + return nil +} From 43f3ad986e7492b54b8d87ca38dd9596c2003ab7 Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Mon, 16 Dec 2024 20:05:37 -0500 Subject: [PATCH 02/15] fix user id, seed data autority --- models/user-roles.go | 4 +-- run-auto-migrations.go | 15 ++++++++++- utils/authority.go | 22 +++++++--------- utils/helpers.go | 60 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 16 deletions(-) diff --git a/models/user-roles.go b/models/user-roles.go index 9c69ba7..1dfcc18 100644 --- a/models/user-roles.go +++ b/models/user-roles.go @@ -6,8 +6,8 @@ package models type UserRole struct { BaseModel - UserID string // The user id - RoleID uint // The role id + UserID uint // The user id + RoleID uint // The role id } // TableName sets the table name diff --git a/run-auto-migrations.go b/run-auto-migrations.go index 119a44c..996ae15 100644 --- a/run-auto-migrations.go +++ b/run-auto-migrations.go @@ -6,8 +6,12 @@ package main import ( + "errors" + "git.smarteching.com/goffee/core" "git.smarteching.com/goffee/cup/models" + "git.smarteching.com/goffee/cup/utils" + "gorm.io/gorm" ) func RunAutoMigrations() { @@ -17,9 +21,18 @@ func RunAutoMigrations() { //############################## // Add auto migrations for your models here... - db.AutoMigrate(&models.User{}) db.AutoMigrate(&models.UserRole{}) db.AutoMigrate(&models.Role{}) db.AutoMigrate(&models.RolePermission{}) db.AutoMigrate(&models.Permission{}) + + // End your auto migrations + + // Create seed data data, DO NOT TOUCH + if err := db.AutoMigrate(&models.User{}); err == nil && db.Migrator().HasTable(&models.User{}) { + if err := db.First(&models.User{}).Error; errors.Is(err, gorm.ErrRecordNotFound) { + utils.CreateSeedData() + } + } + } diff --git a/utils/authority.go b/utils/authority.go index 401f36b..33124e8 100644 --- a/utils/authority.go +++ b/utils/authority.go @@ -111,8 +111,7 @@ func (a *Authority) AssignPermissionsToRole(c *core.Context, roleSlug string, pe } // Assigns a role to a given user -func (a *Authority) AssignRoleToUser(c *core.Context, userID interface{}, roleSlug string) error { - userIDStr := fmt.Sprintf("%v", userID) +func (a *Authority) AssignRoleToUser(c *core.Context, userID uint, roleSlug string) error { var role models.Role res := c.GetGorm().Where("slug = ?", roleSlug).First(&role) if res.Error != nil { @@ -122,9 +121,9 @@ func (a *Authority) AssignRoleToUser(c *core.Context, userID interface{}, roleSl return res.Error } var userRole models.UserRole - res = c.GetGorm().Where("user_id = ?", userIDStr).Where("role_id = ?", role.ID).First(&userRole) + res = c.GetGorm().Where("user_id = ?", userID).Where("role_id = ?", role.ID).First(&userRole) if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { - c.GetGorm().Create(&models.UserRole{UserID: userIDStr, RoleID: role.ID}) + c.GetGorm().Create(&models.UserRole{UserID: userID, RoleID: role.ID}) return nil } if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { @@ -135,8 +134,7 @@ func (a *Authority) AssignRoleToUser(c *core.Context, userID interface{}, roleSl } // Checks if a role is assigned to a user -func (a *Authority) CheckUserRole(c *core.Context, userID interface{}, roleSlug string) (bool, error) { - userIDStr := fmt.Sprintf("%v", userID) +func (a *Authority) CheckUserRole(c *core.Context, userID uint, roleSlug string) (bool, error) { // find the role var role models.Role res := c.GetGorm().Where("slug = ?", roleSlug).First(&role) @@ -149,7 +147,7 @@ func (a *Authority) CheckUserRole(c *core.Context, userID interface{}, roleSlug // check if the role is a assigned var userRole models.UserRole - res = c.GetGorm().Where("user_id = ?", userIDStr).Where("role_id = ?", role.ID).First(&userRole) + res = c.GetGorm().Where("user_id = ?", userID).Where("role_id = ?", role.ID).First(&userRole) if res.Error != nil { if errors.Is(res.Error, gorm.ErrRecordNotFound) { return false, nil @@ -161,11 +159,10 @@ func (a *Authority) CheckUserRole(c *core.Context, userID interface{}, roleSlug } // Checks if a permission is assigned to a user -func (a *Authority) CheckUserPermission(c *core.Context, userID interface{}, permSlug string) (bool, error) { - userIDStr := fmt.Sprintf("%v", userID) +func (a *Authority) CheckUserPermission(c *core.Context, userID uint, permSlug string) (bool, error) { // the user role var userRoles []models.UserRole - res := c.GetGorm().Where("user_id = ?", userIDStr).Find(&userRoles) + res := c.GetGorm().Where("user_id = ?", userID).Find(&userRoles) if res.Error != nil { if errors.Is(res.Error, gorm.ErrRecordNotFound) { return false, nil @@ -280,10 +277,9 @@ func (a *Authority) GetAllRoles(c *core.Context) ([]models.Role, error) { } // Returns all user assigned roles -func (a *Authority) GetUserRoles(c *core.Context, userID interface{}) ([]models.Role, error) { - userIDStr := fmt.Sprintf("%v", userID) +func (a *Authority) GetUserRoles(c *core.Context, userID uint) ([]models.Role, error) { var userRoles []models.UserRole - res := c.GetGorm().Where("user_id = ?", userIDStr).Find(&userRoles) + res := c.GetGorm().Where("user_id = ?", userID).Find(&userRoles) if res.Error != nil { return nil, res.Error } diff --git a/utils/helpers.go b/utils/helpers.go index 36ac76b..8ca7dd8 100644 --- a/utils/helpers.go +++ b/utils/helpers.go @@ -8,9 +8,69 @@ package utils import ( "crypto/md5" "fmt" + "log" "time" + + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/cup/models" ) +func CreateSeedData() { + + db := core.ResolveGorm() + var hashing = new(core.Hashing) + var role models.Role + + // seed user + password := "goffee" + name := "admin" + fullname := "Goffee administrator" + email := "change@me.com" + passwordHashed, _ := hashing.HashPassword(password) + + user := models.User{ + Name: name, + Fullname: fullname, + Email: email, + Password: passwordHashed, + } + result := db.Create(&user) + if result.Error != nil { + log.Fatal("Can't create seed user in database") + } + // seed roles + roles := []models.Role{ + {Name: "Administrator", Slug: "admin"}, + {Name: "Authenticated", Slug: "authenticated"}, + } + + for _, role := range roles { + result = db.Create(&role) + if result.Error != nil { + log.Fatal("Can't create seed role in database") + } + } + + // seed permission + permission := models.Permission{Name: "Users administration", Slug: "admin-users"} + result = db.Create(&permission) + if result.Error != nil { + log.Fatal("Can't create seed permission in database") + } + result = db.Where("slug = ?", "admin").First(&role) + if result.Error != nil { + log.Fatal("Can't find user admin in database") + } + result = db.Create(&models.RolePermission{RoleID: role.ID, PermissionID: permission.ID}) + if result.Error != nil { + log.Fatal("Can't register permission role in database") + } + result = db.Create(&models.UserRole{UserID: user.ID, RoleID: role.ID}) + if result.Error != nil { + log.Fatal("Can't assign role administrator to user in database") + } +} + // generate a hashed string to be used as key for caching auth jwt token func CreateAuthTokenHashedCacheKey(userID uint, userAgent string) string { cacheKey := fmt.Sprintf("userid:_%v_useragent:_%v_jwt_token", userID, userAgent) From d43196318178d1684b5dee618e516c2924ba8ebc Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Tue, 17 Dec 2024 14:23:07 -0500 Subject: [PATCH 03/15] Update sample user admin to user authority, add revoke role to auth --- controllers/adminusers.go | 128 +++++++++++++++++++++++++- storage/templates/admin_useradd.html | 3 +- storage/templates/admin_useredit.html | 3 +- storage/templates/nopermission.html | 12 +++ utils/authority.go | 32 +++++++ 5 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 storage/templates/nopermission.html diff --git a/controllers/adminusers.go b/controllers/adminusers.go index b051308..9ffb762 100644 --- a/controllers/adminusers.go +++ b/controllers/adminusers.go @@ -19,6 +19,29 @@ import ( func AdminUsersList(c *core.Context) *core.Response { + // initiate authority + auth := new(utils.Authority) + var session = new(utils.SessionUser) + // true if session is active + hassession := session.Init(c) + + if !hassession { + type emptytemplate struct{} + emptyData := emptytemplate{} + return c.Response.Template("nopermission.html", emptyData) + } + + session_uid := session.GetUserID() + // check if user has role admin + is_admin, _ := auth.CheckUserRole(c, session_uid, "admin") + + if !is_admin { + type emptytemplate struct{} + emptyData := emptytemplate{} + return c.Response.Template("nopermission.html", emptyData) + } + + // continue if has session and is admin var users []models.User db := c.GetGorm() db.Find(&users) @@ -46,6 +69,10 @@ func AdminUsersList(c *core.Context) *core.Response { Value: "Email", ValueType: "string", }, + { + Value: "Roles", + ValueType: "string", + }, { Value: "Created", }, @@ -57,13 +84,26 @@ func AdminUsersList(c *core.Context) *core.Response { }, } + var listroles string rows := make([][]components.ContentTableTD, len(users)) for i, u := range users { + + roles, _ := auth.GetUserRoles(c, u.ID) + listroles = "" + + for _, role := range roles { + if listroles != "" { + listroles += ", " + } + listroles += role.Name + } + row := []components.ContentTableTD{ {Value: strconv.Itoa(int(u.ID))}, {Value: u.Name}, {Value: u.Fullname}, {Value: u.Email}, + {Value: listroles}, {Value: utils.FormatUnix(u.Created)}, {Value: utils.FormatUnix(u.Updated)}, {Value: components.ContentHref{ @@ -93,10 +133,44 @@ func AdminUsersList(c *core.Context) *core.Response { func AdminUsersAdd(c *core.Context) *core.Response { + // initiate authority + auth := new(utils.Authority) + var session = new(utils.SessionUser) + // true if session is active + hassession := session.Init(c) + + if !hassession { + type emptytemplate struct{} + emptyData := emptytemplate{} + return c.Response.Template("nopermission.html", emptyData) + } + + session_uid := session.GetUserID() + // check if user has role admin + is_admin, _ := auth.CheckUserRole(c, session_uid, "admin") + + if !is_admin { + type emptytemplate struct{} + emptyData := emptytemplate{} + return c.Response.Template("nopermission.html", emptyData) + } + // check if is submit submit := c.GetRequestParam("submit").(string) - errormessages := make([]string, 0) + var listroles []components.FormCheckboxItem + systemroles, _ := auth.GetAllRoles(c) + + for _, systemrole := range systemroles { + var userrole components.FormCheckboxItem + userrole.Label = systemrole.Name + userrole.Name = "roles" + userrole.Value = systemrole.Slug + if systemrole.Slug == "authenticated" { + userrole.IsChecked = true + } + listroles = append(listroles, userrole) + } name := "" fullname := "" @@ -109,6 +183,7 @@ func AdminUsersAdd(c *core.Context) *core.Response { fullname = c.GetRequestParam("fullname").(string) email = c.GetRequestParam("email").(string) password = c.GetRequestParam("password").(string) + roles := c.GetRequesForm("roles").([]string) // check if email exists var user models.User @@ -161,6 +236,11 @@ func AdminUsersAdd(c *core.Context) *core.Response { errormessages = append(errormessages, res.Error.Error()) } else { + // assign roles + for _, role := range roles { + auth.AssignRoleToUser(c, user.ID, role) + } + // fire user registered event err := c.GetEventsManager().Fire(&core.Event{Name: events.USER_REGISTERED, Payload: map[string]interface{}{ "user": user, @@ -180,6 +260,7 @@ func AdminUsersAdd(c *core.Context) *core.Response { FieldName components.FormInput FieldFullname components.FormInput FieldEmail components.FormInput + FieldRoles components.FormCheckbox FieldPassword components.FormInput ErrorMessages []string SubmitButton components.FormButton @@ -208,6 +289,10 @@ func AdminUsersAdd(c *core.Context) *core.Response { //Autocomplete: true, IsRequired: true, }, + FieldRoles: components.FormCheckbox{ + Label: "Roles", + AllCheckbox: listroles, + }, FieldPassword: components.FormInput{ ID: "password", Label: "Password", @@ -236,6 +321,29 @@ func AdminUsersEdit(c *core.Context) *core.Response { user_id := c.GetPathParam("id") errormessages := make([]string, 0) + // initiate authority + auth := new(utils.Authority) + + var listroles []components.FormCheckboxItem + + systemroles, _ := auth.GetAllRoles(c) + user_id_uint, _ := strconv.ParseUint(user_id.(string), 10, 32) + + userroles, _ := auth.GetUserRoles(c, uint(user_id_uint)) + + for _, systemrole := range systemroles { + var userrole components.FormCheckboxItem + userrole.Label = systemrole.Name + userrole.Name = "roles" + userrole.Value = systemrole.Slug + for _, ur := range userroles { + if ur.Slug == systemrole.Slug { + userrole.IsChecked = true + break + } + } + listroles = append(listroles, userrole) + } var origin_user models.User @@ -259,6 +367,7 @@ func AdminUsersEdit(c *core.Context) *core.Response { fullname = c.GetRequestParam("fullname").(string) email = c.GetRequestParam("email").(string) password = c.GetRequestParam("password").(string) + roles := c.GetRequesForm("roles").([]string) key := c.GetRequestParam("key") // check if email exists @@ -316,6 +425,14 @@ func AdminUsersEdit(c *core.Context) *core.Response { c.GetLogger().Error("Admin user: error updating") errormessages = append(errormessages, fmt.Sprintf("Error updating user %s:", user_id_string)) } else { + + // delete roles + auth.RevokeAllUserRole(c, origin_user.ID) + // assign roles + for _, role := range roles { + auth.AssignRoleToUser(c, origin_user.ID, role) + } + return c.Response.Redirect("/admin/users") } } @@ -325,6 +442,7 @@ func AdminUsersEdit(c *core.Context) *core.Response { FieldName components.FormInput FieldFullname components.FormInput FieldEmail components.FormInput + FieldRoles components.FormCheckbox FieldPassword components.FormInput FieldKey components.FormInput ErrorMessages []string @@ -355,6 +473,10 @@ func AdminUsersEdit(c *core.Context) *core.Response { //Autocomplete: true, IsRequired: true, }, + FieldRoles: components.FormCheckbox{ + Label: "Roles", + AllCheckbox: listroles, + }, FieldPassword: components.FormInput{ ID: "password", Label: "Password", @@ -461,6 +583,9 @@ func AdminUsersDelConfirm(c *core.Context) *core.Response { // check if is the seed user seed := "1" if user_id != seed { + + // initiate authority + auth := new(utils.Authority) // Delete the user // fire user delete event err := c.GetEventsManager().Fire(&core.Event{Name: events.USER_DELETED, Payload: map[string]interface{}{ @@ -469,6 +594,7 @@ func AdminUsersDelConfirm(c *core.Context) *core.Response { if err != nil { c.GetLogger().Error(err.Error()) } + auth.RevokeAllUserRole(c, origin_user.ID) result_db.Unscoped().Delete(&origin_user) } } diff --git a/storage/templates/admin_useradd.html b/storage/templates/admin_useradd.html index ccdb363..bc5d780 100644 --- a/storage/templates/admin_useradd.html +++ b/storage/templates/admin_useradd.html @@ -15,7 +15,8 @@
{{template "form_input" .FieldName}} {{template "form_input" .FieldFullname}} - {{template "form_input" .FieldEmail}} + {{template "form_input" .FieldEmail}} + {{template "form_checkbox" .FieldRoles}} {{template "form_input" .FieldPassword}}
{{template "form_button" .SubmitButton}} diff --git a/storage/templates/admin_useredit.html b/storage/templates/admin_useredit.html index 36ebfdf..3c49e08 100644 --- a/storage/templates/admin_useredit.html +++ b/storage/templates/admin_useredit.html @@ -15,7 +15,8 @@ {{template "form_input" .FieldName}} {{template "form_input" .FieldFullname}} - {{template "form_input" .FieldEmail}} + {{template "form_input" .FieldEmail}} + {{template "form_checkbox" .FieldRoles}} {{template "form_input" .FieldPassword}} {{template "form_input" .FieldKey}} {{template "form_button" .SubmitButton}} diff --git a/storage/templates/nopermission.html b/storage/templates/nopermission.html new file mode 100644 index 0000000..bec8f39 --- /dev/null +++ b/storage/templates/nopermission.html @@ -0,0 +1,12 @@ + + + {{template "page_head" "No permission"}} + +
+
+ You do not have permission to visit this page. +
+
+ {{template "page_footer"}} + + \ No newline at end of file diff --git a/utils/authority.go b/utils/authority.go index 33124e8..823f845 100644 --- a/utils/authority.go +++ b/utils/authority.go @@ -265,6 +265,38 @@ func (a *Authority) RevokeRolePermission(c *core.Context, roleSlug string, permS return nil } +// Revokes a user's role +func (a *Authority) RevokeUserRole(c *core.Context, userID uint, roleSlug string) error { + // find the role + var role models.Role + res := c.GetGorm().Where("slug = ?", roleSlug).First(&role) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return ErrRoleNotFound + } + return res.Error + } + + // revoke the role + rRes := c.GetGorm().Where("user_id = ?", userID).Where("role_id = ?", role.ID).Delete(models.UserRole{}) + if rRes.Error != nil { + return rRes.Error + } + + return nil +} + +// Revokes all user's role +func (a *Authority) RevokeAllUserRole(c *core.Context, userID uint) error { + // revoke the role + rRes := c.GetGorm().Where("user_id = ?", userID).Delete(models.UserRole{}) + if rRes.Error != nil { + return rRes.Error + } + + return nil +} + // Returns all stored roles func (a *Authority) GetAllRoles(c *core.Context) ([]models.Role, error) { var roles []models.Role From d413d7def4dcad8e500e194ab6c787aa04fb5d1e Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Mon, 23 Dec 2024 23:19:22 -0500 Subject: [PATCH 04/15] add base queue system --- controllers/queuesample.go | 52 ++++++++++++++++++++++++++++++++++++++ go.mod | 6 +++++ go.sum | 12 +++++++++ main.go | 1 + register-queues.go | 30 ++++++++++++++++++++++ routes.go | 1 + workers/workers.go | 39 ++++++++++++++++++++++++++++ 7 files changed, 141 insertions(+) create mode 100644 controllers/queuesample.go create mode 100644 register-queues.go create mode 100644 workers/workers.go diff --git a/controllers/queuesample.go b/controllers/queuesample.go new file mode 100644 index 0000000..d289aee --- /dev/null +++ b/controllers/queuesample.go @@ -0,0 +1,52 @@ +// Copyright (c) 2025 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package controllers + +import ( + "encoding/json" + "log" + "time" + + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/cup/workers" + "github.com/hibiken/asynq" +) + +// Make samples queues +func Queuesample(c *core.Context) *core.Response { + + // Get client queue asynq + client := c.GetQueueClient() + + // Create a task with typename and payload. + payload, err := json.Marshal(workers.EmailTaskPayload{UserID: 42}) + if err != nil { + log.Fatal(err) + } + + t1 := asynq.NewTask(workers.TypeWelcomeEmail, payload) + + t2 := asynq.NewTask(workers.TypeReminderEmail, payload) + + // Process the task immediately. + info, err := client.Enqueue(t1) + if err != nil { + log.Fatal(err) + } + log.Printf(" [*] Successfully enqueued task: %+v", info) + + // Process 2 task 1 min later. + for i := 1; i < 3; i++ { + info, err = client.Enqueue(t2, asynq.ProcessIn(1*time.Minute)) + if err != nil { + log.Fatal(err) + } + log.Printf(" [*] Successfully enqueued task: %+v", info) + } + + message := "{\"message\": \"Task queued\"}" + return c.Response.Json(message) + +} diff --git a/go.mod b/go.mod index e70b070..2e7078c 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ go 1.23.1 require ( git.smarteching.com/goffee/core v1.8.4 github.com/google/uuid v1.6.0 + github.com/hibiken/asynq v0.25.1 github.com/joho/godotenv v1.5.1 github.com/julienschmidt/httprouter v1.3.0 gorm.io/gorm v1.25.12 @@ -44,14 +45,19 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/redis/go-redis/v9 v9.7.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/sendgrid/sendgrid-go v3.16.0+incompatible // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cast v1.7.0 // indirect golang.org/x/crypto v0.28.0 // indirect golang.org/x/image v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.8.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect gorm.io/driver/mysql v1.5.7 // indirect gorm.io/driver/postgres v1.5.9 // indirect gorm.io/driver/sqlite v1.5.6 // indirect diff --git a/go.sum b/go.sum index be301b7..ca95d88 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/harranali/mailing v1.2.0 h1:ihIyJwB8hyRVcdk+v465wk1PHMrSrgJqo/kMd+gZClY= github.com/harranali/mailing v1.2.0/go.mod h1:4a5N3yG98pZKluMpmcYlTtll7bisvOfGQEMIng3VQk4= +github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw= +github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -89,6 +91,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= @@ -96,6 +100,8 @@ github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBU github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -116,11 +122,17 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go index 4b9c956..8e7c815 100644 --- a/main.go +++ b/main.go @@ -62,6 +62,7 @@ func main() { registerGlobalHooks() registerRoutes() registerEvents() + registerQueues() if config.GetGormConfig().EnableGorm == true { RunAutoMigrations() } diff --git a/register-queues.go b/register-queues.go new file mode 100644 index 0000000..a1cf059 --- /dev/null +++ b/register-queues.go @@ -0,0 +1,30 @@ +// Copyright (c) 2025 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package main + +import ( + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/cup/workers" +) + +// Register queues +func registerQueues() { + + var queque = new(core.Queuemux) + queque.QueueInit() + //######################################## + //# quques registration ##### + //######################################## + + // register your queues here ... + + queque.AddWork(workers.TypeWelcomeEmail, workers.HandleWelcomeEmailTask) + queque.AddWork(workers.TypeReminderEmail, workers.HandleReminderEmailTask) + + //######################################## + // Start queue server, DO NOT TOUCH + //######################################## + go queque.RunQueueserver() +} diff --git a/routes.go b/routes.go index 785c59e..f8d7fec 100644 --- a/routes.go +++ b/routes.go @@ -26,6 +26,7 @@ func registerRoutes() { controller.Get("/themecontent", controllers.Themecontent) controller.Get("/themepanel", controllers.Themedemo) controller.Get("/themeelements", controllers.ThemeElements) + controller.Get("/queuesample", controllers.Queuesample) // Uncomment the lines below to enable authentication controller.Post("/signup", controllers.Signup) diff --git a/workers/workers.go b/workers/workers.go new file mode 100644 index 0000000..e7a9cad --- /dev/null +++ b/workers/workers.go @@ -0,0 +1,39 @@ +package workers + +import ( + "context" + "encoding/json" + "log" + + "github.com/hibiken/asynq" +) + +// A list of task types. +const ( + TypeWelcomeEmail = "email:welcome" + TypeReminderEmail = "email:reminder" +) + +// Task payload for any email related tasks. +type EmailTaskPayload struct { + // ID for the email recipient. + UserID int +} + +func HandleWelcomeEmailTask(ctx context.Context, t *asynq.Task) error { + var p EmailTaskPayload + if err := json.Unmarshal(t.Payload(), &p); err != nil { + return err + } + log.Printf(" [*] Send Welcome Email to User %d", p.UserID) + return nil +} + +func HandleReminderEmailTask(ctx context.Context, t *asynq.Task) error { + var p EmailTaskPayload + if err := json.Unmarshal(t.Payload(), &p); err != nil { + return err + } + log.Printf(" [*] Send Reminder Email to User %d", p.UserID) + return nil +} From 273b43f5e0c4f102405a29a2fea60f0f30f8f865 Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Mon, 23 Dec 2024 23:40:22 -0500 Subject: [PATCH 05/15] add config option to disable queues --- config/queue.go | 20 ++++++++++++++++++++ main.go | 4 +++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 config/queue.go diff --git a/config/queue.go b/config/queue.go new file mode 100644 index 0000000..8060572 --- /dev/null +++ b/config/queue.go @@ -0,0 +1,20 @@ +// Copyright (c) 2025 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package config + +import "git.smarteching.com/goffee/core" + +// Retrieve the main config for the Queue +func GetQueueConfig() core.QueueConfig { + //##################################### + //# Main configuration for Queue ##### + //##################################### + + return core.QueueConfig{ + // For enabling and disabling the queue system + // set to true to enable it, set to false to disable + EnableQueue: false, + } +} diff --git a/main.go b/main.go index 8e7c815..5fac06e 100644 --- a/main.go +++ b/main.go @@ -62,7 +62,9 @@ func main() { registerGlobalHooks() registerRoutes() registerEvents() - registerQueues() + if config.GetQueueConfig().EnableQueue == true { + registerQueues() + } if config.GetGormConfig().EnableGorm == true { RunAutoMigrations() } From d5725093511d0b146e5fa00c495195859fc5ef9b Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Mon, 23 Dec 2024 23:58:48 -0500 Subject: [PATCH 06/15] fix core version --- go.mod | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2e7078c..e6be3df 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module git.smarteching.com/goffee/cup replace ( + git.smarteching.com/goffee/core => ../core git.smarteching.com/goffee/cup/config => ./config git.smarteching.com/goffee/cup/handlers => ./handlers git.smarteching.com/goffee/cup/middlewares => ./middlewares @@ -10,7 +11,7 @@ replace ( go 1.23.1 require ( - git.smarteching.com/goffee/core v1.8.4 + git.smarteching.com/goffee/core v1.9.0 github.com/google/uuid v1.6.0 github.com/hibiken/asynq v0.25.1 github.com/joho/godotenv v1.5.1 From 7a32380274b97b8f073d1f0a4be2763fbaa4086f Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Tue, 24 Dec 2024 00:01:42 -0500 Subject: [PATCH 07/15] fix core version --- go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/go.mod b/go.mod index e6be3df..8b0cfa0 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,6 @@ module git.smarteching.com/goffee/cup replace ( - git.smarteching.com/goffee/core => ../core git.smarteching.com/goffee/cup/config => ./config git.smarteching.com/goffee/cup/handlers => ./handlers git.smarteching.com/goffee/cup/middlewares => ./middlewares From f92910abfd8305659bfabfafda107030553ea120 Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Sat, 22 Feb 2025 20:59:16 -0500 Subject: [PATCH 08/15] update go1.24.0 --- go.mod | 32 ++++++++++++++++---------------- go.sum | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 8b0cfa0..a7cfe99 100644 --- a/go.mod +++ b/go.mod @@ -25,40 +25,40 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/go-chi/chi/v5 v5.1.0 // indirect + github.com/go-chi/chi/v5 v5.2.1 // indirect github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect - github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/go-sql-driver/mysql v1.9.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/harranali/mailing v1.2.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/pgx/v5 v5.7.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailgun/errors v0.4.0 // indirect - github.com/mailgun/mailgun-go/v4 v4.17.3 // indirect + github.com/mailgun/mailgun-go/v4 v4.22.1 // indirect github.com/mattn/go-sqlite3 v1.14.24 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/redis/go-redis/v9 v9.7.0 // indirect + github.com/redis/go-redis/v9 v9.7.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/sendgrid/sendgrid-go v3.16.0+incompatible // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/cast v1.7.0 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/image v0.21.0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.19.0 // indirect - golang.org/x/time v0.8.0 // indirect - google.golang.org/protobuf v1.35.2 // indirect + github.com/spf13/cast v1.7.1 // indirect + golang.org/x/crypto v0.34.0 // indirect + golang.org/x/image v0.24.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/time v0.10.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect gorm.io/driver/mysql v1.5.7 // indirect - gorm.io/driver/postgres v1.5.9 // indirect - gorm.io/driver/sqlite v1.5.6 // indirect + gorm.io/driver/postgres v1.5.11 // indirect + gorm.io/driver/sqlite v1.5.7 // indirect ) diff --git a/go.sum b/go.sum index ca95d88..c25cd73 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= git.smarteching.com/goffee/core v1.8.4 h1:XB+vpe7e8muiDChRVDaJ1TG7H+/FBxDQcMfWp4zloPs= git.smarteching.com/goffee/core v1.8.4/go.mod h1:JxXDvTQU2shKYY6c9aS3s6sFh7mEDzgmjzdc85HhBV8= +git.smarteching.com/goffee/core v1.9.0 h1:aND9YkkDXEqsgEoDA04D9pmum2D3/nNljO1ojE1nSM4= +git.smarteching.com/goffee/core v1.9.0/go.mod h1:L9a+kL1RVHRHzp+DzCS1apwVLyZAvGE6B94UlyIMhIg= git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q= git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ= github.com/SparkPost/gosparkpost v0.2.0 h1:yzhHQT7cE+rqzd5tANNC74j+2x3lrPznqPJrxC1yR8s= @@ -31,11 +33,15 @@ github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQD github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= +github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= @@ -55,6 +61,8 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= @@ -75,6 +83,8 @@ github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8= github.com/mailgun/errors v0.4.0/go.mod h1:xGBaaKdEdQT0/FhwvoXv4oBaqqmVZz9P1XEnvD/onc0= github.com/mailgun/mailgun-go/v4 v4.17.3 h1:WoO48/VeXgAVSzjgzyeLvF08AoPzWU2EBz79INN8rEA= github.com/mailgun/mailgun-go/v4 v4.17.3/go.mod h1:0ood70bQR/SffQ9NxIsAY06H+HG0hrvMVApfUp9TihI= +github.com/mailgun/mailgun-go/v4 v4.22.1 h1:yMvPeo9m5XPVVg3XF0aPiJiiGt/n/cayBa4eQBDYqtc= +github.com/mailgun/mailgun-go/v4 v4.22.1/go.mod h1:JA2xbLTkEWrX2TO+RUiJQALZus6WLLoXym2i8a8F5sE= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= @@ -91,6 +101,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc= +github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= @@ -102,37 +114,56 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA= +golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= +golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= +golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -141,8 +172,12 @@ gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= From bbadb4e3f869d29839ebda4bdfa5f16a335f398d Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Mon, 24 Feb 2025 09:58:53 -0500 Subject: [PATCH 09/15] update gitignore --- .gitignore | 2 ++ README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 84146d5..90a928b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ tls/* !tls/.gitkeep .DS_Store storage/sqlite/* +.idea +cup diff --git a/README.md b/README.md index 7397849..4ae95df 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![gocondor logo](https://git.smarteching.com/avatars/cd7cd5b690adc8e5ec6d6cdb117f1bf5a9e9353dae111bfbb394d2c3d4497537?size=200) +![goffee logo](https://git.smarteching.com/avatars/cd7cd5b690adc8e5ec6d6cdb117f1bf5a9e9353dae111bfbb394d2c3d4497537?size=200) # Cup of Goffee ## What is Goffee? From 5aee1d1508753d0608cffd77c1b007bef01b63ca Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Mon, 24 Feb 2025 10:12:51 -0500 Subject: [PATCH 10/15] update core and tested on go1.24.0 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a7cfe99..b0f8a68 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ replace ( go 1.23.1 require ( - git.smarteching.com/goffee/core v1.9.0 + git.smarteching.com/goffee/core v1.9.1 github.com/google/uuid v1.6.0 github.com/hibiken/asynq v0.25.1 github.com/joho/godotenv v1.5.1 diff --git a/go.sum b/go.sum index c25cd73..d2b6fd3 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ git.smarteching.com/goffee/core v1.8.4 h1:XB+vpe7e8muiDChRVDaJ1TG7H+/FBxDQcMfWp4 git.smarteching.com/goffee/core v1.8.4/go.mod h1:JxXDvTQU2shKYY6c9aS3s6sFh7mEDzgmjzdc85HhBV8= git.smarteching.com/goffee/core v1.9.0 h1:aND9YkkDXEqsgEoDA04D9pmum2D3/nNljO1ojE1nSM4= git.smarteching.com/goffee/core v1.9.0/go.mod h1:L9a+kL1RVHRHzp+DzCS1apwVLyZAvGE6B94UlyIMhIg= +git.smarteching.com/goffee/core v1.9.1 h1:/WkIHkLJuNGf72tUJx7s/jt4gIA0EMkR1wemr+gnKg0= +git.smarteching.com/goffee/core v1.9.1/go.mod h1:L9a+kL1RVHRHzp+DzCS1apwVLyZAvGE6B94UlyIMhIg= git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q= git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ= github.com/SparkPost/gosparkpost v0.2.0 h1:yzhHQT7cE+rqzd5tANNC74j+2x3lrPznqPJrxC1yR8s= From 44228d87e8544d8c54ed42c2e60550840b8ab54b Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Wed, 19 Mar 2025 00:07:46 -0500 Subject: [PATCH 11/15] update core 1.9.2 and go1.24.1 --- go.mod | 18 +++++++++--------- go.sum | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index b0f8a68..4fc453a 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ replace ( go 1.23.1 require ( - git.smarteching.com/goffee/core v1.9.1 + git.smarteching.com/goffee/core v1.9.2 github.com/google/uuid v1.6.0 github.com/hibiken/asynq v0.25.1 github.com/joho/godotenv v1.5.1 @@ -39,7 +39,7 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailgun/errors v0.4.0 // indirect - github.com/mailgun/mailgun-go/v4 v4.22.1 // indirect + github.com/mailgun/mailgun-go/v4 v4.23.0 // indirect github.com/mattn/go-sqlite3 v1.14.24 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -50,13 +50,13 @@ require ( github.com/sendgrid/sendgrid-go v3.16.0+incompatible // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cast v1.7.1 // indirect - golang.org/x/crypto v0.34.0 // indirect - golang.org/x/image v0.24.0 // indirect - golang.org/x/net v0.35.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect - golang.org/x/time v0.10.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/image v0.25.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.11.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gorm.io/driver/mysql v1.5.7 // indirect gorm.io/driver/postgres v1.5.11 // indirect diff --git a/go.sum b/go.sum index d2b6fd3..2954a37 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ git.smarteching.com/goffee/core v1.9.0 h1:aND9YkkDXEqsgEoDA04D9pmum2D3/nNljO1ojE git.smarteching.com/goffee/core v1.9.0/go.mod h1:L9a+kL1RVHRHzp+DzCS1apwVLyZAvGE6B94UlyIMhIg= git.smarteching.com/goffee/core v1.9.1 h1:/WkIHkLJuNGf72tUJx7s/jt4gIA0EMkR1wemr+gnKg0= git.smarteching.com/goffee/core v1.9.1/go.mod h1:L9a+kL1RVHRHzp+DzCS1apwVLyZAvGE6B94UlyIMhIg= +git.smarteching.com/goffee/core v1.9.2 h1:SpLAhsTxssPItgkBYLN3UxMH5s+q8qVtbvmRom3WKh8= +git.smarteching.com/goffee/core v1.9.2/go.mod h1:L9a+kL1RVHRHzp+DzCS1apwVLyZAvGE6B94UlyIMhIg= git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q= git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ= github.com/SparkPost/gosparkpost v0.2.0 h1:yzhHQT7cE+rqzd5tANNC74j+2x3lrPznqPJrxC1yR8s= @@ -87,6 +89,8 @@ github.com/mailgun/mailgun-go/v4 v4.17.3 h1:WoO48/VeXgAVSzjgzyeLvF08AoPzWU2EBz79 github.com/mailgun/mailgun-go/v4 v4.17.3/go.mod h1:0ood70bQR/SffQ9NxIsAY06H+HG0hrvMVApfUp9TihI= github.com/mailgun/mailgun-go/v4 v4.22.1 h1:yMvPeo9m5XPVVg3XF0aPiJiiGt/n/cayBa4eQBDYqtc= github.com/mailgun/mailgun-go/v4 v4.22.1/go.mod h1:JA2xbLTkEWrX2TO+RUiJQALZus6WLLoXym2i8a8F5sE= +github.com/mailgun/mailgun-go/v4 v4.23.0 h1:jPEMJzzin2s7lvehcfv/0UkyBu18GvcURPr2+xtZRbk= +github.com/mailgun/mailgun-go/v4 v4.23.0/go.mod h1:imTtizoFtpfZqPqGP8vltVBB6q9yWcv6llBhfFeElZU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= @@ -130,19 +134,27 @@ golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/crypto v0.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA= golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= @@ -151,16 +163,22 @@ golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= From 368d6c8e6b526f6f19a603551852c8f80de1ed90 Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Wed, 16 Apr 2025 22:50:56 -0500 Subject: [PATCH 12/15] sample code for select multiple and custom attributes in form --- controllers/themedemo.go | 44 +++++++++++++++++++ .../templates/custom_theme_contentpage.html | 3 ++ 2 files changed, 47 insertions(+) diff --git a/controllers/themedemo.go b/controllers/themedemo.go index bbde3c6..696862b 100644 --- a/controllers/themedemo.go +++ b/controllers/themedemo.go @@ -646,8 +646,37 @@ func Themecontent(c *core.Context) *core.Response { ContentTable components.ContentTable ContentTabledetail components.ContentTabledetail ContentGraph components.ContentGraph + FieldText components.FormInput + FormSelectCityM components.FormSelect } + // for select options + var allOptions []components.FormSelectOption + var option components.FormSelectOption + option.Value = "ch" + option.Caption = "China" + allOptions = append(allOptions, option) + option.Value = "ba" + option.Caption = "Buenos Aires" + allOptions = append(allOptions, option) + option.Value = "fr" + option.Caption = "France" + selectedOption := option + allOptions = append(allOptions, option) + option.Value = "kr" + option.Caption = "Korea" + allOptions = append(allOptions, option) + + // for custom attributes in form + var customAtt []components.CustomAtt + var attribute components.CustomAtt + attribute.AttName = "code" + attribute.AttValue = "five" + customAtt = append(customAtt, attribute) + attribute.AttName = "mytag" + attribute.AttValue = "My value" + customAtt = append(customAtt, attribute) + // TABLES // for th head var allTh []components.ContentTableTH @@ -725,6 +754,21 @@ func Themecontent(c *core.Context) *core.Response { // now fill data of the components tmplData := templateData{ + FormSelectCityM: components.FormSelect{ + ID: "city", + Label: "Select city", + AllOptions: allOptions, + SelectedOption: selectedOption, + IsMultiple: true, + }, + FieldText: components.FormInput{ + ID: "text", + Label: "Name", + Type: "text", + Hint: "This is sample hint", + Placeholder: "Enter your name", + CustomAtt: customAtt, + }, ContentTable: components.ContentTable{ ID: "table_demo", AllTH: allTh, diff --git a/storage/templates/custom_theme_contentpage.html b/storage/templates/custom_theme_contentpage.html index 67b35f9..1024f56 100644 --- a/storage/templates/custom_theme_contentpage.html +++ b/storage/templates/custom_theme_contentpage.html @@ -12,6 +12,9 @@

Pie chart

{{template "content_graph" .ContentGraph}} +
+ {{template "form_input" .FieldText}} + {{template "form_select" .FormSelectCityM}}
{{template "content_tabledetail" .ContentTabledetail}} From 51d76c2a94471368122ed3cfbcf2cc0aa20b9d77 Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Thu, 17 Apr 2025 01:51:08 -0500 Subject: [PATCH 13/15] add sample element paginator --- controllers/themedemo.go | 78 +++++++++++++++++-- .../templates/custom_theme_contentpage.html | 3 + 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/controllers/themedemo.go b/controllers/themedemo.go index 696862b..03c55c1 100644 --- a/controllers/themedemo.go +++ b/controllers/themedemo.go @@ -643,11 +643,13 @@ func Themecontent(c *core.Context) *core.Response { // first, include all compoments type templateData struct { - ContentTable components.ContentTable - ContentTabledetail components.ContentTabledetail - ContentGraph components.ContentGraph - FieldText components.FormInput - FormSelectCityM components.FormSelect + ContentTable components.ContentTable + ContentTabledetail components.ContentTabledetail + ContentGraph components.ContentGraph + FieldText components.FormInput + FormSelectCityM components.FormSelect + Pagination components.ContentPagination + ShouldShowPagination bool } // for select options @@ -699,7 +701,7 @@ func Themecontent(c *core.Context) *core.Response { var allTd [][]components.ContentTableTD //var vals []components.ContentTableTD // rows - for i := 1; i <= 10; i++ { + for i := 1; i <= 28; i++ { vals := make([]components.ContentTableTD, len(allTh)) for b := 0; b < len(allTh)-2; b++ { vals[b].Value = fmt.Sprintf("%s%d%d", "TD data: ", i, b) @@ -718,6 +720,46 @@ func Themecontent(c *core.Context) *core.Response { allTd = append(allTd, vals) } + // Pagination demo + // start config + limit := 10 + shouldShowPagination := false + pageViewTableOffset := 0 + // Get the length of AllOptions + totalrecords := len(allTd) + // get current table offset + tpage := c.RequestParamExists("tpage") + if tpage { + pageViewTableOffset, _ = strconv.Atoi(c.GetRequestParam("tpage").(string)) + } + + // start default option paginator + var pagination components.ContentPagination + pagination.PageStartRecord = pageViewTableOffset + 1 + pagination.PageEndRecord = 0 + pagination.TotalRecords = 0 + pagination.PrevLink = "" + pagination.NextLink = "" + + // check current page + // fake function to emulate a query offset + newTd := getPaginatedPageViews(allTd, limit, pageViewTableOffset) + + if len(newTd) > 0 { + pagination.TotalRecords = totalrecords + pagination.PageStartRecord = pageViewTableOffset + 1 + pagination.PageEndRecord = pageViewTableOffset + len(newTd) + shouldShowPagination = totalrecords > limit + } + + if shouldShowPagination && pageViewTableOffset != 0 { + pagination.PrevLink = fmt.Sprintf("/themecontent?tpage=%d", pageViewTableOffset-limit) + } + + if shouldShowPagination && pageViewTableOffset+limit < totalrecords { + pagination.NextLink = fmt.Sprintf("/themecontent?tpage=%d", pageViewTableOffset+limit) + } + // for td items in table detail var allTdetail []components.ContentTabledetailTD // table detail @@ -772,7 +814,7 @@ func Themecontent(c *core.Context) *core.Response { ContentTable: components.ContentTable{ ID: "table_demo", AllTH: allTh, - AllTD: allTd, + AllTD: newTd, }, ContentTabledetail: components.ContentTabledetail{ ID: "table_demodetail", @@ -785,6 +827,8 @@ func Themecontent(c *core.Context) *core.Response { Labels: "Berlin|Paris|Venecia", Values: valuesgraph, }, + Pagination: pagination, + ShouldShowPagination: shouldShowPagination, } return c.Response.Template("custom_theme_contentpage.html", tmplData) @@ -797,3 +841,23 @@ func Themecontent(c *core.Context) *core.Response { } } + +func getPaginatedPageViews(values [][]components.ContentTableTD, limit int, offset int) [][]components.ContentTableTD { + + // Validate the offset and adjust if necessary + if offset < 0 { + offset = 0 // Ensure offset is not negative + } else if offset >= len(values) { + var emptytd [][]components.ContentTableTD + return emptytd + } + + // Calculate the end index (limit the slice to the size of the array) + end := offset + limit + if end > len(values) { + end = len(values) // Ensure end doesn't exceed the length of the array + } + + return values[offset:end] // Slice the array + +} diff --git a/storage/templates/custom_theme_contentpage.html b/storage/templates/custom_theme_contentpage.html index 1024f56..422c253 100644 --- a/storage/templates/custom_theme_contentpage.html +++ b/storage/templates/custom_theme_contentpage.html @@ -7,6 +7,9 @@ Content demos
{{template "content_table" .ContentTable}} + {{if .ShouldShowPagination}} + {{template "content_pagination" .Pagination}} + {{end}}
From ffd3c89c1e4ccf1b8b27038f105fda5e0de68b9a Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Tue, 15 Jul 2025 11:27:52 -0500 Subject: [PATCH 14/15] add folders to cdn --- go.mod | 4 +++- go.sum | 24 ++++++++++++++++++------ storage/app/img/goffee.png | Bin 0 -> 25928 bytes storage/app/index.html | 9 +++++++++ 4 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 storage/app/img/goffee.png create mode 100644 storage/app/index.html diff --git a/go.mod b/go.mod index 8b0cfa0..a72bbeb 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ replace ( git.smarteching.com/goffee/cup/models => ./models ) -go 1.23.1 +go 1.24.1 require ( git.smarteching.com/goffee/core v1.9.0 @@ -21,10 +21,12 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect git.smarteching.com/zeni/go-chart/v2 v2.1.4 // indirect + git.smarteching.com/zeni/go-charts/v2 v2.6.11 // indirect github.com/SparkPost/gosparkpost v0.2.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect diff --git a/go.sum b/go.sum index ca95d88..95137c3 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -git.smarteching.com/goffee/core v1.8.4 h1:XB+vpe7e8muiDChRVDaJ1TG7H+/FBxDQcMfWp4zloPs= -git.smarteching.com/goffee/core v1.8.4/go.mod h1:JxXDvTQU2shKYY6c9aS3s6sFh7mEDzgmjzdc85HhBV8= git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q= git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ= +git.smarteching.com/zeni/go-charts/v2 v2.6.11 h1:9udzlv3uxGXszpplfkL5IaTUrgkNj++KwhbaN1vVEqI= +git.smarteching.com/zeni/go-charts/v2 v2.6.11/go.mod h1:3OpRPSXg7Qx4zcgsmwsC9ZFB9/wAkGSbnXf1wIbHYCg= github.com/SparkPost/gosparkpost v0.2.0 h1:yzhHQT7cE+rqzd5tANNC74j+2x3lrPznqPJrxC1yR8s= github.com/SparkPost/gosparkpost v0.2.0/go.mod h1:S9WKcGeou7cbPpx0kTIgo8Q69WZvUmVeVzbD+djalJ4= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -23,12 +23,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= @@ -42,6 +46,8 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -69,6 +75,10 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8= @@ -93,6 +103,8 @@ github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= @@ -106,8 +118,10 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02n github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= @@ -120,8 +134,6 @@ golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/storage/app/img/goffee.png b/storage/app/img/goffee.png new file mode 100644 index 0000000000000000000000000000000000000000..d0491fce12ada6be01434cd41b956a32d65d901a GIT binary patch literal 25928 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 literal 0 HcmV?d00001 diff --git a/storage/app/index.html b/storage/app/index.html new file mode 100644 index 0000000..326fcb0 --- /dev/null +++ b/storage/app/index.html @@ -0,0 +1,9 @@ + + + + Title! + + +

This is an example conten.

+ + From 2a3d5486e9a4e4d204b62412902985604221f714 Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Tue, 15 Jul 2025 11:31:16 -0500 Subject: [PATCH 15/15] add env vars --- .env-dev | 4 +++- .env-example | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.env-dev b/.env-dev index ded2eab..c88f67a 100644 --- a/.env-dev +++ b/.env-dev @@ -8,7 +8,7 @@ App_HTTP_HOST=localhost App_HTTP_PORT=8080 App_USE_HTTPS=false App_USE_LETSENCRYPT=false -App_USE_CORESERVICES=false +App_USE_CORESERVICES=true APP_LETSENCRYPT_EMAIL=mail@example.com App_HTTPS_HOSTS=example.com, www.example.com App_REDIRECT_HTTP_TO_HTTPS=false @@ -19,6 +19,8 @@ App_KEY_FILE_PATH=tls/server.key ###### TEMPLATES ###### ####################################### TEMPLATE_ENABLE=true +APP_ENABLE=true +CDNEnable=false COOKIE_SECRET=13d6b4dff8f84a10851021ec8608f814570d562c92fe6b5ec4c9f595bcb3234b ####################################### diff --git a/.env-example b/.env-example index ded2eab..d0780b5 100644 --- a/.env-example +++ b/.env-example @@ -20,6 +20,9 @@ App_KEY_FILE_PATH=tls/server.key ####################################### TEMPLATE_ENABLE=true COOKIE_SECRET=13d6b4dff8f84a10851021ec8608f814570d562c92fe6b5ec4c9f595bcb3234b +APP_ENABLE=true +CDNEnable=false + ####################################### ###### JWT ######