diff --git a/.env-dev b/.env-dev index ded2eab..800ca98 100644 --- a/.env-dev +++ b/.env-dev @@ -8,17 +8,21 @@ 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 App_CERT_FILE_PATH=tls/server.crt App_KEY_FILE_PATH=tls/server.key +LOG_STDOUT_ENABLE=true +LOG_LEVEL=debug ####################################### ###### 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 ###### 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? 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/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/controllers/sample.go b/controllers/sample.go deleted file mode 100644 index 9f5687b..0000000 --- a/controllers/sample.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (c) 2024 Zeni Kim -// Use of this source code is governed by MIT-style -// license that can be found in the LICENSE file. - -package controllers - -import ( - "fmt" - - "git.smarteching.com/goffee/core" - "git.smarteching.com/goffee/core/template/components" - "git.smarteching.com/goffee/cup/utils" -) - -// Show basic template -func Sample(c *core.Context) *core.Response { - - // first, include all compoments - type templateData struct { - PageCard components.PageCard - } - - // now fill data of the components - tmplData := templateData{ - PageCard: components.PageCard{ - CardTitle: "Framework Goffee", - CardBody: "Powered by Golang", - }, - } - - return c.Response.Template("basic.html", tmplData) - -} - -// Show basic app login -func AppLogin(c *core.Context) *core.Response { - - // first, include all compoments - // first, include all compoments - type templateData struct { - PageCard components.PageCard - } - - // now fill data of the components - tmplData := templateData{ - PageCard: components.PageCard{ - CardTitle: "Card title", - CardBody: "Loerm ipsum at deim", - }, - } - return c.Response.Template("login.html", tmplData) -} - -// Show basic app login -func AppSession(c *core.Context) *core.Response { - - var session = new(utils.SessionUser) - - // true if session is active - hassession := session.Init(c) - - //session.Set("numberdos", 66) - - type templateData struct { - PageCard components.PageCard - } - - // now fill data of the components - tmplData := templateData{} - - if hassession { - - sesiondata := "" - cardtitle := fmt.Sprintf("Session user id: %v", session.GetUserID()) - numberdos, ok := session.Get("numberdos") - - if numberdos != nil { - numberdos = numberdos.(float64) + 10 - } else { - numberdos = 10 - } - - session.Set("numberdos", numberdos) - - if ok { - sesiondata = fmt.Sprintf("OK, Session numberdos has %v", numberdos) - } else { - sesiondata = fmt.Sprintf("No ok, session numberdos has %v", numberdos) - } - - // delete single - //session.Delete("numberdos") - - // delete all data - //session.Flush() - - tmplData = templateData{ - PageCard: components.PageCard{ - CardTitle: cardtitle, - CardBody: sesiondata, - }, - } - - return c.Response.Template("appsession.html", tmplData) - - } else { - - return c.Response.Template("login.html", tmplData) - - } - -} - -// Show basic app sample -func AppSample(c *core.Context) *core.Response { - - // first, include all compoments - type templateData struct { - PageCard components.PageCard - ContentDropdown components.ContentDropdown - } - - // now fill data of the components - tmplData := templateData{ - PageCard: components.PageCard{ - CardTitle: "Protected page", - CardBody: "If you can see this page, your are loggedin", - }, - ContentDropdown: components.ContentDropdown{ - Label: "dropdown", - Items: []components.ContentDropdownItem{ - { - Text: "Signout", - Link: "#", - ID: "signout", - }, - { - Text: "item disabled", - Link: "#", - IsDisabled: true, - }, - }, - }, - } - //fmt.Printf("Outside cookie user is: %s", user.Email) - - return c.Response.Template("app.html", tmplData) - -} diff --git a/controllers/themedemo.go b/controllers/themedemo.go deleted file mode 100644 index bbde3c6..0000000 --- a/controllers/themedemo.go +++ /dev/null @@ -1,755 +0,0 @@ -// Copyright (c) 2024 Zeni Kim -// Use of this source code is governed by MIT-style -// license that can be found in the LICENSE file. - -package controllers - -import ( - "fmt" - "math/rand/v2" - "os" - "strconv" - - "git.smarteching.com/goffee/core" - "git.smarteching.com/goffee/core/template/components" -) - -// Show home page -func Themedemo(c *core.Context) *core.Response { - - // check if template engine is enabled - TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE") - if TemplateEnableStr == "" { - TemplateEnableStr = "false" - } - TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr) - - if TemplateEnable { - - // first, include all compoments - type templateData struct { - PageCard components.PageCard - } - - // now fill data of the components - tmplData := templateData{ - PageCard: components.PageCard{ - CardTitle: "Card title", - CardBody: "Loerm ipsum at deim", - }, - } - - return c.Response.Template("custom_theme_base.html", tmplData) - - } else { - - message := "{\"message\": \"Error, template not enabled\"}" - return c.Response.Json(message) - - } - -} - -// Show form element page -func Themeform(c *core.Context) *core.Response { - - // check if template engine is enabled - TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE") - if TemplateEnableStr == "" { - TemplateEnableStr = "false" - } - TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr) - - if TemplateEnable { - - // first, include all compoments - type templateData struct { - FormText components.FormInput - FormEmail components.FormInput - FormButton components.FormButton - FormSelectCity components.FormSelect - FormTextarea components.FormTextarea - FormRadio components.FormRadio - FormCheckbox components.FormCheckbox - } - - // 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) - - // for radio options - var allOptionsr []components.FormRadioItem - var optionr components.FormRadioItem - optionr.ID = "citysch" - optionr.Name = "citys" - optionr.Value = "china" - optionr.Label = "China" - allOptionsr = append(allOptionsr, optionr) - optionr.ID = "citysba" - optionr.Name = "citys" - optionr.Value = "buenosaires" - optionr.Label = "Buenos Aires" - //optionr.IsDisabled = true - allOptionsr = append(allOptionsr, optionr) - - // for radio options - var allOptionsc []components.FormCheckboxItem - var optionc components.FormCheckboxItem - optionc.ID = "citysch" - optionc.Name = "citys" - optionc.Value = "china" - optionc.Label = "China" - allOptionsc = append(allOptionsc, optionc) - optionc.ID = "citysba" - optionc.Name = "citys" - optionc.Value = "buenosaires" - optionc.Label = "Buenos Aires" - allOptionsc = append(allOptionsc, optionc) - optionc.ID = "london" - optionc.Name = "london" - optionc.Value = "london" - optionc.Label = "London" - //optionc.IsChecked = true - allOptionsc = append(allOptionsc, optionc) - - // now fill data of the components - tmplData := templateData{ - FormText: components.FormInput{ - ID: "text", - Label: "Name", - Type: "text", - Hint: "This is sample hint", - Placeholder: "Enter your name", - }, - FormEmail: components.FormInput{ - ID: "email", - Label: "Email", - Type: "email", - IsRequired: true, - Placeholder: "Enter your email address", - }, - FormButton: components.FormButton{ - Text: "Login", - IsSubmit: true, - TypeClass: "primary", - }, - FormSelectCity: components.FormSelect{ - ID: "city", - Label: "Select city", - AllOptions: allOptions, - SelectedOption: selectedOption, - }, - FormTextarea: components.FormTextarea{ - ID: "text", - Label: "Example textarea", - }, - FormRadio: components.FormRadio{ - Label: "Radio buttons", - AllRadios: allOptionsr, - }, - FormCheckbox: components.FormCheckbox{ - Label: "Checkbox options", - AllCheckbox: allOptionsc, - }, - } - return c.Response.Template("custom_theme_formpage.html", tmplData) - - } else { - - message := "{\"message\": \"Error, template not enabled\"}" - return c.Response.Json(message) - - } - -} - -func ThemeElements(c *core.Context) *core.Response { - // check if template engine is enabled - TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE") - if TemplateEnableStr == "" { - TemplateEnableStr = "false" - } - TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr) - - if TemplateEnable { - type templateData struct { - Buttons []components.FormButton - Hrefs []components.ContentHref - Badges []components.ContentBadge - Dropdowns []components.ContentDropdown - Lists []components.ContentList - Menus []components.PageNav - } - buttons := []components.FormButton{ - { - Text: "primary", - TypeClass: "primary", - }, - { - Text: "secondary", - TypeClass: "secondary", - }, - { - Text: "success", - TypeClass: "success", - }, - { - Text: "danger", - TypeClass: "danger", - }, - { - Text: "warning", - TypeClass: "warning", - }, - { - Text: "info", - TypeClass: "info", - }, - { - Text: "light", - TypeClass: "light", - }, - { - Text: "dark", - TypeClass: "dark", - }, - { - Text: "link", - TypeClass: "link", - }, - { - Text: "disabled", - TypeClass: "primary", - IsDisabled: true, - }, - { - Text: "outline-primary", - TypeClass: "outline-primary", - }, - { - Text: "outline-secondary", - TypeClass: "outline-secondary", - }, - { - Text: "outline-success", - TypeClass: "outline-success", - }, - { - Text: "outline-danger", - TypeClass: "outline-danger", - }, - { - Text: "outline-warning", - TypeClass: "outline-warning", - }, - { - Text: "outline-info", - TypeClass: "outline-info", - }, - { - Text: "outline-light", - TypeClass: "outline-light", - }, - { - Text: "outline-dark", - TypeClass: "outline-dark", - }, - } - hrefs := []components.ContentHref{ - { - Text: "href", - Link: "#", - IsButton: false, - }, - { - Text: "link", - Link: "#", - IsButton: false, - TypeClass: "link", - }, - { - Text: "button", - Link: "#", - IsButton: true, - TypeClass: "primary", - }, - { - Text: "href disabled", - Link: "#", - IsButton: false, - IsDisabled: true, - }, - { - Text: "link disabled", - Link: "#", - TypeClass: "link", - IsDisabled: true, - }, - { - Text: "button disabled", - Link: "#", - IsButton: true, - TypeClass: "primary", - IsDisabled: true, - }, - } - badges := []components.ContentBadge{ - { - Text: "primary", - TypeClass: "primary", - }, - { - Text: "secondary", - TypeClass: "secondary", - }, - { - Text: "success", - TypeClass: "success", - }, - { - Text: "danger", - TypeClass: "danger", - }, - { - Text: "warning", - TypeClass: "warning", - }, - { - Text: "info", - TypeClass: "info", - }, - { - Text: "light", - TypeClass: "light", - }, - { - Text: "dark", - TypeClass: "dark", - }, - { - Text: "outline", - TypeClass: "primary", - IsOutline: true, - }, - { - Text: "outline", - TypeClass: "success", - IsOutline: true, - }, - { - Text: "outline", - TypeClass: "danger", - IsOutline: true, - }, - { - Text: "outline", - TypeClass: "warning", - IsOutline: true, - }, - } - dropdowns := []components.ContentDropdown{ - // dropdown - { - Label: "dropdown", - Items: []components.ContentDropdownItem{ - { - Text: "item ", - Link: "#", - }, - { - Text: "item disabled", - Link: "#", - IsDisabled: true, - }, - }, - }, - // dropdown - { - Label: "primary", - TypeClass: "primary", - Items: []components.ContentDropdownItem{ - { - Text: "item ", - Link: "#", - }, - { - Text: "item ", - Link: "#", - IsActive: true, - }, - { - Text: "item disabled", - Link: "#", - IsDisabled: true, - }, - }, - }, - // dropdown - { - Label: "outline", - TypeClass: "outline-primary", - Items: []components.ContentDropdownItem{ - { - Text: "item ", - Link: "#", - }, - }, - }, - // dropdown - { - Label: "disabled", - TypeClass: "primary", - IsDisabled: true, - // items - }, - } - list := []components.ContentList{ - // basic list - { - Items: []components.ContentListItem{ - { - Text: "item 1", - }, - { - Text: "item 2", - EndElement: "end text", - }, - { - Text: "item disabled", - IsDisabled: true, - }, - }, - }, - // description list - { - Items: []components.ContentListItem{ - { - Text: "item 1", - Description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", - }, - { - Text: "item 2", - Description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - }, - { - Text: "item disabled", - Description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - IsDisabled: true, - }, - }, - }, - // list with class - { - Items: []components.ContentListItem{ - { - Text: "class primary", - TypeClass: "primary", - }, - { - Text: "class success", - TypeClass: "success", - }, - { - Text: "class danger", - TypeClass: "danger", - }, - }, - }, - } - menus := []components.PageNav{ - // nav - { - NavClass: "nav-pills", - NavItems: []components.PageNavItem{ - { - Text: "item active", - Link: "#", - IsActive: true, - }, - { - Text: "item", - Link: "#", - IsActive: false, - }, - { - Text: "item", - Link: "#", - IsActive: false, - ChildItems: []components.PageNavItem{ - { - Text: "item ", - Link: "#", - }, - { - Text: "item disabled", - Link: "#", - IsDisabled: true, - }, - }, - }, - { - Text: "item disabled", - Link: "#", - IsDisabled: true, - }, - }, - }, - // nav - { - NavClass: "", - NavItems: []components.PageNavItem{ - { - Text: "item active", - Link: "#", - IsActive: true, - }, - { - Text: "item", - Link: "#", - IsActive: false, - }, - { - Text: "item", - Link: "#", - IsActive: false, - }, - { - Text: "item disabled", - Link: "#", - IsDisabled: true, - }, - }, - }, - // nav underline - { - NavClass: "nav-underline", - NavItems: []components.PageNavItem{ - { - Text: "item active", - Link: "#", - IsActive: true, - }, - { - Text: "item", - Link: "#", - IsActive: false, - }, - { - Text: "item", - Link: "#", - IsActive: false, - }, - { - Text: "item disabled", - Link: "#", - IsDisabled: true, - }, - }, - }, - // nav tabs - { - NavClass: "", - IsTab: true, - NavItems: []components.PageNavItem{ - { - Text: "tab active", - Link: "#", - IsActive: true, - }, - { - Text: "tab", - Link: "#", - IsActive: false, - }, - { - Text: "tab", - Link: "#", - IsActive: false, - }, - { - Text: "tab disabled", - Link: "#", - IsDisabled: true, - }, - }, - }, - // nav vertical - { - NavClass: "", - IsVertical: true, - NavItems: []components.PageNavItem{ - { - Text: "item active", - Link: "#", - IsActive: true, - }, - { - Text: "item", - Link: "#", - IsActive: false, - }, - { - Text: "item", - Link: "#", - IsActive: false, - }, - { - Text: "item disabled", - Link: "#", - IsDisabled: true, - }, - }, - }, - } - - tmplData := templateData{ - Buttons: buttons, - Hrefs: hrefs, - Badges: badges, - Dropdowns: dropdowns, - Lists: list, - Menus: menus, - } - return c.Response.Template("custom_theme_elements.html", tmplData) - - } else { - - message := "{\"message\": \"Error, template not enabled\"}" - return c.Response.Json(message) - } -} - -// Show form element page -func Themecontent(c *core.Context) *core.Response { - - // check if template engine is enabled - TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE") - if TemplateEnableStr == "" { - TemplateEnableStr = "false" - } - TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr) - - if TemplateEnable { - - // first, include all compoments - type templateData struct { - ContentTable components.ContentTable - ContentTabledetail components.ContentTabledetail - ContentGraph components.ContentGraph - } - - // TABLES - // for th head - var allTh []components.ContentTableTH - var th components.ContentTableTH - th.Value = "Column heading 1" - allTh = append(allTh, th) - th.Value = "Column heading 2" - allTh = append(allTh, th) - th.ID = "ba" - th.Value = "Column heading 3" - allTh = append(allTh, th) - th.Value = "Column badge" - th.ValueType = "badge" // column type badge - allTh = append(allTh, th) - th.Value = "Column action" - th.ValueType = "href" // column type href - allTh = append(allTh, th) - - // for td items - var allTd [][]components.ContentTableTD - //var vals []components.ContentTableTD - // rows - for i := 1; i <= 10; 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) - vals[b].ID = fmt.Sprintf("%s%d%d", "idtd_", i, b) - } - // column badge - vals[len(allTh)-2].Value = components.ContentBadge{ - Text: "success", - TypeClass: "success", - } - // last column href - vals[len(allTh)-1].Value = components.ContentHref{ - Text: "edit", - Link: "#", - } - allTd = append(allTd, vals) - } - - // for td items in table detail - var allTdetail []components.ContentTabledetailTD - // table detail - var thd components.ContentTabledetailTD - thd.Caption = "Continent" - thd.Value = "Asia" - allTdetail = append(allTdetail, thd) - thd.Caption = "Country" - thd.Value = "South Korea" - allTdetail = append(allTdetail, thd) - thd.Caption = "Capital" - thd.Value = "Seoul" - allTdetail = append(allTdetail, thd) - thd.Caption = "Details" - thd.ValueType = "href" // column type href - thd.Value = components.ContentHref{ - Text: "edit", - Link: "#", - } - allTdetail = append(allTdetail, thd) - thd.Caption = "Notifications" - thd.ValueType = "badge" // column type href - thd.Value = components.ContentBadge{ - Text: "success", - TypeClass: "success", - } - allTdetail = append(allTdetail, thd) - - // random values for pie - one := rand.IntN(50) - two := rand.IntN(50) - three := rand.IntN(50) - valuesgraph := fmt.Sprintf("%d|%d|%d", one, two, three) - - // now fill data of the components - tmplData := templateData{ - ContentTable: components.ContentTable{ - ID: "table_demo", - AllTH: allTh, - AllTD: allTd, - }, - ContentTabledetail: components.ContentTabledetail{ - ID: "table_demodetail", - Title: "Sample table detail", - HeadClass: "table-warning", - AllTD: allTdetail, - }, - ContentGraph: components.ContentGraph{ - Graph: "pie", - Labels: "Berlin|Paris|Venecia", - Values: valuesgraph, - }, - } - - return c.Response.Template("custom_theme_contentpage.html", tmplData) - - } else { - - message := "{\"message\": \"Error, template not enabled\"}" - return c.Response.Json(message) - - } - -} diff --git a/go.mod b/go.mod index e70b070..21103fe 100644 --- a/go.mod +++ b/go.mod @@ -7,52 +7,59 @@ replace ( git.smarteching.com/goffee/cup/models => ./models ) -go 1.23.1 +go 1.25.0 require ( - git.smarteching.com/goffee/core v1.8.4 + git.smarteching.com/goffee/core v1.9.6 github.com/google/uuid v1.6.0 + github.com/hibiken/asynq v0.26.0 github.com/joho/godotenv v1.5.1 github.com/julienschmidt/httprouter v1.3.0 - gorm.io/gorm v1.25.12 + gorm.io/gorm v1.31.1 ) require ( - filippo.io/edwards25519 v1.1.0 // indirect + filippo.io/edwards25519 v1.2.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/go-chi/chi/v5 v5.1.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect - github.com/go-sql-driver/mysql v1.8.1 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/go-sql-driver/mysql v1.10.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.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.9.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/mattn/go-sqlite3 v1.14.24 // indirect + github.com/mailgun/errors v0.5.0 // indirect + github.com/mailgun/mailgun-go/v4 v4.23.0 // indirect + github.com/mattn/go-sqlite3 v1.14.44 // 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.19.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 - 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/text v0.19.0 // 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 + github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect + github.com/spf13/cast v1.10.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/image v0.39.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gorm.io/driver/mysql v1.6.0 // indirect + gorm.io/driver/postgres v1.6.0 // indirect + gorm.io/driver/sqlite v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index be301b7..2bf5036 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,11 @@ -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= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +git.smarteching.com/goffee/core v1.9.6 h1:GY1EXqbmBEWZAVrl3q22Izb6aXhQzFVQBv2hWhK/So8= +git.smarteching.com/goffee/core v1.9.6/go.mod h1:ifiBgTOR4zCMzdGsabNrEO792EHny2o149NGe3TSlms= 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= @@ -21,38 +23,37 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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/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/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/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.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= 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.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= 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= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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= 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.26.0 h1:1Zxr92MlDnb1Zt/QR5g2vSCqUS03i95lUfqx5X7/wrw= +github.com/hibiken/asynq v0.26.0/go.mod h1:Qk4e57bTnWDoyJ67VkchuV6VzSM9IQW2nPvAGuDyw58= 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= 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.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= 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= @@ -67,15 +68,21 @@ 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/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +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= -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/errors v0.5.0 h1:pLQo8uhAdORsjN69mGixSr0pGs46z/BW/FQXd8HG1VM= +github.com/mailgun/errors v0.5.0/go.mod h1:+2nrgY77E0vDkG4ErehpcpbSkMLkseJzKbrva89WeSs= +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= +github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= +github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -87,50 +94,64 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k= +github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= +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= -github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBUpXyTcTXy/Rl9N9aZ/Qw= -github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= +github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1GjJohAA0p6hVEaDtHWWs= +github.com/sendgrid/sendgrid-go v3.16.1+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.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +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= -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/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= +golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= 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/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -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/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= -gorm.io/driver/sqlite v1.5.6/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= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/main.go b/main.go index 4b9c956..5fac06e 100644 --- a/main.go +++ b/main.go @@ -62,6 +62,9 @@ func main() { registerGlobalHooks() registerRoutes() registerEvents() + if config.GetQueueConfig().EnableQueue == true { + registerQueues() + } if config.GetGormConfig().EnableGorm == true { RunAutoMigrations() } 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..1dfcc18 --- /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 uint // The user id + RoleID uint // The role id +} + +// TableName sets the table name +func (UserRole) TableName() string { + return "user_roles" +} 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..193da2d 100644 --- a/routes.go +++ b/routes.go @@ -8,7 +8,6 @@ package main import ( "git.smarteching.com/goffee/core" "git.smarteching.com/goffee/cup/controllers" - "git.smarteching.com/goffee/cup/hooks" ) // Register the app controllers @@ -20,14 +19,8 @@ func registerRoutes() { // Define your routes here... controller.Get("/", controllers.WelcomeHome) - // Uncomment the lines below to enable theme demo - controller.Get("/themebase", controllers.Themedemo) - controller.Get("/themeform", controllers.Themeform) - controller.Get("/themecontent", controllers.Themecontent) - controller.Get("/themepanel", controllers.Themedemo) - controller.Get("/themeelements", controllers.ThemeElements) - - // Uncomment the lines below to enable authentication + + // Uncomment the lines below to enable authentication API controller.Post("/signup", controllers.Signup) controller.Post("/signin", controllers.Signin) controller.Post("/signout", controllers.Signout) @@ -43,20 +36,6 @@ func registerRoutes() { controller.Post("/admin/users/edit/:id", controllers.AdminUsersEdit) controller.Post("/admin/users/delete", controllers.AdminUsersDelete) controller.Post("/admin/users/deleteconfirm", controllers.AdminUsersDelConfirm) - //controller.Get("/admin/users/roles", controllers.Signout) - //controller.Get("/admin/users/permissions", controllers.ResetPasswordRequest) - controller.Get("/dashboard", controllers.WelcomeToDashboard, hooks.AuthCheck) - // templates demos - controller.Get("/signout", controllers.Signout) - - controller.Get("/appsample", controllers.AppSample, hooks.AuthCheck) - controller.Post("/appsample", controllers.AppSample, hooks.AuthCheck) - - controller.Get("/applogin", controllers.AppLogin, hooks.CheckSessionCookie) - controller.Post("/applogin", controllers.AppLogin, hooks.CheckSessionCookie) - - controller.Get("/appsession", controllers.AppSession) - controller.Post("/appsession", controllers.AppSession) } diff --git a/run-auto-migrations.go b/run-auto-migrations.go index 8410637..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,5 +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/storage/app/img/goffee.png b/storage/app/img/goffee.png new file mode 100644 index 0000000..d0491fc Binary files /dev/null and b/storage/app/img/goffee.png differ 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.

+ + diff --git a/storage/sqlite/sqlite.db b/storage/sqlite/sqlite.db index 97751be..57a82d2 100644 Binary files a/storage/sqlite/sqlite.db and b/storage/sqlite/sqlite.db differ 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/app.html b/storage/templates/app.html deleted file mode 100644 index 2bc5f93..0000000 --- a/storage/templates/app.html +++ /dev/null @@ -1,17 +0,0 @@ - - - {{template "page_head" "Sample page"}} - -
-
- {{template "content_dropdown" .ContentDropdown}} - {{template "page_card" .PageCard}} - {{ define "page_card_content" }} - - - {{ end }} -
-
- {{template "page_footer"}} - - \ No newline at end of file diff --git a/storage/templates/custom_theme_base.html b/storage/templates/custom_theme_base.html deleted file mode 100644 index 9c28a1e..0000000 --- a/storage/templates/custom_theme_base.html +++ /dev/null @@ -1,10 +0,0 @@ - - - {{template "page_head" "Goffee"}} - -
- {{template "page_card" .PageCard}} -
- {{template "page_footer"}} - - \ No newline at end of file diff --git a/storage/templates/custom_theme_contentpage.html b/storage/templates/custom_theme_contentpage.html deleted file mode 100644 index 67b35f9..0000000 --- a/storage/templates/custom_theme_contentpage.html +++ /dev/null @@ -1,24 +0,0 @@ - - - {{template "page_head" "Goffee"}} - -
-
- Content demos -
- {{template "content_table" .ContentTable}} -
-
-
-

Pie chart

- {{template "content_graph" .ContentGraph}} -
-
- {{template "content_tabledetail" .ContentTabledetail}} -
-
-
-
- {{template "page_footer"}} - - diff --git a/storage/templates/custom_theme_elements.html b/storage/templates/custom_theme_elements.html deleted file mode 100644 index fbe2d52..0000000 --- a/storage/templates/custom_theme_elements.html +++ /dev/null @@ -1,66 +0,0 @@ - - - {{template "page_head" "Goffee"}} - -
-
- Demos buttons -
- {{range .Buttons}} - {{template "form_button" .}} - {{end}} -
-
- -
- Demos href -
- {{range .Hrefs}} - {{template "content_href" .}} - {{end}} -
-
- -
- Demos Badges -
- {{range .Badges}} - {{template "content_badge" .}} - {{end}} -
-
- - -
- Demos dropdown -
- {{range .Dropdowns}} - {{template "content_dropdown" .}} - {{end}} -
-
- - -
- Demos List -
- {{range .Lists}} - {{template "content_list" .}} - {{end}} -
-
- -
- Demos nav -
- {{range .Menus}} -
- {{template "page_nav" .}} -
- {{end}} -
-
-
- {{template "page_footer"}} - - diff --git a/storage/templates/custom_theme_formpage.html b/storage/templates/custom_theme_formpage.html deleted file mode 100644 index f45a558..0000000 --- a/storage/templates/custom_theme_formpage.html +++ /dev/null @@ -1,23 +0,0 @@ - - - {{template "page_head" "Goffee"}} - -
- -
- form demos -
- {{template "form_input" .FormText}} - {{template "form_input" .FormEmail}} - {{template "form_select" .FormSelectCity}} - {{template "form_textarea" .FormTextarea}} - {{template "form_radio" .FormRadio}} - {{template "form_checkbox" .FormCheckbox}} -
-
- {{template "form_button" .FormButton}} - -
- {{template "page_footer"}} - - diff --git a/storage/templates/appsession.html b/storage/templates/nopermission.html similarity index 57% rename from storage/templates/appsession.html rename to storage/templates/nopermission.html index 32364f0..bec8f39 100644 --- a/storage/templates/appsession.html +++ b/storage/templates/nopermission.html @@ -1,10 +1,10 @@ - {{template "page_head" "Sample page test session vars"}} + {{template "page_head" "No permission"}}
- {{template "page_card" .PageCard}} + You do not have permission to visit this page.
{{template "page_footer"}} diff --git a/utils/authority.go b/utils/authority.go new file mode 100644 index 0000000..823f845 --- /dev/null +++ b/utils/authority.go @@ -0,0 +1,436 @@ +// 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 uint, roleSlug string) error { + 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 = ?", userID).Where("role_id = ?", role.ID).First(&userRole) + if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { + c.GetGorm().Create(&models.UserRole{UserID: userID, 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 uint, roleSlug 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 + } + + // check if the role is a assigned + var userRole models.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 + } + return false, res.Error + } + + return true, nil +} + +// Checks if a permission is assigned to a user +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 = ?", userID).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 +} + +// 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 + 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 uint) ([]models.Role, error) { + var userRoles []models.UserRole + res := c.GetGorm().Where("user_id = ?", userID).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 +} 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) 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 +}