From 73f7fe63533972add0f52e250664cd6478e87ee2 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Fri, 25 Nov 2022 05:25:39 +0400 Subject: [PATCH 01/92] dAPI auth and OpenAPI schema for it --- auth.go | 8 +- d_api.go | 1 - d_api_auth.go | 3 +- d_api_change_password.go | 48 +++ d_api_login.go | 16 +- d_api_logout.go | 12 +- d_api_reset_password.go | 162 +++++++++- d_api_signup.go | 78 +++++ forgot_password_handler.go | 40 ++- forgot_password_handler_test.go | 4 +- global.go | 38 ++- login_handler.go | 2 +- openapi.go | 190 ++++++++++-- openapi/auth_paths.go | 449 ++++++++++++++++++++++++++++ openapi/generate_schema.go | 26 +- openapi/schema_object.go | 3 +- password_reset_handler.go | 9 + templates/uadmin/default/trail.html | 5 +- user.go | 15 +- 19 files changed, 1034 insertions(+), 75 deletions(-) create mode 100644 openapi/auth_paths.go diff --git a/auth.go b/auth.go index 8388c199..ed958d31 100644 --- a/auth.go +++ b/auth.go @@ -228,7 +228,7 @@ func getSessionFromRequest(r *http.Request) *Session { key := getSession(r) s := getSessionByKey(key) - if s.ID != 0 { + if s != nil && s.ID != 0 { return s } return nil @@ -734,6 +734,12 @@ func GetRemoteIP(r *http.Request) string { return r.RemoteAddr } +func verifyPassword(hash string, plain string) error { + password := []byte(plain + Salt) + hashedPassword := []byte(hash) + return bcrypt.CompareHashAndPassword(hashedPassword, password) +} + // sanitizeFileName is a function to sanitize file names to pretect // from path traversal attacks using ../ func sanitizeFileName(v string) string { diff --git a/d_api.go b/d_api.go index d7b9883d..3ab72570 100644 --- a/d_api.go +++ b/d_api.go @@ -117,7 +117,6 @@ func dAPIHandler(w http.ResponseWriter, r *http.Request, s *Session) { r.URL.Path = strings.TrimPrefix(r.URL.Path, "/") urlParts := strings.Split(r.URL.Path, "/") - Trail(DEBUG, "%#v", urlParts) ctx := context.WithValue(r.Context(), CKey("dAPI"), true) r = r.WithContext(ctx) diff --git a/d_api_auth.go b/d_api_auth.go index 16ebbdd0..6481486c 100644 --- a/d_api_auth.go +++ b/d_api_auth.go @@ -12,8 +12,9 @@ func dAPIAuthHandler(w http.ResponseWriter, r *http.Request, s *Session) { "status": "error", "err_msg": "dAPI auth is disabled", }) - + return } + // Trim leading path r.URL.Path = strings.TrimPrefix(r.URL.Path, "auth") r.URL.Path = strings.TrimPrefix(r.URL.Path, "/") diff --git a/d_api_change_password.go b/d_api_change_password.go index c4d00b20..19a07171 100644 --- a/d_api_change_password.go +++ b/d_api_change_password.go @@ -3,5 +3,53 @@ package uadmin import "net/http" func dAPIChangePasswordHandler(w http.ResponseWriter, r *http.Request, s *Session) { + if s == nil { + w.WriteHeader(http.StatusForbidden) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "", + }) + return + } + if CheckCSRF(r) { + w.WriteHeader(http.StatusUnauthorized) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "Missing CSRF token", + }) + return + } + + oldPassword := r.FormValue("old_password") + newPassword := r.FormValue("new_password") + + // Check if there is a new password + if newPassword == "" { + w.WriteHeader(http.StatusBadRequest) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "Missing new password", + }) + return + } + + // Verify old password + err := verifyPassword(s.User.Password, oldPassword) + if err != nil { + incrementInvalidLogins(r) + w.WriteHeader(401) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "invalid password", + }) + return + } + + s.User.Password = newPassword + s.User.Save() + + ReturnJSON(w, r, map[string]interface{}{ + "status": "ok", + }) } diff --git a/d_api_login.go b/d_api_login.go index 19e5704d..551669bb 100644 --- a/d_api_login.go +++ b/d_api_login.go @@ -3,9 +3,7 @@ package uadmin import "net/http" func dAPILoginHandler(w http.ResponseWriter, r *http.Request, s *Session) { - if s != nil { - Logout(r) - } + _ = s // Get request variables username := r.FormValue("username") @@ -17,7 +15,6 @@ func dAPILoginHandler(w http.ResponseWriter, r *http.Request, s *Session) { if otp != "" { // Check if there is username and password or a session key if session != "" { - w.WriteHeader(http.StatusAccepted) s = Login2FAKey(r, session, otp) } else { s = Login2FA(r, username, password, otp) @@ -27,7 +24,7 @@ func dAPILoginHandler(w http.ResponseWriter, r *http.Request, s *Session) { } if optRequired { - w.WriteHeader(http.StatusUnauthorized) + w.WriteHeader(http.StatusAccepted) ReturnJSON(w, r, map[string]interface{}{ "status": "error", "err_msg": "OTP Required", @@ -37,14 +34,17 @@ func dAPILoginHandler(w http.ResponseWriter, r *http.Request, s *Session) { } if s == nil { - w.WriteHeader(http.StatusForbidden) + w.WriteHeader(http.StatusUnauthorized) ReturnJSON(w, r, map[string]interface{}{ "status": "error", - "err_msg": "Invalid username or password", + "err_msg": "Invalid credentials", }) return } + // Preload the user to get the group name + Preload(&s.User) + jwt := SetSessionCookie(w, r, s) ReturnJSON(w, r, map[string]interface{}{ "status": "ok", @@ -54,7 +54,7 @@ func dAPILoginHandler(w http.ResponseWriter, r *http.Request, s *Session) { "username": s.User.Username, "first_name": s.User.FirstName, "last_name": s.User.LastName, - "group_id": s.User.UserGroupID, + "group_name": s.User.UserGroup.GroupName, "admin": s.User.Admin, }, }) diff --git a/d_api_logout.go b/d_api_logout.go index 671206fe..30eb7506 100644 --- a/d_api_logout.go +++ b/d_api_logout.go @@ -7,10 +7,20 @@ func dAPILogoutHandler(w http.ResponseWriter, r *http.Request, s *Session) { w.WriteHeader(http.StatusUnauthorized) ReturnJSON(w, r, map[string]interface{}{ "status": "error", - "err_msg": "Already logged out", + "err_msg": "User not logged in", }) return } + + if CheckCSRF(r) { + w.WriteHeader(http.StatusUnauthorized) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "Missing CSRF token", + }) + return + } + Logout(r) ReturnJSON(w, r, map[string]interface{}{ "status": "ok", diff --git a/d_api_reset_password.go b/d_api_reset_password.go index 22034da9..124046dd 100644 --- a/d_api_reset_password.go +++ b/d_api_reset_password.go @@ -1,7 +1,167 @@ package uadmin -import "net/http" +import ( + "net/http" + "time" +) func dAPIResetPasswordHandler(w http.ResponseWriter, r *http.Request, s *Session) { + // Get parameters + username := r.FormValue("username") + email := r.FormValue("email") + otp := r.FormValue("otp") + password := r.FormValue("password") + // check if there is an email or a username + if username == "" && email == "" { + w.WriteHeader(400) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "No username nor email", + }) + // log the request + go func() { + log := &Log{} + if password != "" { + r.Form.Set("password", "*****") + } + log.PasswordReset("", log.Action.PasswordResetDenied(), r) + log.Save() + }() + return + } + + // get user + user := User{} + if username != "" { + Get(&user, "username = ? AND active = ?", username, true) + } else { + Get(&user, "email = ? AND active = ?", email, true) + } + + // log the request + go func() { + log := &Log{} + if password != "" { + r.Form.Set("password", "*****") + } + log.PasswordReset(user.Username, log.Action.PasswordResetRequest(), r) + log.Save() + }() + + // check if the user exists and active + if user.ID == 0 || (user.ExpiresOn != nil || user.ExpiresOn.After(time.Now())) { + w.WriteHeader(404) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "username or email do not match any active user", + }) + // log the request + go func() { + log := &Log{} + if password != "" { + r.Form.Set("password", "*****") + } + log.PasswordReset(user.Username, log.Action.PasswordResetDenied(), r) + log.Save() + }() + return + } + + // If there is no otp, then we assume this is a request to send a password + // reset email + if otp == "" { + err := forgotPasswordHandler(&s.User, r, CustomResetPasswordLink, ResetPasswordMessage) + + if err != nil { + w.WriteHeader(403) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": err.Error(), + }) + // log the request + go func() { + log := &Log{} + if password != "" { + r.Form.Set("password", "*****") + } + log.PasswordReset(user.Username, log.Action.PasswordResetDenied(), r) + log.Save() + }() + return + } + // log the request + w.WriteHeader(http.StatusAccepted) + go func() { + log := &Log{} + if password != "" { + r.Form.Set("password", "*****") + } + r.Form.Set("reset-status", "Email was sent with the OTP") + log.PasswordReset(user.Username, log.Action.PasswordResetSuccessful(), r) + log.Save() + }() + ReturnJSON(w, r, map[string]interface{}{ + "status": "ok", + }) + return + } + + // Since there is an OTP, we can check it and reset the password + // Check if there is a a new password + if password == "" { + w.WriteHeader(400) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "missing password", + }) + // log the request + go func() { + log := &Log{} + if password != "" { + r.Form.Set("password", "*****") + } + log.PasswordReset("", log.Action.PasswordResetDenied(), r) + log.Save() + }() + return + } + + // check OTP + if !user.VerifyOTP(otp) { + incrementInvalidLogins(r) + w.WriteHeader(401) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "invalid or expired OTP", + }) + // log the request + go func() { + log := &Log{} + if password != "" { + r.Form.Set("password", "*****") + } + log.PasswordReset("", log.Action.PasswordResetDenied(), r) + log.Save() + }() + return + } + + // reset the password + user.Password = password + user.Save() + + // log the request + go func() { + log := &Log{} + if password != "" { + r.Form.Set("password", "*****") + } + r.Form.Set("reset-status", "Successfully changed the password") + log.PasswordReset("", log.Action.PasswordResetSuccessful(), r) + log.Save() + }() + ReturnJSON(w, r, map[string]interface{}{ + "status": "ok", + }) } diff --git a/d_api_signup.go b/d_api_signup.go index c1131139..1a03570a 100644 --- a/d_api_signup.go +++ b/d_api_signup.go @@ -3,5 +3,83 @@ package uadmin import "net/http" func dAPISignupHandler(w http.ResponseWriter, r *http.Request, s *Session) { + // Check if signup API is allowed + if !AllowDAPISignup { + w.WriteHeader(http.StatusForbidden) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "Signup API is disabled", + }) + return + } + // get variables from request + username := r.FormValue("username") + email := r.FormValue("email") + firstName := r.FormValue("first_name") + lastName := r.FormValue("last_name") + password := r.FormValue("password") + + // set the username to email if there is no username + if username == "" && email != "" { + username = email + } + + // check if password is empty + if password == "" { + w.WriteHeader(http.StatusBadRequest) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "password is empty", + }) + return + } + + // create user object + user := User{ + Username: username, + FirstName: firstName, + LastName: lastName, + Password: password, + Email: email, + Active: DAPISignupActive, + Admin: false, + RemoteAccess: DAPISignupAllowRemote, + UserGroupID: uint(DAPISignupGroupID), + } + + // run custom validation + if SignupValidationHandler != nil { + err := SignupValidationHandler(&user) + w.WriteHeader(http.StatusBadRequest) + if err != nil { + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": err.Error(), + }) + return + } + } + + // Save user record + user.Save() + + // Check if the record was not saved, that means the username is taken + if user.ID == 0 { + w.WriteHeader(400) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "username taken", + }) + } + + // if the user is active, then login in + if user.Active { + dAPILoginHandler(w, r, s) + return + } + + ReturnJSON(w, r, map[string]interface{}{ + "status": "ok", + }) } diff --git a/forgot_password_handler.go b/forgot_password_handler.go index a9a9d314..0428f7d4 100644 --- a/forgot_password_handler.go +++ b/forgot_password_handler.go @@ -8,22 +8,25 @@ import ( ) // forgotPasswordHandler ! -func forgotPasswordHandler(u *User, r *http.Request) error { +func forgotPasswordHandler(u *User, r *http.Request, link string, msg string) error { if u.Email == "" { return fmt.Errorf("unable to reset password, the user does not have an email") } - msg := `Dear {NAME}, + if msg == "" { + msg = `Dear {NAME}, -Have you forgotten your password to access {WEBSITE}. Don't worry we got your back. Please follow the link below to reset your password. - -If you want to reset your password, click this link: -{URL} - -If you didn't request a password reset, you can ignore this message. + Have you forgotten your password to access {WEBSITE}. Don't worry we got your back. Please follow the link below to reset your password. + + If you want to reset your password, click this link: + {URL} + + If you didn't request a password reset, you can ignore this message. + + Regards, + {WEBSITE} Support + ` + } -Regards, -{WEBSITE} Support -` // Check if the host name is in the allowed hosts list allowed := false var host string @@ -46,10 +49,17 @@ Regards, } urlParts := strings.Split(r.Header.Get("origin"), "://") - link := urlParts[0] + "://" + r.Host + RootURL + "resetpassword?u=" + fmt.Sprint(u.ID) + "&key=" + u.GetOTP() - msg = strings.Replace(msg, "{NAME}", u.String(), -1) - msg = strings.Replace(msg, "{WEBSITE}", SiteName, -1) - msg = strings.Replace(msg, "{URL}", link, -1) + if link == "" { + link = "{PROTOCOL}://{HOST}" + RootURL + "resetpassword?u={USER_ID}&key={OTP}" + } + link = strings.ReplaceAll(link, "{PROTOCOL}", urlParts[0]) + link = strings.ReplaceAll(link, "{HOST}", RootURL) + link = strings.ReplaceAll(link, "{USER_ID}", fmt.Sprint(u.ID)) + link = strings.ReplaceAll(link, "{OTP}", u.GetOTP()) + + msg = strings.ReplaceAll(msg, "{NAME}", u.String()) + msg = strings.ReplaceAll(msg, "{WEBSITE}", SiteName) + msg = strings.ReplaceAll(msg, "{URL}", link) subject := "Password reset for " + SiteName err = SendEmail([]string{u.Email}, []string{}, []string{}, subject, msg) diff --git a/forgot_password_handler_test.go b/forgot_password_handler_test.go index 541db364..314401b5 100644 --- a/forgot_password_handler_test.go +++ b/forgot_password_handler_test.go @@ -12,13 +12,13 @@ func (t *UAdminTests) TestForgotPasswordHandler() { user := User{} Get(&user, "id = ?", 1) - err := forgotPasswordHandler(&user, r) + err := forgotPasswordHandler(&user, r, "", "") if err == nil { t.Errorf("forgotPasswordHandler didn't return an error on a user with no email") } user.Email = "user@example.com" - err = forgotPasswordHandler(&user, r) + err = forgotPasswordHandler(&user, r, "", "") if err != nil { t.Errorf("forgotPasswordHandler returned an error. %s", err) } diff --git a/global.go b/global.go index c73bd3e3..e39fa500 100644 --- a/global.go +++ b/global.go @@ -380,7 +380,43 @@ var DisableDAPIAuth = true var AllowDAPISignup = false // DAPISignupGroupID is the default user group id new users get when they sign up -var DAPISignupGroupID = false +// to leave new signed up users without a group, use value 0 for this variable +var DAPISignupGroupID = 0 + +// DAPISignupActive controls if new signed up users are activate automatically +var DAPISignupActive = true + +// DAPISignupAllowRemote controls if new signed up users are can login over the internet +var DAPISignupAllowRemote = true + +// SignupValidationHandler can be used to validate or customize new +// signed up users. Note that the password in the password field +// is passed in plain text. Do not plain text passwords anywhere. +var SignupValidationHandler func(user *User) error + +// CustomResetPasswordLink is the link sent to the user's email to reset their password +// the string may include the following place holder: +// "{PROTOCOL}://{HOST}/resetpassword?u={USER_ID}&key={OTP}" +var CustomResetPasswordLink = "" + +// ResetPasswordMessage is a message that can be sent to the user email when +// a password reset request sends an email. This message may include the +// following place holders: +// {NAME}: user real name +// {WEBSITE}: website name +// {URL}: link to the password reset page +var ResetPasswordMessage = `Dear {NAME}, + +Have you forgotten your password to access {WEBSITE}. Don't worry we got your back. Please follow the link below to reset your password. + +If you want to reset your password, click this link: +{URL} + +If you didn't request a password reset, you can ignore this message. + +Regards, +{WEBSITE} Support +` // Private Global Variables // Regex diff --git a/login_handler.go b/login_handler.go index d83c49e6..19053791 100644 --- a/login_handler.go +++ b/login_handler.go @@ -39,7 +39,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { IncrementMetric("uadmin/security/passwordreset/emailsent") c.ErrExists = true c.Err = "Password recovery request sent. Please check email to reset your password" - forgotPasswordHandler(&user, r) + forgotPasswordHandler(&user, r, "", "") } else { IncrementMetric("uadmin/security/passwordreset/invalidemail") c.ErrExists = true diff --git a/openapi.go b/openapi.go index e9ae80f6..1e150097 100644 --- a/openapi.go +++ b/openapi.go @@ -71,7 +71,10 @@ func GenerateOpenAPISchema() { } case cFK: return &openapi.SchemaObject{ - Ref: "#/components/schemas/" + v.Fields[i].TypeName, + AllOf: []*openapi.SchemaObject{ + {Ref: "#/components/schemas/" + v.Fields[i].TypeName}, + {}, + }, } case cHTML: return &openapi.SchemaObject{ @@ -155,14 +158,20 @@ func GenerateOpenAPISchema() { } // Set other schema properties - fields[v.Fields[i].Name].Description = v.Fields[i].Help - fields[v.Fields[i].Name].Default = v.Fields[i].DefaultValue - fields[v.Fields[i].Name].Title = v.Fields[i].DisplayName - if val, ok := v.Fields[i].Max.(string); ok && val != "" { - fields[v.Fields[i].Name].Maximum, _ = strconv.Atoi(val) - } - if val, ok := v.Fields[i].Min.(string); ok && val != "" { - fields[v.Fields[i].Name].Minimum, _ = strconv.Atoi(val) + if v.Fields[i].Type != cFK { + fields[v.Fields[i].Name].Description = v.Fields[i].Help + fields[v.Fields[i].Name].Default = v.Fields[i].DefaultValue + fields[v.Fields[i].Name].Title = v.Fields[i].DisplayName + if val, ok := v.Fields[i].Max.(string); ok && val != "" { + fields[v.Fields[i].Name].Maximum, _ = strconv.Atoi(val) + } + if val, ok := v.Fields[i].Min.(string); ok && val != "" { + fields[v.Fields[i].Name].Minimum, _ = strconv.Atoi(val) + } + } else { + fields[v.Fields[i].Name].AllOf[1].Description = v.Fields[i].Help + fields[v.Fields[i].Name].AllOf[1].Default = v.Fields[i].DefaultValue + fields[v.Fields[i].Name].AllOf[1].Title = v.Fields[i].DisplayName } // Add parameters @@ -332,7 +341,7 @@ func GenerateOpenAPISchema() { Summary: "Read one " + v.DisplayName, Description: "Read one " + v.DisplayName, Get: &openapi.Operation{ - Tags: []string{v.Name, func() string { + Tags: []string{func() string { if v.Category != "" { return v.Category } else { @@ -376,10 +385,10 @@ func GenerateOpenAPISchema() { } // Read Many s.Paths[fmt.Sprintf("/api/d/%s/read", v.ModelName)] = openapi.Path{ - Summary: "Read one " + v.DisplayName, - Description: "Read one " + v.DisplayName, + Summary: "Read many " + v.DisplayName, + Description: "Read many " + v.DisplayName, Get: &openapi.Operation{ - Tags: []string{v.Name, func() string { + Tags: []string{func() string { if v.Category != "" { return v.Category } else { @@ -388,7 +397,7 @@ func GenerateOpenAPISchema() { }()}, Responses: map[string]openapi.Response{ "200": { - Description: v.DisplayName + " record", + Description: v.DisplayName + " records", Content: map[string]openapi.MediaType{ "application/json": { Schema: &openapi.SchemaObject{ @@ -447,7 +456,7 @@ func GenerateOpenAPISchema() { Summary: "Add one " + v.DisplayName, Description: "Add one " + v.DisplayName, Post: &openapi.Operation{ - Tags: []string{v.Name, func() string { + Tags: []string{func() string { if v.Category != "" { return v.Category } else { @@ -456,7 +465,7 @@ func GenerateOpenAPISchema() { }()}, Responses: map[string]openapi.Response{ "200": { - Description: v.DisplayName + " record", + Description: v.DisplayName + " record added", Content: map[string]openapi.MediaType{ "application/json": { Schema: &openapi.SchemaObject{ @@ -475,21 +484,21 @@ func GenerateOpenAPISchema() { }, }, }, - Parameters: append(writeParameters, []openapi.Parameter{ + Parameters: append([]openapi.Parameter{ { Ref: "#/components/parameters/CSRF", }, { Ref: "#/components/parameters/stat", }, - }...), + }, writeParameters...), } - // Add One - s.Paths[fmt.Sprintf("/api/d/%s/add", v.ModelName)] = openapi.Path{ - Summary: "Add one " + v.DisplayName, - Description: "Add one " + v.DisplayName, + // Edit One + s.Paths[fmt.Sprintf("/api/d/%s/edit/{id}", v.ModelName)] = openapi.Path{ + Summary: "Edit one " + v.DisplayName, + Description: "Edit one " + v.DisplayName, Post: &openapi.Operation{ - Tags: []string{v.Name, func() string { + Tags: []string{func() string { if v.Category != "" { return v.Category } else { @@ -498,25 +507,152 @@ func GenerateOpenAPISchema() { }()}, Responses: map[string]openapi.Response{ "200": { - Description: v.DisplayName + " record", + Description: v.DisplayName + " record edited", Content: map[string]openapi.MediaType{ "application/json": { Schema: &openapi.SchemaObject{ - Ref: "#/components/schemas/" + v.Name, + Type: "object", + Properties: map[string]*openapi.SchemaObject{ + "rows_count": {Type: "integer"}, + "status": {Type: "string"}, + }, }, }, }, }, }, }, - Parameters: append(writeParameters, []openapi.Parameter{ + Parameters: append([]openapi.Parameter{ + { + Ref: "#/components/parameters/PathID", + }, { Ref: "#/components/parameters/CSRF", }, { Ref: "#/components/parameters/stat", }, - }...), + }, writeParameters...), + } + // Edit Many + s.Paths[fmt.Sprintf("/api/d/%s/edit", v.ModelName)] = openapi.Path{ + Summary: "Edit many " + v.DisplayName, + Description: "Edit many " + v.DisplayName, + Post: &openapi.Operation{ + Tags: []string{func() string { + if v.Category != "" { + return v.Category + } else { + return "Other" + } + }()}, + Responses: map[string]openapi.Response{ + "200": { + Description: v.DisplayName + " records edited", + Content: map[string]openapi.MediaType{ + "application/json": { + Schema: &openapi.SchemaObject{ + Type: "object", + Properties: map[string]*openapi.SchemaObject{ + "rows_count": {Type: "integer"}, + "status": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + Parameters: append([]openapi.Parameter{ + { + Ref: "#/components/parameters/PathID", + }, + { + Ref: "#/components/parameters/CSRF", + }, + { + Ref: "#/components/parameters/stat", + }, + }, append(writeParameters, parameters...)...), + } + // Delete One + s.Paths[fmt.Sprintf("/api/d/%s/delete/{id}", v.ModelName)] = openapi.Path{ + Summary: "Delete one " + v.DisplayName, + Description: "Delete one " + v.DisplayName, + Post: &openapi.Operation{ + Tags: []string{func() string { + if v.Category != "" { + return v.Category + } else { + return "Other" + } + }()}, + Responses: map[string]openapi.Response{ + "200": { + Description: v.DisplayName + " record deleted", + Content: map[string]openapi.MediaType{ + "application/json": { + Schema: &openapi.SchemaObject{ + Type: "object", + Properties: map[string]*openapi.SchemaObject{ + "rows_count": {Type: "integer"}, + "status": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + Parameters: []openapi.Parameter{ + { + Ref: "#/components/parameters/PathID", + }, + { + Ref: "#/components/parameters/CSRF", + }, + { + Ref: "#/components/parameters/stat", + }, + }, + } + // Delete Many + s.Paths[fmt.Sprintf("/api/d/%s/delete", v.ModelName)] = openapi.Path{ + Summary: "Delete many " + v.DisplayName, + Description: "Delete many " + v.DisplayName, + Post: &openapi.Operation{ + Tags: []string{func() string { + if v.Category != "" { + return v.Category + } else { + return "Other" + } + }()}, + Responses: map[string]openapi.Response{ + "200": { + Description: v.DisplayName + " records deleted", + Content: map[string]openapi.MediaType{ + "application/json": { + Schema: &openapi.SchemaObject{ + Type: "object", + Properties: map[string]*openapi.SchemaObject{ + "rows_count": {Type: "integer"}, + "status": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + Parameters: append([]openapi.Parameter{ + { + Ref: "#/components/parameters/CSRF", + }, + { + Ref: "#/components/parameters/stat", + }, + }, parameters...), } s.Components.Schemas[v.Name] = openapi.SchemaObject{ diff --git a/openapi/auth_paths.go b/openapi/auth_paths.go new file mode 100644 index 00000000..a8c73bee --- /dev/null +++ b/openapi/auth_paths.go @@ -0,0 +1,449 @@ +package openapi + +func getAuthPaths() map[string]Path { + return map[string]Path{ + // Login auth API + "/api/d/auth/login": { + Summary: "Login", + Description: "Login API", + Post: &Operation{ + Tags: []string{"Auth"}, + Responses: map[string]Response{ + "200": { + Description: "Successful login", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + "jwt": {Type: "string"}, + "session": {Type: "string"}, + "user": { + Type: "object", + Properties: map[string]*SchemaObject{ + "username": {Type: "string"}, + "admin": {Type: "boolean"}, + "first_name": {Type: "string"}, + "last_name": {Type: "string"}, + "group_name": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + "202": { + Description: "Username and password are correct but MFA is required and OTP was not provided", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + "err_msg": {Type: "string"}, + "session": {Type: "string"}, + }, + }, + }, + }, + }, + "401": { + Description: "Invalid credentials", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + "err_msg": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + Parameters: []Parameter{ + { + Name: "username", + In: "query", + Description: "Required for username/password login and single step MFA. But not required during the second step of a two-step MFA authentication", + Schema: &SchemaObject{ + Type: "string", + }, + }, + { + Name: "password", + In: "query", + Description: "Required for username/password login and single step MFA. But not required during the second step of a two-step MFA authentication", + Schema: &SchemaObject{ + Type: "string", + }, + }, + { + Name: "otp", + In: "query", + Description: "Not required for username/password login. Required for the second step in a two-step MFA and required single-step for MFA", + Schema: &SchemaObject{ + Type: "string", + }, + }, + { + Name: "session", + In: "query", + Description: "Only required during the second step of a two-step MFA authentication", + Schema: &SchemaObject{ + Type: "string", + }, + }, + }, + }, + + // Logout auth API + "/api/d/auth/logout": { + Summary: "Logout", + Description: "Logout API", + Post: &Operation{ + Tags: []string{"Auth"}, + Responses: map[string]Response{ + "200": { + Description: "Successful logout", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + }, + }, + }, + }, + }, + "401": { + Description: "User not logged in", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + "err_msg": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + Parameters: []Parameter{ + { + Ref: "#/components/parameters/CSRF", + }, + }, + }, + + // Signup auth API + "/api/d/auth/signup": { + Summary: "Signup", + Description: "Signup API", + Post: &Operation{ + Tags: []string{"Auth"}, + Responses: map[string]Response{ + "200": { + Description: "Successful signup", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + "jwt": {Type: "string"}, + "session": {Type: "string"}, + "user": { + Type: "object", + Properties: map[string]*SchemaObject{ + "username": {Type: "string"}, + "admin": {Type: "boolean"}, + "first_name": {Type: "string"}, + "last_name": {Type: "string"}, + "group_name": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + "400": { + Description: "Invalid or missing signup data. More about the error in err_msg.", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + "err_msg": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + Parameters: []Parameter{ + { + Name: "username", + In: "query", + Required: true, + Description: "Username can be any string or an email", + Schema: &SchemaObject{ + Type: "string", + }, + }, + { + Name: "password", + In: "query", + Required: true, + Description: "Password", + Schema: &SchemaObject{ + Type: "string", + }, + }, + { + Name: "first_name", + In: "query", + Required: true, + Description: "First name", + Schema: &SchemaObject{ + Type: "string", + }, + }, + { + Name: "last_name", + In: "query", + Required: true, + Description: "Last name", + Schema: &SchemaObject{ + Type: "string", + }, + }, + { + Name: "email", + In: "query", + Required: true, + Description: "Email", + Schema: &SchemaObject{ + Type: "string", + }, + }, + }, + }, + + // Reset password auth API + "/api/d/auth/resetpassword": { + Summary: "Reset Password", + Description: "Reset Password API", + Post: &Operation{ + Tags: []string{"Auth"}, + Responses: map[string]Response{ + "200": { + Description: "Successful password reset", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + }, + }, + }, + }, + }, + "202": { + Description: "Password reset email sent", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + }, + }, + }, + }, + }, + "400": { + Description: "Missing password rest data. More about the error in err_msg.", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + "err_msg": {Type: "string"}, + }, + }, + }, + }, + }, + "401": { + Description: "Invalid or expired OTP", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + "err_msg": {Type: "string"}, + }, + }, + }, + }, + }, + "403": { + Description: "User does not have an email", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + "err_msg": {Type: "string"}, + }, + }, + }, + }, + }, + "404": { + Description: "username or email do not match any active user", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + "err_msg": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + Parameters: []Parameter{ + { + Name: "username", + In: "query", + Description: "Username or email is required", + Schema: &SchemaObject{ + Type: "string", + }, + }, + { + Name: "email", + In: "query", + Required: true, + Description: "Username or email is required", + Schema: &SchemaObject{ + Type: "string", + }, + }, + { + Name: "password", + In: "query", + Required: true, + Description: "New password which is required in the second step with the OTP", + Schema: &SchemaObject{ + Type: "string", + }, + }, + { + Name: "otp", + In: "query", + Required: true, + Description: "OTP is required in the second step with the new password", + Schema: &SchemaObject{ + Type: "string", + }, + }, + }, + }, + + // Change password auth API + "/api/d/auth/changepassword": { + Summary: "Change Password", + Description: "Change Password API", + Post: &Operation{ + Tags: []string{"Auth"}, + Responses: map[string]Response{ + "200": { + Description: "Successful password reset", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + }, + }, + }, + }, + }, + "400": { + Description: "Missing new password", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + "err_msg": {Type: "string"}, + }, + }, + }, + }, + }, + "401": { + Description: "current password is invalid", + Content: map[string]MediaType{ + "application/json": { + Schema: &SchemaObject{ + Type: "object", + Properties: map[string]*SchemaObject{ + "status": {Type: "string"}, + "err_msg": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + Parameters: []Parameter{ + { + Ref: "#/components/parameters/CSRF", + }, + { + Name: "old_password", + In: "query", + Required: true, + Description: "Current user password", + Schema: &SchemaObject{ + Type: "string", + }, + }, + { + Name: "new_password", + In: "query", + Required: true, + Description: "New password", + Schema: &SchemaObject{ + Type: "string", + }, + }, + }, + }, + } +} diff --git a/openapi/generate_schema.go b/openapi/generate_schema.go index 65e3e80c..27dc7474 100644 --- a/openapi/generate_schema.go +++ b/openapi/generate_schema.go @@ -2,12 +2,26 @@ package openapi func GenerateBaseSchema() *Schema { s := &Schema{ - OpenAPI: "3.1.0", + OpenAPI: "3.0.3", Info: &SchemaInfo{ Title: " API documentation", Description: "API documentation", Version: "1.0.0", }, + Tags: []Tag{ + { + Name: "Auth", + Description: "Authentication API", + }, + { + Name: "System", + Description: "CRUD APIs for uAdmin core models", + }, + { + Name: "Other", + Description: "CRUD APIs for models with no category", + }, + }, Components: &Components{ Schemas: map[string]SchemaObject{ "Integer": { @@ -19,7 +33,7 @@ func GenerateBaseSchema() *Schema { {Modifier: "__lte", In: "suffix", Summary: "Less than or equal to"}, {Modifier: "__in", In: "suffix", Summary: "Find a value matching any of these values"}, {Modifier: "__between", In: "suffix", Summary: "Selects values within a given range"}, - {Modifier: "!", In: "", Summary: "Negates operator"}, + {Modifier: "!", In: "prefix", Summary: "Negates operator"}, }, XAggregator: []XModifier{ {Modifier: "__sum", In: "suffix", Summary: "Returns the total sum of a numeric field"}, @@ -38,7 +52,7 @@ func GenerateBaseSchema() *Schema { {Modifier: "__lte", In: "suffix", Summary: "Less than or equal to"}, {Modifier: "__in", In: "suffix", Summary: "Find a value matching any of these values"}, {Modifier: "__between", In: "suffix", Summary: "Selects values within a given range"}, - {Modifier: "!", In: "", Summary: "Negates operator"}, + {Modifier: "!", In: "prefix", Summary: "Negates operator"}, }, XAggregator: []XModifier{ {Modifier: "__sum", In: "suffix", Summary: "Returns the total sum of a numeric field"}, @@ -59,7 +73,7 @@ func GenerateBaseSchema() *Schema { {Modifier: "__istartswith", In: "suffix", Summary: "Search for string values that starts with a given substring"}, {Modifier: "__iendswith", In: "", Summary: "Search for string values that ends with a given substring"}, {Modifier: "__in", In: "", Summary: "Find a value matching any of these values"}, - {Modifier: "!", In: "", Summary: "Negates operator"}, + {Modifier: "!", In: "prefix", Summary: "Negates operator"}, }, XAggregator: []XModifier{ {Modifier: "__count", In: "suffix", Summary: "Returns the number of rows"}, @@ -76,7 +90,7 @@ func GenerateBaseSchema() *Schema { {Modifier: "__istartswith", In: "suffix", Summary: "Search for string values that starts with a given substring"}, {Modifier: "__iendswith", In: "", Summary: "Search for string values that ends with a given substring"}, {Modifier: "__in", In: "", Summary: "Find a value matching any of these values"}, - {Modifier: "!", In: "", Summary: "Negates operator"}, + {Modifier: "!", In: "prefix", Summary: "Negates operator"}, }, XAggregator: []XModifier{ {Modifier: "__count", In: "suffix", Summary: "Returns the number of rows"}, @@ -392,7 +406,7 @@ func GenerateBaseSchema() *Schema { }, }, }, - Paths: map[string]Path{}, + Paths: getAuthPaths(), Security: []SecurityRequirement{ { "apiKeyCookie": []string{}, diff --git a/openapi/schema_object.go b/openapi/schema_object.go index acc61dfd..bdae2e6b 100644 --- a/openapi/schema_object.go +++ b/openapi/schema_object.go @@ -20,7 +20,8 @@ type SchemaObject struct { Example *Example `json:"example,omitempty"` Enum []interface{} `json:"enum,omitempty"` OneOf []*SchemaObject `json:"oneOf,omitempty"` - Const interface{} `json:"const,omitempty"` + AllOf []*SchemaObject `json:"allOf,omitempty"` + Const interface{} `json:"x-const,omitempty"` XFilters []XModifier `json:"x-filter,omitempty"` XAggregator []XModifier `json:"x-aggregator,omitempty"` } diff --git a/password_reset_handler.go b/password_reset_handler.go index e6dab260..b6f686d6 100644 --- a/password_reset_handler.go +++ b/password_reset_handler.go @@ -31,6 +31,9 @@ func passwordResetHandler(w http.ResponseWriter, r *http.Request) { if user.ID == 0 { go func() { log := &Log{} + if r.Form.Get("password") != "" { + r.Form.Set("password", "*****") + } r.Form.Set("reset-status", "invalid user id") log.PasswordReset(userID, log.Action.PasswordResetDenied(), r) log.Save() @@ -42,6 +45,9 @@ func passwordResetHandler(w http.ResponseWriter, r *http.Request) { if !user.VerifyOTP(otpCode) { go func() { log := &Log{} + if r.Form.Get("password") != "" { + r.Form.Set("password", "*****") + } r.Form.Set("reset-status", "invalid otp code: "+otpCode) log.PasswordReset(user.Username, log.Action.PasswordResetDenied(), r) log.Save() @@ -61,6 +67,9 @@ func passwordResetHandler(w http.ResponseWriter, r *http.Request) { go func() { log := &Log{} r.Form.Set("reset-status", "Successfully changed the password") + if r.Form.Get("password") != "" { + r.Form.Set("password", "*****") + } log.PasswordReset(user.Username, log.Action.PasswordResetSuccessful(), r) log.Save() }() diff --git a/templates/uadmin/default/trail.html b/templates/uadmin/default/trail.html index a1cfa6a7..02c0830b 100644 --- a/templates/uadmin/default/trail.html +++ b/templates/uadmin/default/trail.html @@ -1,7 +1,7 @@ - {{.SiteName}} - {{Tf "uadmin/system" .Language.Code "Dashboard"}} + {{.SiteName}} - {{Tf "uadmin/system" .Language.Code "Trail"}} @@ -19,7 +19,8 @@ "\x1b[0m": "" } var justLoaded = true; - $.ajax('/uadmin-portal/api/trail/', { + var RootURL = '{{.RootURL}}'; + $.ajax(RootURL+'api/trail/', { xhrFields: { onprogress: function(e) { diff --git a/user.go b/user.go index 46e08ec9..342daffe 100644 --- a/user.go +++ b/user.go @@ -4,8 +4,6 @@ import ( "fmt" "strings" "time" - - "golang.org/x/crypto/bcrypt" ) // User ! @@ -36,7 +34,12 @@ func (u User) String() string { // Save ! func (u *User) Save() { - if !strings.HasPrefix(u.Password, "$2a$") && len(u.Password) != 60 { + err := u.Validate() + if len(err) != 0 { + return + } + + if !strings.HasPrefix(u.Password, "$2a$") || len(u.Password) != 60 { u.Password = hashPass(u.Password) } if u.OTPSeed == "" { @@ -70,9 +73,7 @@ func (u *User) Login(pass string, otp string) *Session { return nil } - password := []byte(pass + Salt) - hashedPassword := []byte(u.Password) - err := bcrypt.CompareHashAndPassword(hashedPassword, password) + err := verifyPassword(u.Password, pass) if err == nil && u.ID != 0 { s := u.GetActiveSession() if s == nil { @@ -163,7 +164,7 @@ func (u *User) GetDashboardMenu() (menus []DashboardMenu) { // HasAccess returns the user level permission to a model. The modelName // the the URL of the model func (u *User) HasAccess(modelName string) UserPermission { - Trail(WARNING, "User.HasAccess will be deprecated in version 0.6.0. Use User.GetAccess instead.") + Trail(WARNING, "User.HasAccess was deprecated in version 0.6.0. Use User.GetAccess instead.") return u.hasAccess(modelName) } From 08a60b22b9babb7e4b5c436ba3268786b86601e9 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Fri, 25 Nov 2022 13:35:21 +0400 Subject: [PATCH 02/92] BUG FIXES: auth dAPI and send email dAPI --- auth.go | 36 ++++++++++++++++++++++++++++++++++-- d_api_change_password.go | 2 +- d_api_reset_password.go | 18 +++++++++--------- forgot_password_handler.go | 15 +++++++++------ global.go | 4 ++-- openapi/auth_paths.go | 11 ++++------- send_email.go | 5 +++-- 7 files changed, 62 insertions(+), 29 deletions(-) diff --git a/auth.go b/auth.go index ed958d31..6587529c 100644 --- a/auth.go +++ b/auth.go @@ -707,11 +707,17 @@ func getSession(r *http.Request) string { // user from a request func GetRemoteIP(r *http.Request) string { ips := r.Header.Get("X-Forwarded-For") + splitIps := strings.Split(ips, ",") - if len(splitIps) > 0 { + if ips != "" { + // trim IP list + for i := range splitIps { + splitIps[i] = strings.TrimSpace(splitIps[i]) + } + // get last IP in list since ELB prepends other user defined IPs, meaning the last one is the actual client IP. - netIP := net.ParseIP(splitIps[len(splitIps)-1]) + netIP := net.ParseIP(splitIps[0]) if netIP != nil { return netIP.String() } @@ -734,6 +740,32 @@ func GetRemoteIP(r *http.Request) string { return r.RemoteAddr } +// GetHostName is a function that returns the host name from a request +func GetHostName(r *http.Request) string { + host := r.Header.Get("X-Forwarded-Host") + if host != "" { + return host + } + return r.Host +} + +// GetSchema is a function that returns the schema for a request (http, https) +func GetSchema(r *http.Request) string { + schema := r.Header.Get("X-Forwarded-Proto") + if schema != "" { + return schema + } + + if r.URL.Scheme != "" { + return r.URL.Scheme + } + + if r.TLS != nil { + return "https" + } + return "http" +} + func verifyPassword(hash string, plain string) error { password := []byte(plain + Salt) hashedPassword := []byte(hash) diff --git a/d_api_change_password.go b/d_api_change_password.go index 19a07171..81d1bd30 100644 --- a/d_api_change_password.go +++ b/d_api_change_password.go @@ -7,7 +7,7 @@ func dAPIChangePasswordHandler(w http.ResponseWriter, r *http.Request, s *Sessio w.WriteHeader(http.StatusForbidden) ReturnJSON(w, r, map[string]interface{}{ "status": "error", - "err_msg": "", + "err_msg": "User not logged in", }) return } diff --git a/d_api_reset_password.go b/d_api_reset_password.go index 124046dd..9cc79ae6 100644 --- a/d_api_reset_password.go +++ b/d_api_reset_password.go @@ -7,17 +7,17 @@ import ( func dAPIResetPasswordHandler(w http.ResponseWriter, r *http.Request, s *Session) { // Get parameters - username := r.FormValue("username") + uid := r.FormValue("uid") email := r.FormValue("email") otp := r.FormValue("otp") password := r.FormValue("password") // check if there is an email or a username - if username == "" && email == "" { + if email == "" && uid == "" { w.WriteHeader(400) ReturnJSON(w, r, map[string]interface{}{ "status": "error", - "err_msg": "No username nor email", + "err_msg": "No email or uid", }) // log the request go func() { @@ -33,10 +33,10 @@ func dAPIResetPasswordHandler(w http.ResponseWriter, r *http.Request, s *Session // get user user := User{} - if username != "" { - Get(&user, "username = ? AND active = ?", username, true) - } else { + if email != "" { Get(&user, "email = ? AND active = ?", email, true) + } else { + Get(&user, "id = ? AND active = ?", uid, true) } // log the request @@ -50,11 +50,11 @@ func dAPIResetPasswordHandler(w http.ResponseWriter, r *http.Request, s *Session }() // check if the user exists and active - if user.ID == 0 || (user.ExpiresOn != nil || user.ExpiresOn.After(time.Now())) { + if user.ID == 0 || (user.ExpiresOn != nil && user.ExpiresOn.After(time.Now())) { w.WriteHeader(404) ReturnJSON(w, r, map[string]interface{}{ "status": "error", - "err_msg": "username or email do not match any active user", + "err_msg": "email or uid do not match any active user", }) // log the request go func() { @@ -71,7 +71,7 @@ func dAPIResetPasswordHandler(w http.ResponseWriter, r *http.Request, s *Session // If there is no otp, then we assume this is a request to send a password // reset email if otp == "" { - err := forgotPasswordHandler(&s.User, r, CustomResetPasswordLink, ResetPasswordMessage) + err := forgotPasswordHandler(&user, r, CustomResetPasswordLink, ResetPasswordMessage) if err != nil { w.WriteHeader(403) diff --git a/forgot_password_handler.go b/forgot_password_handler.go index 0428f7d4..0d99b1ce 100644 --- a/forgot_password_handler.go +++ b/forgot_password_handler.go @@ -13,7 +13,7 @@ func forgotPasswordHandler(u *User, r *http.Request, link string, msg string) er return fmt.Errorf("unable to reset password, the user does not have an email") } if msg == "" { - msg = `Dear {NAME}, + msg = `

Dear {NAME},

Have you forgotten your password to access {WEBSITE}. Don't worry we got your back. Please follow the link below to reset your password. @@ -32,7 +32,7 @@ func forgotPasswordHandler(u *User, r *http.Request, link string, msg string) er var host string var allowedHost string var err error - if host, _, err = net.SplitHostPort(r.Host); err != nil { + if host, _, err = net.SplitHostPort(GetHostName(r)); err != nil { host = r.Host } for _, v := range strings.Split(AllowedHosts, ",") { @@ -41,19 +41,21 @@ func forgotPasswordHandler(u *User, r *http.Request, link string, msg string) er } if allowedHost == host { allowed = true + break } } + host = GetHostName(r) if !allowed { Trail(CRITICAL, "Reset password request for host: (%s) which is not in AllowedHosts settings", host) return nil } - urlParts := strings.Split(r.Header.Get("origin"), "://") + schema := GetSchema(r) if link == "" { - link = "{PROTOCOL}://{HOST}" + RootURL + "resetpassword?u={USER_ID}&key={OTP}" + link = "{SCHEMA}://{HOST}" + RootURL + "resetpassword?u={USER_ID}&key={OTP}" } - link = strings.ReplaceAll(link, "{PROTOCOL}", urlParts[0]) - link = strings.ReplaceAll(link, "{HOST}", RootURL) + link = strings.ReplaceAll(link, "{SCHEMA}", schema) + link = strings.ReplaceAll(link, "{HOST}", host) link = strings.ReplaceAll(link, "{USER_ID}", fmt.Sprint(u.ID)) link = strings.ReplaceAll(link, "{OTP}", u.GetOTP()) @@ -61,6 +63,7 @@ func forgotPasswordHandler(u *User, r *http.Request, link string, msg string) er msg = strings.ReplaceAll(msg, "{WEBSITE}", SiteName) msg = strings.ReplaceAll(msg, "{URL}", link) subject := "Password reset for " + SiteName + err = SendEmail([]string{u.Email}, []string{}, []string{}, subject, msg) return err diff --git a/global.go b/global.go index e39fa500..044e8ce9 100644 --- a/global.go +++ b/global.go @@ -80,7 +80,7 @@ const cEMAIL = "email" const cM2M = "m2m" // Version number as per Semantic Versioning 2.0.0 (semver.org) -const Version = "0.9.1" +const Version = "0.9.2" // VersionCodeName is the cool name we give to versions with significant changes. // This name should always be a bug's name starting from A-Z them revolving back. @@ -396,7 +396,7 @@ var SignupValidationHandler func(user *User) error // CustomResetPasswordLink is the link sent to the user's email to reset their password // the string may include the following place holder: -// "{PROTOCOL}://{HOST}/resetpassword?u={USER_ID}&key={OTP}" +// "{SCHEMA}://{HOST}/resetpassword?u={USER_ID}&key={OTP}" var CustomResetPasswordLink = "" // ResetPasswordMessage is a message that can be sent to the user email when diff --git a/openapi/auth_paths.go b/openapi/auth_paths.go index a8c73bee..f3e49ca5 100644 --- a/openapi/auth_paths.go +++ b/openapi/auth_paths.go @@ -334,9 +334,9 @@ func getAuthPaths() map[string]Path { }, Parameters: []Parameter{ { - Name: "username", + Name: "uid", In: "query", - Description: "Username or email is required", + Description: "Email or uid is required", Schema: &SchemaObject{ Type: "string", }, @@ -344,8 +344,7 @@ func getAuthPaths() map[string]Path { { Name: "email", In: "query", - Required: true, - Description: "Username or email is required", + Description: "Email or uid is required", Schema: &SchemaObject{ Type: "string", }, @@ -353,7 +352,6 @@ func getAuthPaths() map[string]Path { { Name: "password", In: "query", - Required: true, Description: "New password which is required in the second step with the OTP", Schema: &SchemaObject{ Type: "string", @@ -362,8 +360,7 @@ func getAuthPaths() map[string]Path { { Name: "otp", In: "query", - Required: true, - Description: "OTP is required in the second step with the new password", + Description: "OTP is required in the second step with a new password", Schema: &SchemaObject{ Type: "string", }, diff --git a/send_email.go b/send_email.go index 5d9bbd2b..d105498e 100644 --- a/send_email.go +++ b/send_email.go @@ -30,7 +30,8 @@ func SendEmail(to, cc, bcc []string, subject, body string, attachments ...string // prepare body by splitting it into lines of length 73 followed by = body = strings.ReplaceAll(body, "\n", "
") - body = strings.Join(splitString(body, 73), "=\n") + body = strings.ReplaceAll(body, "=", "=3D") + body = strings.Join(splitString(body, 73), "=\r\n") // Construct the email MIME := "MIME-version: 1.0;\r\nContent-Type: text/html; charset=\"utf-8\";\r\nContent-Transfer-Encoding: quoted-printable\r\n" @@ -70,7 +71,7 @@ func SendEmail(to, cc, bcc []string, subject, body string, attachments ...string msg += "Subject: " + subject + "\r\n" msg += MIME + "\r\n" msg += body - msg += "\r\n" + msg += "\r\n\r\n" // Append CC and BCC if cc != nil { to = append(to, cc...) From 4eb601b251d4bfabc4a01a8d1a06079e4ed9894a Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Fri, 25 Nov 2022 13:42:31 +0400 Subject: [PATCH 03/92] add go install to README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 07fa8ae3..e7859081 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,8 @@ Social Media: ## Installation ```bash -$ go get -u github.com/uadmin/uadmin/... +go get -u github.com/uadmin/uadmin/ +go install github.com/uadmin/uadmin/cmd/uadmin@latest ``` To test if your installation is fine, run the `uadmin` command line: From ad0276bc8572f3e0e1aa4118458b7649699b44d1 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Sat, 26 Nov 2022 09:42:45 +0400 Subject: [PATCH 04/92] DOCS: corrected SchemaCategory docs --- register.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/register.go b/register.go index b5cd1fd2..2cf25253 100644 --- a/register.go +++ b/register.go @@ -19,8 +19,8 @@ type HideInDashboarder interface { HideInDashboard() bool } -// SchemaCategory used to check if a model should be hidden in -// dashboard +// SchemaCategory provides a default category for the model. This can be +// customized later from the UI type SchemaCategory interface { SchemaCategory() string } From 0da272fbcbb0d95285d224dfbf1657322ecb3d38 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Sun, 27 Nov 2022 12:25:43 +0400 Subject: [PATCH 05/92] add deprecated to model schema --- get_schema.go | 1 + openapi.go | 29 +++++++++++++++++++++++++++++ openapi/schema_object.go | 4 +++- schema.go | 3 +++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/get_schema.go b/get_schema.go index 281003f0..212c966d 100644 --- a/get_schema.go +++ b/get_schema.go @@ -136,6 +136,7 @@ func getSchema(a interface{}) (s ModelSchema, ok bool) { _, f.Approval = tagMap["approval"] _, f.WebCam = tagMap["webcam"] _, f.Stringer = tagMap["stringer"] + _, f.Deprecated = tagMap["deprecated"] f.Min = tagMap["min"] f.Max = tagMap["max"] f.Format = tagMap["format"] diff --git a/openapi.go b/openapi.go index 1e150097..92aa7d29 100644 --- a/openapi.go +++ b/openapi.go @@ -162,6 +162,28 @@ func GenerateOpenAPISchema() { fields[v.Fields[i].Name].Description = v.Fields[i].Help fields[v.Fields[i].Name].Default = v.Fields[i].DefaultValue fields[v.Fields[i].Name].Title = v.Fields[i].DisplayName + fields[v.Fields[i].Name].ReadOnly = v.Fields[i].ReadOnly != "" + fields[v.Fields[i].Name].Pattern = v.Fields[i].Pattern + fields[v.Fields[i].Name].Format = func() string { + switch v.Fields[i].Type { + case cDATE: + return "date-time" + case cPASSWORD: + return "password" + case cEMAIL: + return "email" + case cHTML: + return "html" + default: + return "" + } + }() + fields[v.Fields[i].Name].Deprecated = func() *bool { + if v.Fields[i].Deprecated { + return &v.Fields[i].Deprecated + } + return nil + }() if val, ok := v.Fields[i].Max.(string); ok && val != "" { fields[v.Fields[i].Name].Maximum, _ = strconv.Atoi(val) } @@ -172,6 +194,13 @@ func GenerateOpenAPISchema() { fields[v.Fields[i].Name].AllOf[1].Description = v.Fields[i].Help fields[v.Fields[i].Name].AllOf[1].Default = v.Fields[i].DefaultValue fields[v.Fields[i].Name].AllOf[1].Title = v.Fields[i].DisplayName + fields[v.Fields[i].Name].ReadOnly = v.Fields[i].ReadOnly != "" + fields[v.Fields[i].Name].Deprecated = func() *bool { + if v.Fields[i].Deprecated { + return &v.Fields[i].Deprecated + } + return nil + }() } // Add parameters diff --git a/openapi/schema_object.go b/openapi/schema_object.go index bdae2e6b..7514c92b 100644 --- a/openapi/schema_object.go +++ b/openapi/schema_object.go @@ -10,7 +10,8 @@ type SchemaObject struct { Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` Default string `json:"default,omitempty"` - ReadOnly bool `json:"ReadOnly,omitempty"` + ReadOnly bool `json:"readOnly,omitempty"` + Format string `json:"format,omitempty"` Examples []Example `json:"examples,omitempty"` Items *SchemaObject `json:"items,omitempty"` Properties map[string]*SchemaObject `json:"properties,omitempty"` @@ -22,6 +23,7 @@ type SchemaObject struct { OneOf []*SchemaObject `json:"oneOf,omitempty"` AllOf []*SchemaObject `json:"allOf,omitempty"` Const interface{} `json:"x-const,omitempty"` + Deprecated *bool `json:"deprecated,omitempty"` XFilters []XModifier `json:"x-filter,omitempty"` XAggregator []XModifier `json:"x-aggregator,omitempty"` } diff --git a/schema.go b/schema.go index 07641f09..79f054e1 100644 --- a/schema.go +++ b/schema.go @@ -146,6 +146,7 @@ type F struct { ApprovalID uint WebCam bool Stringer bool + Deprecated bool } // MarshalJSON customizes F json export @@ -191,6 +192,7 @@ func (f F) MarshalJSON() ([]byte, error) { ApprovalID uint WebCam bool Stringer bool + Deprecated bool }{ Name: f.Name, DisplayName: f.DisplayName, @@ -244,6 +246,7 @@ func (f F) MarshalJSON() ([]byte, error) { ApprovalID: f.ApprovalID, WebCam: f.WebCam, Stringer: f.Stringer, + Deprecated: f.Deprecated, }) } From 055825f2b8b6b39530ad46e2c9dae7c4ffee5d5c Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Sun, 27 Nov 2022 12:32:42 +0400 Subject: [PATCH 06/92] add deprecated to model schema --- get_schema.go | 1 + 1 file changed, 1 insertion(+) diff --git a/get_schema.go b/get_schema.go index 212c966d..b4bca0d2 100644 --- a/get_schema.go +++ b/get_schema.go @@ -136,6 +136,7 @@ func getSchema(a interface{}) (s ModelSchema, ok bool) { _, f.Approval = tagMap["approval"] _, f.WebCam = tagMap["webcam"] _, f.Stringer = tagMap["stringer"] + Trail(DEBUG, "deprecated: %v", tagMap["deprecated"]) _, f.Deprecated = tagMap["deprecated"] f.Min = tagMap["min"] f.Max = tagMap["max"] From 8d8454356843b25ad20f53ba4724f6dd9a0b8a62 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Sun, 27 Nov 2022 13:03:38 +0400 Subject: [PATCH 07/92] add deprecated to model schema --- get_schema.go | 1 - openapi.go | 14 ++++++++++++-- openapi/schema_object.go | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/get_schema.go b/get_schema.go index b4bca0d2..212c966d 100644 --- a/get_schema.go +++ b/get_schema.go @@ -136,7 +136,6 @@ func getSchema(a interface{}) (s ModelSchema, ok bool) { _, f.Approval = tagMap["approval"] _, f.WebCam = tagMap["webcam"] _, f.Stringer = tagMap["stringer"] - Trail(DEBUG, "deprecated: %v", tagMap["deprecated"]) _, f.Deprecated = tagMap["deprecated"] f.Min = tagMap["min"] f.Max = tagMap["max"] diff --git a/openapi.go b/openapi.go index 92aa7d29..50c03ea9 100644 --- a/openapi.go +++ b/openapi.go @@ -162,7 +162,12 @@ func GenerateOpenAPISchema() { fields[v.Fields[i].Name].Description = v.Fields[i].Help fields[v.Fields[i].Name].Default = v.Fields[i].DefaultValue fields[v.Fields[i].Name].Title = v.Fields[i].DisplayName - fields[v.Fields[i].Name].ReadOnly = v.Fields[i].ReadOnly != "" + fields[v.Fields[i].Name].ReadOnly = func() *bool { + if val := v.Fields[i].ReadOnly != ""; val { + return &val + } + return nil + }() fields[v.Fields[i].Name].Pattern = v.Fields[i].Pattern fields[v.Fields[i].Name].Format = func() string { switch v.Fields[i].Type { @@ -194,7 +199,12 @@ func GenerateOpenAPISchema() { fields[v.Fields[i].Name].AllOf[1].Description = v.Fields[i].Help fields[v.Fields[i].Name].AllOf[1].Default = v.Fields[i].DefaultValue fields[v.Fields[i].Name].AllOf[1].Title = v.Fields[i].DisplayName - fields[v.Fields[i].Name].ReadOnly = v.Fields[i].ReadOnly != "" + fields[v.Fields[i].Name].ReadOnly = func() *bool { + if val := v.Fields[i].ReadOnly != ""; val { + return &val + } + return nil + }() fields[v.Fields[i].Name].Deprecated = func() *bool { if v.Fields[i].Deprecated { return &v.Fields[i].Deprecated diff --git a/openapi/schema_object.go b/openapi/schema_object.go index 7514c92b..8aa308cf 100644 --- a/openapi/schema_object.go +++ b/openapi/schema_object.go @@ -10,7 +10,7 @@ type SchemaObject struct { Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` Default string `json:"default,omitempty"` - ReadOnly bool `json:"readOnly,omitempty"` + ReadOnly *bool `json:"readOnly,omitempty"` Format string `json:"format,omitempty"` Examples []Example `json:"examples,omitempty"` Items *SchemaObject `json:"items,omitempty"` From e2b5491bd05a4e156414e486f258dcecd516ce66 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Sun, 27 Nov 2022 13:25:19 +0400 Subject: [PATCH 08/92] cange control paprameters for dAPI from array to string --- openapi/generate_schema.go | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/openapi/generate_schema.go b/openapi/generate_schema.go index 27dc7474..d4d8b53c 100644 --- a/openapi/generate_schema.go +++ b/openapi/generate_schema.go @@ -173,10 +173,7 @@ func GenerateBaseSchema() *Schema { Required: false, AllowReserved: true, Schema: &SchemaObject{ - Type: "array", - Items: &SchemaObject{ - Type: "string", - }, + Type: "string", }, Examples: map[string]Example{ "multiColumn": { @@ -192,10 +189,7 @@ func GenerateBaseSchema() *Schema { Required: false, AllowReserved: true, Schema: &SchemaObject{ - Type: "array", - Items: &SchemaObject{ - Type: "string", - }, + Type: "string", }, Examples: map[string]Example{ "multiColumn": { @@ -219,10 +213,7 @@ func GenerateBaseSchema() *Schema { Required: false, AllowReserved: true, Schema: &SchemaObject{ - Type: "array", - Items: &SchemaObject{ - Type: "string", - }, + Type: "string", }, Examples: map[string]Example{ "simple": { @@ -259,10 +250,7 @@ func GenerateBaseSchema() *Schema { Required: false, AllowReserved: true, Schema: &SchemaObject{ - Type: "array", - Items: &SchemaObject{ - Type: "string", - }, + Type: "string", }, Examples: map[string]Example{ "getGroupName": { @@ -355,10 +343,7 @@ func GenerateBaseSchema() *Schema { Required: false, AllowReserved: true, Schema: &SchemaObject{ - Type: "array", - Items: &SchemaObject{ - Type: "string", - }, + Type: "string", }, Examples: map[string]Example{ "simple": { From 9a575541d505bb63d2f1c003665793201513f73e Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Sun, 27 Nov 2022 13:56:11 +0400 Subject: [PATCH 09/92] Add custom tags to OpenAPI schema --- openapi.go | 80 +++++++++++++++----------------------- openapi/generate_schema.go | 2 + openapi/parameter.go | 1 + 3 files changed, 34 insertions(+), 49 deletions(-) diff --git a/openapi.go b/openapi.go index 50c03ea9..5dff28e6 100644 --- a/openapi.go +++ b/openapi.go @@ -33,6 +33,30 @@ func GenerateOpenAPISchema() { required := []string{} parameters := []openapi.Parameter{} writeParameters := []openapi.Parameter{} + + // Add tag to schema if it doesn't exist + tag := "Other" + if v.Category != "" { + tag = v.Category + + // check if it exists + tagExists := false + for i := range s.Tags { + if s.Tags[i].Name == tag { + tagExists = true + break + } + + // if it doesn't exist, add it + if !tagExists { + s.Tags = append(s.Tags, openapi.Tag{ + Name: tag, + Description: "CRUD APIs for " + tag + " models", + }) + } + } + } + for i := range v.Fields { // Determine data type fields[v.Fields[i].Name] = func() *openapi.SchemaObject { @@ -380,13 +404,7 @@ func GenerateOpenAPISchema() { Summary: "Read one " + v.DisplayName, Description: "Read one " + v.DisplayName, Get: &openapi.Operation{ - Tags: []string{func() string { - if v.Category != "" { - return v.Category - } else { - return "Other" - } - }()}, + Tags: []string{tag}, Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " record", @@ -427,13 +445,7 @@ func GenerateOpenAPISchema() { Summary: "Read many " + v.DisplayName, Description: "Read many " + v.DisplayName, Get: &openapi.Operation{ - Tags: []string{func() string { - if v.Category != "" { - return v.Category - } else { - return "Other" - } - }()}, + Tags: []string{tag}, Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " records", @@ -495,13 +507,7 @@ func GenerateOpenAPISchema() { Summary: "Add one " + v.DisplayName, Description: "Add one " + v.DisplayName, Post: &openapi.Operation{ - Tags: []string{func() string { - if v.Category != "" { - return v.Category - } else { - return "Other" - } - }()}, + Tags: []string{tag}, Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " record added", @@ -537,13 +543,7 @@ func GenerateOpenAPISchema() { Summary: "Edit one " + v.DisplayName, Description: "Edit one " + v.DisplayName, Post: &openapi.Operation{ - Tags: []string{func() string { - if v.Category != "" { - return v.Category - } else { - return "Other" - } - }()}, + Tags: []string{tag}, Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " record edited", @@ -578,13 +578,7 @@ func GenerateOpenAPISchema() { Summary: "Edit many " + v.DisplayName, Description: "Edit many " + v.DisplayName, Post: &openapi.Operation{ - Tags: []string{func() string { - if v.Category != "" { - return v.Category - } else { - return "Other" - } - }()}, + Tags: []string{tag}, Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " records edited", @@ -619,13 +613,7 @@ func GenerateOpenAPISchema() { Summary: "Delete one " + v.DisplayName, Description: "Delete one " + v.DisplayName, Post: &openapi.Operation{ - Tags: []string{func() string { - if v.Category != "" { - return v.Category - } else { - return "Other" - } - }()}, + Tags: []string{tag}, Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " record deleted", @@ -660,13 +648,7 @@ func GenerateOpenAPISchema() { Summary: "Delete many " + v.DisplayName, Description: "Delete many " + v.DisplayName, Post: &openapi.Operation{ - Tags: []string{func() string { - if v.Category != "" { - return v.Category - } else { - return "Other" - } - }()}, + Tags: []string{tag}, Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " records deleted", diff --git a/openapi/generate_schema.go b/openapi/generate_schema.go index d4d8b53c..e958d07b 100644 --- a/openapi/generate_schema.go +++ b/openapi/generate_schema.go @@ -233,8 +233,10 @@ func GenerateBaseSchema() *Schema { Required: false, AllowReserved: true, AllowEmptyValue: true, + Default: "", Schema: &SchemaObject{ Type: "string", + Enum: []interface{}{"", "true"}, }, Examples: map[string]Example{ "getDeleted": { diff --git a/openapi/parameter.go b/openapi/parameter.go index 7b8376ef..f179eb38 100644 --- a/openapi/parameter.go +++ b/openapi/parameter.go @@ -15,4 +15,5 @@ type Parameter struct { Example *Example `json:"example,omitempty"` Examples map[string]Example `json:"examples,omitempty"` Content map[string]MediaType `json:"content,omitempty"` + Default interface{} `json:"default,omitempty"` } From ac55711aea19a54e7beb5d26478b986ce5e623b6 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 28 Nov 2022 06:40:11 +0400 Subject: [PATCH 10/92] BUG FIX: Remove duplicate records in OpenAPI tags --- openapi.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openapi.go b/openapi.go index 5dff28e6..e13520e4 100644 --- a/openapi.go +++ b/openapi.go @@ -46,14 +46,14 @@ func GenerateOpenAPISchema() { tagExists = true break } + } - // if it doesn't exist, add it - if !tagExists { - s.Tags = append(s.Tags, openapi.Tag{ - Name: tag, - Description: "CRUD APIs for " + tag + " models", - }) - } + // if it doesn't exist, add it + if !tagExists { + s.Tags = append(s.Tags, openapi.Tag{ + Name: tag, + Description: "CRUD APIs for " + tag + " models", + }) } } From d3891e5d99c6bcbb6d4b75031eeed592b6e9f198 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 28 Nov 2022 15:11:53 +0400 Subject: [PATCH 11/92] add default empty string to parameters in OpenAPI schema --- openapi/generate_schema.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openapi/generate_schema.go b/openapi/generate_schema.go index e958d07b..a1bac3a7 100644 --- a/openapi/generate_schema.go +++ b/openapi/generate_schema.go @@ -172,6 +172,7 @@ func GenerateBaseSchema() *Schema { Description: "Sort the results. Use '-' for descending order and comma for more field", Required: false, AllowReserved: true, + Default: "", Schema: &SchemaObject{ Type: "string", }, @@ -188,6 +189,7 @@ func GenerateBaseSchema() *Schema { Description: "Selecting fields to return in results", Required: false, AllowReserved: true, + Default: "", Schema: &SchemaObject{ Type: "string", }, @@ -212,6 +214,7 @@ func GenerateBaseSchema() *Schema { Description: "Groups rows that have the same values into summary rows", Required: false, AllowReserved: true, + Default: "", Schema: &SchemaObject{ Type: "string", }, @@ -251,6 +254,7 @@ func GenerateBaseSchema() *Schema { Description: "Joins results from another model based on a foreign key", Required: false, AllowReserved: true, + Default: "", Schema: &SchemaObject{ Type: "string", }, @@ -271,6 +275,7 @@ func GenerateBaseSchema() *Schema { Description: "Returns results from M2M fields", Required: false, AllowReserved: true, + Default: "", Schema: &SchemaObject{ Type: "string", Description: "0=don't get, 1/fill=get full records, id=get ids only", @@ -292,6 +297,7 @@ func GenerateBaseSchema() *Schema { Description: "Searches all string fields marked as Searchable", Required: false, AllowReserved: true, + Default: "", Schema: &SchemaObject{ Type: "string", }, @@ -302,6 +308,7 @@ func GenerateBaseSchema() *Schema { Description: "Fills the data from foreign keys", Required: false, AllowReserved: true, + Default: "", Schema: &SchemaObject{ Type: "string", }, @@ -318,6 +325,7 @@ func GenerateBaseSchema() *Schema { Description: "Used in operation `method` to redirect the user to the specified path after the request. Value of `$back` will return the user back to the page", Required: false, AllowReserved: true, + Default: "", Schema: &SchemaObject{ Type: "string", }, @@ -328,6 +336,7 @@ func GenerateBaseSchema() *Schema { Description: "Returns the API call execution time in milliseconds", Required: false, AllowReserved: true, + Default: "", Schema: &SchemaObject{ Type: "string", }, @@ -344,6 +353,7 @@ func GenerateBaseSchema() *Schema { Description: "OR operator with multiple queries in the format of field=value. This `|` is used to separate the query parts and `+` is used for nested `AND` inside the the `OR` statement.", Required: false, AllowReserved: true, + Default: "", Schema: &SchemaObject{ Type: "string", }, From 7d3d5489de87bdcd30b3531aa5092535a29ff754 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 28 Nov 2022 15:14:15 +0400 Subject: [PATCH 12/92] add enums to OpenAPI schema parametes that need it --- openapi/generate_schema.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openapi/generate_schema.go b/openapi/generate_schema.go index a1bac3a7..99ba9991 100644 --- a/openapi/generate_schema.go +++ b/openapi/generate_schema.go @@ -278,6 +278,7 @@ func GenerateBaseSchema() *Schema { Default: "", Schema: &SchemaObject{ Type: "string", + Enum: []interface{}{"0", "fill", "id"}, Description: "0=don't get, 1/fill=get full records, id=get ids only", }, Examples: map[string]Example{ @@ -311,6 +312,7 @@ func GenerateBaseSchema() *Schema { Default: "", Schema: &SchemaObject{ Type: "string", + Enum: []interface{}{"true", "false"}, }, Examples: map[string]Example{ "getDeleted": { @@ -338,6 +340,7 @@ func GenerateBaseSchema() *Schema { AllowReserved: true, Default: "", Schema: &SchemaObject{ + Enum: []interface{}{"true", "false"}, Type: "string", }, Examples: map[string]Example{ From fb182c4a93ff687e1abb85a1803f633f0e60bed0 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 28 Nov 2022 15:24:51 +0400 Subject: [PATCH 13/92] OpenAPI schema: remove default from parameters and add it to schem --- openapi/generate_schema.go | 48 +++++++++++++++++++------------------- openapi/parameter.go | 1 - 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/openapi/generate_schema.go b/openapi/generate_schema.go index 99ba9991..8ff951dc 100644 --- a/openapi/generate_schema.go +++ b/openapi/generate_schema.go @@ -172,9 +172,9 @@ func GenerateBaseSchema() *Schema { Description: "Sort the results. Use '-' for descending order and comma for more field", Required: false, AllowReserved: true, - Default: "", Schema: &SchemaObject{ - Type: "string", + Type: "string", + Default: "", }, Examples: map[string]Example{ "multiColumn": { @@ -189,9 +189,9 @@ func GenerateBaseSchema() *Schema { Description: "Selecting fields to return in results", Required: false, AllowReserved: true, - Default: "", Schema: &SchemaObject{ - Type: "string", + Type: "string", + Default: "", }, Examples: map[string]Example{ "multiColumn": { @@ -214,9 +214,9 @@ func GenerateBaseSchema() *Schema { Description: "Groups rows that have the same values into summary rows", Required: false, AllowReserved: true, - Default: "", Schema: &SchemaObject{ - Type: "string", + Type: "string", + Default: "", }, Examples: map[string]Example{ "simple": { @@ -236,10 +236,10 @@ func GenerateBaseSchema() *Schema { Required: false, AllowReserved: true, AllowEmptyValue: true, - Default: "", Schema: &SchemaObject{ - Type: "string", - Enum: []interface{}{"", "true"}, + Type: "string", + Default: "", + Enum: []interface{}{"", "true"}, }, Examples: map[string]Example{ "getDeleted": { @@ -254,9 +254,9 @@ func GenerateBaseSchema() *Schema { Description: "Joins results from another model based on a foreign key", Required: false, AllowReserved: true, - Default: "", Schema: &SchemaObject{ - Type: "string", + Type: "string", + Default: "", }, Examples: map[string]Example{ "getGroupName": { @@ -275,10 +275,10 @@ func GenerateBaseSchema() *Schema { Description: "Returns results from M2M fields", Required: false, AllowReserved: true, - Default: "", Schema: &SchemaObject{ Type: "string", Enum: []interface{}{"0", "fill", "id"}, + Default: "", Description: "0=don't get, 1/fill=get full records, id=get ids only", }, Examples: map[string]Example{ @@ -298,9 +298,9 @@ func GenerateBaseSchema() *Schema { Description: "Searches all string fields marked as Searchable", Required: false, AllowReserved: true, - Default: "", Schema: &SchemaObject{ - Type: "string", + Type: "string", + Default: "", }, }, "preload": { @@ -309,10 +309,10 @@ func GenerateBaseSchema() *Schema { Description: "Fills the data from foreign keys", Required: false, AllowReserved: true, - Default: "", Schema: &SchemaObject{ - Type: "string", - Enum: []interface{}{"true", "false"}, + Type: "string", + Default: "", + Enum: []interface{}{"true", "false"}, }, Examples: map[string]Example{ "getDeleted": { @@ -327,9 +327,9 @@ func GenerateBaseSchema() *Schema { Description: "Used in operation `method` to redirect the user to the specified path after the request. Value of `$back` will return the user back to the page", Required: false, AllowReserved: true, - Default: "", Schema: &SchemaObject{ - Type: "string", + Type: "string", + Default: "", }, }, "stat": { @@ -338,10 +338,10 @@ func GenerateBaseSchema() *Schema { Description: "Returns the API call execution time in milliseconds", Required: false, AllowReserved: true, - Default: "", Schema: &SchemaObject{ - Enum: []interface{}{"true", "false"}, - Type: "string", + Type: "string", + Default: "", + Enum: []interface{}{"true", "false"}, }, Examples: map[string]Example{ "getDeleted": { @@ -356,9 +356,9 @@ func GenerateBaseSchema() *Schema { Description: "OR operator with multiple queries in the format of field=value. This `|` is used to separate the query parts and `+` is used for nested `AND` inside the the `OR` statement.", Required: false, AllowReserved: true, - Default: "", Schema: &SchemaObject{ - Type: "string", + Type: "string", + Default: "", }, Examples: map[string]Example{ "simple": { diff --git a/openapi/parameter.go b/openapi/parameter.go index f179eb38..7b8376ef 100644 --- a/openapi/parameter.go +++ b/openapi/parameter.go @@ -15,5 +15,4 @@ type Parameter struct { Example *Example `json:"example,omitempty"` Examples map[string]Example `json:"examples,omitempty"` Content map[string]MediaType `json:"content,omitempty"` - Default interface{} `json:"default,omitempty"` } From b4971ff06d9059e16b79ec78445e04c2bea1318a Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 29 Nov 2022 07:46:22 +0400 Subject: [PATCH 14/92] make OpenAPI schema human readable --- openapi.go | 2 +- openapi/schema_object.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi.go b/openapi.go index e13520e4..f6c19785 100644 --- a/openapi.go +++ b/openapi.go @@ -693,7 +693,7 @@ func GenerateOpenAPISchema() { } func getOpenAPIJSON(s *openapi.Schema) []byte { - buf, err := json.Marshal(*s) + buf, err := json.MarshalIndent(*s, "", " ") if err != nil { return nil } diff --git a/openapi/schema_object.go b/openapi/schema_object.go index 8aa308cf..8b4a2304 100644 --- a/openapi/schema_object.go +++ b/openapi/schema_object.go @@ -9,7 +9,7 @@ type SchemaObject struct { Required []string `json:"required,omitempty"` Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` - Default string `json:"default,omitempty"` + Default interface{} `json:"default,omitempty"` ReadOnly *bool `json:"readOnly,omitempty"` Format string `json:"format,omitempty"` Examples []Example `json:"examples,omitempty"` From 63a6c063a7f7e77ae115e1fd40c9d022b8685b62 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 5 Dec 2022 21:30:02 +0400 Subject: [PATCH 15/92] make file and image write parameters in OpenAPI schema binray --- openapi.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openapi.go b/openapi.go index f6c19785..f83d0d16 100644 --- a/openapi.go +++ b/openapi.go @@ -346,10 +346,6 @@ func GenerateOpenAPISchema() { fallthrough case cEMAIL: fallthrough - case cFILE: - fallthrough - case cIMAGE: - fallthrough case cHTML: fallthrough case cLINK: @@ -360,6 +356,13 @@ func GenerateOpenAPISchema() { return &openapi.SchemaObject{ Type: "string", } + case cFILE: + fallthrough + case cIMAGE: + return &openapi.SchemaObject{ + Type: "string", + Format: "binary", + } case cFK: fallthrough case cLIST: From a38fbb04db0c69a4a9ab15f1fd7a3cc39b8b1ffb Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 6 Dec 2022 15:56:16 +0400 Subject: [PATCH 16/92] change OpenAPI schema CSRF to header --- openapi/generate_schema.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openapi/generate_schema.go b/openapi/generate_schema.go index 8ff951dc..51d660d9 100644 --- a/openapi/generate_schema.go +++ b/openapi/generate_schema.go @@ -138,9 +138,9 @@ func GenerateBaseSchema() *Schema { }, }, "CSRF": { - Name: "x-csrf-token", - In: "query", - Description: "Token for CSRF protection which should be set to the session token or JWT token", + Name: "X-CSRF-TOKEN", + In: "header", + Description: "Token for CSRF protection which should be set to the session token", Required: true, Schema: &SchemaObject{ Type: "string", From 34e1ff73a9358c926c6e99f89e4cb896a9535e4f Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 6 Dec 2022 22:33:14 +0400 Subject: [PATCH 17/92] try multipart parse before parse --- auth.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/auth.go b/auth.go index 6587529c..9c093efb 100644 --- a/auth.go +++ b/auth.go @@ -578,7 +578,10 @@ func getSession(r *http.Request) string { return r.FormValue("session") } if r.Method == "POST" { - r.ParseForm() + err := r.ParseMultipartForm(2 << 10) + if err != nil { + r.ParseForm() + } if r.FormValue("session") != "" { return r.FormValue("session") } From e30dec4ade146456c67e49b57b105aa645d1b4b3 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 7 Dec 2022 09:21:02 +0400 Subject: [PATCH 18/92] add empty enum option for OpenAPI schema --- openapi/generate_schema.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openapi/generate_schema.go b/openapi/generate_schema.go index 51d660d9..46225210 100644 --- a/openapi/generate_schema.go +++ b/openapi/generate_schema.go @@ -277,7 +277,7 @@ func GenerateBaseSchema() *Schema { AllowReserved: true, Schema: &SchemaObject{ Type: "string", - Enum: []interface{}{"0", "fill", "id"}, + Enum: []interface{}{"", "0", "fill", "id"}, Default: "", Description: "0=don't get, 1/fill=get full records, id=get ids only", }, @@ -312,7 +312,7 @@ func GenerateBaseSchema() *Schema { Schema: &SchemaObject{ Type: "string", Default: "", - Enum: []interface{}{"true", "false"}, + Enum: []interface{}{"", "true", "false"}, }, Examples: map[string]Example{ "getDeleted": { @@ -341,7 +341,7 @@ func GenerateBaseSchema() *Schema { Schema: &SchemaObject{ Type: "string", Default: "", - Enum: []interface{}{"true", "false"}, + Enum: []interface{}{"", "true", "false"}, }, Examples: map[string]Example{ "getDeleted": { From 10c684a83d362b02d0332f5a8f2282009b78d246 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 7 Dec 2022 22:07:24 +0400 Subject: [PATCH 19/92] business logic now runs on dAPI add and edit --- d_api_add.go | 8 ++++++++ d_api_edit.go | 23 +++++++++++++++++------ d_api_upload.go | 18 +++++++++--------- db_helper.go | 7 +++++++ schema.go | 11 +++++++++++ 5 files changed, 52 insertions(+), 15 deletions(-) diff --git a/d_api_add.go b/d_api_add.go index 41bf8792..0f6d621b 100644 --- a/d_api_add.go +++ b/d_api_add.go @@ -154,6 +154,14 @@ func dAPIAddHandler(w http.ResponseWriter, r *http.Request, s *Session) { createAPIAddLog(q, args, GetDB().Config.NamingStrategy.ColumnName("", model.Type().Name()), createdIDs[i], s, r) } } + // Execute business logic + // if _, ok := model.Addr().Interface().(saver); ok { + // for _, id := range createdIDs { + // model, _ = NewModel(modelName, false) + // Get(model.Addr().Interface(), "id = ?", id) + // model.Addr().Interface().(saver).Save() + // } + // } } else { // Error: Unknown format ReturnJSON(w, r, map[string]interface{}{ diff --git a/d_api_edit.go b/d_api_edit.go index ada2444a..dfdc5eaa 100644 --- a/d_api_edit.go +++ b/d_api_edit.go @@ -103,9 +103,7 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) { q, args := getFilters(r, params, tableName, &schema) modelArray, _ := NewModelArray(modelName, true) - if log { - db.Model(model.Interface()).Where(q, args...).Scan(modelArray.Interface()) - } + db.Model(model.Interface()).Where(q, args...).Scan(modelArray.Interface()) db = db.Model(model.Interface()).Where(q, args...).Updates(writeMap) if db.Error != nil { ReturnJSON(w, r, map[string]interface{}{ @@ -153,12 +151,20 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) { createAPIEditLog(modelName, modelArray.Elem().Index(i).Interface(), &s.User, r) } } + + // Execute business logic + if _, ok := model.Addr().Interface().(saver); ok { + for i := 0; i < modelArray.Len(); i++ { + id := GetID(modelArray.Index(i)) + model, _ = NewModel(modelName, false) + Get(model.Addr().Interface(), "id = ?", id) + model.Addr().Interface().(saver).Save() + } + } } else if len(urlParts) == 3 { // Edit One m, _ := NewModel(modelName, true) - if log { - db.Model(model.Interface()).Where("id = ?", urlParts[2]).Scan(m.Interface()) - } + db.Model(model.Interface()).Where("id = ?", urlParts[2]).Scan(m.Interface()) db = db.Model(model.Interface()).Where("id = ?", urlParts[2]).Updates(writeMap) if db.Error != nil { ReturnJSON(w, r, map[string]interface{}{ @@ -199,6 +205,11 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) { createAPIEditLog(modelName, m.Interface(), &s.User, r) } + // Execute business logic + if modelSaver, ok := m.Interface().(saver); ok { + modelSaver.Save() + } + returnDAPIJSON(w, r, map[string]interface{}{ "status": "ok", "rows_count": rowsAffected, diff --git a/d_api_upload.go b/d_api_upload.go index a5022f4c..dcbb93bf 100644 --- a/d_api_upload.go +++ b/d_api_upload.go @@ -11,22 +11,22 @@ func dAPIUpload(w http.ResponseWriter, r *http.Request, schema *ModelSchema) (ma return fileList, nil } + // make a list of files + kList := []string{} for k := range r.MultipartForm.File { + kList = append(kList, k) + } + + for _, k := range kList { // Process File - // Check if the file is type file or image - var field *F - for i := range schema.Fields { - if schema.Fields[i].ColumnName == k[1:] { - field = &schema.Fields[i] - r.MultipartForm.File[k[1:]] = r.MultipartForm.File[k] - break - } - } + var field *F = schema.FieldByColumnName(k[1:]) if field == nil { Trail(WARNING, "dAPIUpload received a file that has no field: %s", k) continue } + r.MultipartForm.File[k[1:]] = r.MultipartForm.File[k] + s := r.Context().Value(CKey("session")) var session *Session if s != nil { diff --git a/db_helper.go b/db_helper.go index f623b2ff..2406d56a 100644 --- a/db_helper.go +++ b/db_helper.go @@ -9,6 +9,13 @@ func fixQueryEnclosure(v string) string { return v } +func trimEnclosure(v string) string { + if Database.Type == "postgres" { + return strings.ReplaceAll(v, "\"", "") + } + return strings.ReplaceAll(v, "`", "") +} + func columnEnclosure() string { if Database.Type == "postgres" { return "\"" diff --git a/schema.go b/schema.go index 79f054e1..b728a486 100644 --- a/schema.go +++ b/schema.go @@ -39,6 +39,17 @@ func (s ModelSchema) FieldByName(a string) *F { return &F{} } +// FieldByName returns a field from a ModelSchema by name or nil if +// it doesn't exist +func (s ModelSchema) FieldByColumnName(a string) *F { + for i := range s.Fields { + if strings.EqualFold(s.Fields[i].ColumnName, a) { + return &s.Fields[i] + } + } + return nil +} + // GetFormTheme returns the theme for this model or the // global theme if there is no assigned theme for the model func (s *ModelSchema) GetFormTheme() string { From ab11b5fdf357f045c57fc966dcb81159c3398270 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 7 Dec 2022 22:12:02 +0400 Subject: [PATCH 20/92] limit dAPI all models to admin users only --- d_api.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/d_api.go b/d_api.go index 3ab72570..0b46670b 100644 --- a/d_api.go +++ b/d_api.go @@ -132,6 +132,13 @@ func dAPIHandler(w http.ResponseWriter, r *http.Request, s *Session) { return } if urlParts[0] == "$allmodels" { + if !s.User.Admin { + w.WriteHeader(http.StatusForbidden) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "access denied", + }) + } dAPIAllModelsHandler(w, r, s) return } From d1d44356877b196929e77743400c85cad0980e31 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 7 Dec 2022 23:05:17 +0400 Subject: [PATCH 21/92] Add REST-like API to dAPI --- d_api.go | 120 +++++++++++++++++++++++++++++++++--------------- d_api_add.go | 5 +- d_api_delete.go | 10 ++-- d_api_edit.go | 14 +++--- d_api_method.go | 16 +++---- d_api_read.go | 10 ++-- d_api_schema.go | 8 ++-- 7 files changed, 112 insertions(+), 71 deletions(-) diff --git a/d_api.go b/d_api.go index 0b46670b..ea9b9a70 100644 --- a/d_api.go +++ b/d_api.go @@ -3,6 +3,7 @@ package uadmin import ( "context" "net/http" + "strconv" "strings" ) @@ -115,14 +116,33 @@ func dAPIHandler(w http.ResponseWriter, r *http.Request, s *Session) { r.URL.Path = strings.TrimPrefix(r.URL.Path, RootURL+"api/d") r.URL.Path = strings.TrimPrefix(r.URL.Path, "/") + r.URL.Path = strings.TrimSuffix(r.URL.Path, "/") urlParts := strings.Split(r.URL.Path, "/") ctx := context.WithValue(r.Context(), CKey("dAPI"), true) r = r.WithContext(ctx) + // auth dAPI + if urlParts[0] == "auth" { + dAPIAuthHandler(w, r, s) + return + } + + if urlParts[0] == "$allmodels" { + if !s.User.Admin { + w.WriteHeader(http.StatusForbidden) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "access denied", + }) + } + dAPIAllModelsHandler(w, r, s) + return + } + // Check if there is no command and show help - if r.URL.Path == "" || r.URL.Path == "/" || len(urlParts) < 2 { + if r.URL.Path == "" || r.URL.Path == "help" { if s == nil { w.WriteHeader(http.StatusForbidden) ReturnJSON(w, r, map[string]interface{}{ @@ -131,24 +151,8 @@ func dAPIHandler(w http.ResponseWriter, r *http.Request, s *Session) { }) return } - if urlParts[0] == "$allmodels" { - if !s.User.Admin { - w.WriteHeader(http.StatusForbidden) - ReturnJSON(w, r, map[string]interface{}{ - "status": "error", - "err_msg": "access denied", - }) - } - dAPIAllModelsHandler(w, r, s) - return - } - w.Write([]byte(dAPIHelp)) - return - } - // auth dAPI - if urlParts[0] == "auth" { - dAPIAuthHandler(w, r, s) + w.Write([]byte(dAPIHelp)) return } @@ -160,6 +164,15 @@ func dAPIHandler(w http.ResponseWriter, r *http.Request, s *Session) { if urlParts[0] == k { modelExists = true model = v + + // add model to context + ctx := context.WithValue(r.Context(), CKey("modelName"), urlParts[0]) + r = r.WithContext(ctx) + + // trim model name from URL + r.URL.Path = strings.TrimPrefix(r.URL.Path, urlParts[0]) + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/") + break } } @@ -171,69 +184,100 @@ func dAPIHandler(w http.ResponseWriter, r *http.Request, s *Session) { }) return } + //check command commandExists := false - for _, i := range []string{"read", "add", "edit", "delete", "schema", "method"} { - if urlParts[1] == i { - commandExists = true - break + command := "" + secondPartIsANumber := false + if len(urlParts) > 1 { + if _, err := strconv.Atoi(urlParts[1]); err == nil { + secondPartIsANumber = true + } + } + if len(urlParts) > 1 && !secondPartIsANumber { + for _, i := range []string{"read", "add", "edit", "delete", "schema", "method"} { + if urlParts[1] == i { + commandExists = true + command = i + + // trim command from URL + r.URL.Path = strings.TrimPrefix(r.URL.Path, urlParts[1]) + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/") + + break + } + } + } else { + commandExists = true + switch r.Method { + case http.MethodGet: + command = "read" + case http.MethodPost: + command = "add" + case http.MethodPut: + command = "edit" + case http.MethodDelete: + command = "delete" } } + if !commandExists { w.WriteHeader(404) ReturnJSON(w, r, map[string]string{ "status": "error", - "err_msg": "Invalid command (" + urlParts[1] + ")", + "err_msg": "Invalid command (" + command + ")", }) return } - r.URL.Path = strings.TrimSuffix(r.URL.Path, "/") - // Route the request to the correct handler based on the command - if urlParts[1] == "read" { + if command == "read" { + // check if there is a prequery if preQuery, ok := model.(APIPreQueryReader); ok && !preQuery.APIPreQueryRead(w, r) { } else { dAPIReadHandler(w, r, s) } return } - if urlParts[1] == "add" { + if command == "add" { if preQuery, ok := model.(APIPreQueryAdder); ok && !preQuery.APIPreQueryAdd(w, r) { } else { dAPIAddHandler(w, r, s) } + return } - if urlParts[1] == "edit" { + if command == "edit" { // check if there is a prequery if preQuery, ok := model.(APIPreQueryEditor); ok && !preQuery.APIPreQueryEdit(w, r) { } else { dAPIEditHandler(w, r, s) } + return } - if urlParts[1] == "delete" { + if command == "delete" { // check if there is a prequery if preQuery, ok := model.(APIPreQueryDeleter); ok && !preQuery.APIPreQueryDelete(w, r) { } else { dAPIDeleteHandler(w, r, s) } + return } - if urlParts[1] == "schema" { + if command == "schema" { // check if there is a prequery if preQuery, ok := model.(APIPreQuerySchemer); ok && !preQuery.APIPreQuerySchema(w, r) { } else { dAPISchemaHandler(w, r, s) } + return } - if urlParts[1] == "method" { + if command == "method" { dAPIMethodHandler(w, r, s) - } - - if r.URL.Query().Get("$next") != "" { - if strings.HasPrefix(r.URL.Query().Get("$next"), "$back") && r.Header.Get("Referer") != "" { - http.Redirect(w, r, r.Header.Get("Referer")+strings.TrimPrefix(r.URL.Query().Get("$next"), "$back"), http.StatusSeeOther) - } else { - http.Redirect(w, r, r.URL.Query().Get("$next"), http.StatusSeeOther) + if r.URL.Query().Get("$next") != "" { + if strings.HasPrefix(r.URL.Query().Get("$next"), "$back") && r.Header.Get("Referer") != "" { + http.Redirect(w, r, r.Header.Get("Referer")+strings.TrimPrefix(r.URL.Query().Get("$next"), "$back"), http.StatusSeeOther) + } else { + http.Redirect(w, r, r.URL.Query().Get("$next"), http.StatusSeeOther) + } } } } diff --git a/d_api_add.go b/d_api_add.go index 0f6d621b..a2e20662 100644 --- a/d_api_add.go +++ b/d_api_add.go @@ -13,8 +13,7 @@ import ( func dAPIAddHandler(w http.ResponseWriter, r *http.Request, s *Session) { var rowsCount int64 - urlParts := strings.Split(r.URL.Path, "/") - modelName := urlParts[0] + modelName := r.Context().Value(CKey("modelName")).(string) model, _ := NewModel(modelName, false) schema, _ := getSchema(modelName) tableName := schema.TableName @@ -77,7 +76,7 @@ func dAPIAddHandler(w http.ResponseWriter, r *http.Request, s *Session) { params["_"+k] = v } - if len(urlParts) == 2 { + if r.URL.Path == "" { // Add One/Many q, args, m2mFields := getAddFilters(params, &schema) diff --git a/d_api_delete.go b/d_api_delete.go index b4d9b9f8..a69676c4 100644 --- a/d_api_delete.go +++ b/d_api_delete.go @@ -10,7 +10,7 @@ import ( func dAPIDeleteHandler(w http.ResponseWriter, r *http.Request, s *Session) { var rowsCount int64 urlParts := strings.Split(r.URL.Path, "/") - modelName := urlParts[0] + modelName := r.Context().Value(CKey("modelName")).(string) model, _ := NewModel(modelName, false) schema, _ := getSchema(modelName) tableName := schema.TableName @@ -59,7 +59,7 @@ func dAPIDeleteHandler(w http.ResponseWriter, r *http.Request, s *Session) { log = logDeleter.APILogDelete(r) } - if len(urlParts) == 2 { + if r.URL.Path == "" { // Delete Multiple q, args := getFilters(r, params, tableName, &schema) @@ -125,15 +125,15 @@ func dAPIDeleteHandler(w http.ResponseWriter, r *http.Request, s *Session) { "status": "ok", "rows_count": rowsCount, }, params, "delete", model.Interface()) - } else if len(urlParts) == 3 { + } else if len(urlParts) == 1 { // Delete One m, _ := NewModel(modelName, true) db := GetDB() if log { - db.Model(model.Interface()).Where("id = ?", urlParts[2]).Scan(m.Interface()) + db.Model(model.Interface()).Where("id = ?", urlParts[0]).Scan(m.Interface()) } - db = db.Where("id = ?", urlParts[2]).Delete(model.Addr().Interface()) + db = db.Where("id = ?", urlParts[0]).Delete(model.Addr().Interface()) if db.Error != nil { ReturnJSON(w, r, map[string]interface{}{ "status": "error", diff --git a/d_api_edit.go b/d_api_edit.go index dfdc5eaa..33978034 100644 --- a/d_api_edit.go +++ b/d_api_edit.go @@ -9,7 +9,7 @@ import ( func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) { urlParts := strings.Split(r.URL.Path, "/") - modelName := urlParts[0] + modelName := r.Context().Value(CKey("modelName")).(string) model, _ := NewModel(modelName, false) schema, _ := getSchema(modelName) tableName := schema.TableName @@ -98,7 +98,7 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) { db := GetDB() - if len(urlParts) == 2 { + if r.URL.Path == "" { // Edit multiple q, args := getFilters(r, params, tableName, &schema) @@ -161,11 +161,11 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) { model.Addr().Interface().(saver).Save() } } - } else if len(urlParts) == 3 { + } else if len(urlParts) == 1 { // Edit One m, _ := NewModel(modelName, true) - db.Model(model.Interface()).Where("id = ?", urlParts[2]).Scan(m.Interface()) - db = db.Model(model.Interface()).Where("id = ?", urlParts[2]).Updates(writeMap) + db.Model(model.Interface()).Where("id = ?", urlParts[0]).Scan(m.Interface()) + db = db.Model(model.Interface()).Where("id = ?", urlParts[0]).Updates(writeMap) if db.Error != nil { ReturnJSON(w, r, map[string]interface{}{ "status": "error", @@ -185,7 +185,7 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) { sql := sqlDialect[Database.Type]["deleteM2M"] sql = strings.Replace(sql, "{TABLE1}", table1, -1) sql = strings.Replace(sql, "{TABLE2}", table2, -1) - db = db.Exec(sql, urlParts[2]) + db = db.Exec(sql, urlParts[0]) if v == "" { continue @@ -196,7 +196,7 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) { sql = sqlDialect[Database.Type]["insertM2M"] sql = strings.Replace(sql, "{TABLE1}", table1, -1) sql = strings.Replace(sql, "{TABLE2}", table2, -1) - db = db.Exec(sql, urlParts[2], id) + db = db.Exec(sql, urlParts[0], id) } } db.Commit() diff --git a/d_api_method.go b/d_api_method.go index cf7d3299..7f60a544 100644 --- a/d_api_method.go +++ b/d_api_method.go @@ -9,12 +9,12 @@ import ( func dAPIMethodHandler(w http.ResponseWriter, r *http.Request, s *Session) { urlParts := strings.Split(r.URL.Path, "/") - modelName := urlParts[0] + modelName := r.Context().Value(CKey("modelName")).(string) model, _ := NewModel(modelName, true) params := getURLArgs(r) - if len(urlParts) < 4 { + if len(urlParts) < 2 { w.WriteHeader(400) ReturnJSON(w, r, map[string]interface{}{ "status": "error", @@ -31,31 +31,31 @@ func dAPIMethodHandler(w http.ResponseWriter, r *http.Request, s *Session) { return } - f := model.MethodByName(urlParts[2]) + f := model.MethodByName(urlParts[0]) if !f.IsValid() { - f = model.Elem().MethodByName(urlParts[2]) + f = model.Elem().MethodByName(urlParts[0]) } if !f.IsValid() { w.WriteHeader(404) ReturnJSON(w, r, map[string]interface{}{ "status": "error", - "err_msg": "Method (" + urlParts[2] + ") doesn't exist.", + "err_msg": "Method (" + urlParts[0] + ") doesn't exist.", }) return } - Get(model.Interface(), "id = ?", urlParts[3]) + Get(model.Interface(), "id = ?", urlParts[1]) if GetID(model) == 0 { w.WriteHeader(404) ReturnJSON(w, r, map[string]interface{}{ "status": "error", - "err_msg": "ID doesn't exist (" + urlParts[3] + ").", + "err_msg": "ID doesn't exist (" + urlParts[1] + ").", }) return } - ret := model.MethodByName(urlParts[2]).Call([]reflect.Value{}) + ret := model.MethodByName(urlParts[0]).Call([]reflect.Value{}) // Return if the method has a return value if len(ret) != 0 { diff --git a/d_api_read.go b/d_api_read.go index 0f231af3..12cf94ce 100644 --- a/d_api_read.go +++ b/d_api_read.go @@ -11,7 +11,7 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { var rowsCount int64 urlParts := strings.Split(r.URL.Path, "/") - modelName := urlParts[0] + modelName := r.Context().Value(CKey("modelName")).(string) model, _ := NewModel(modelName, false) params := getURLArgs(r) schema, _ := getSchema(modelName) @@ -52,7 +52,7 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { log = logReader.APILogRead(r) } - if len(urlParts) == 2 { + if r.URL.Path == "" { // Read Multiple var m interface{} @@ -193,10 +193,10 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { } }() return - } else if len(urlParts) == 3 { + } else if len(urlParts) == 1 { // Read One m, _ := NewModel(modelName, true) - Get(m.Interface(), "id = ?", urlParts[2]) + Get(m.Interface(), "id = ?", urlParts[0]) rowsCount = 0 var i interface{} @@ -215,7 +215,7 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { }, params, "read", model.Interface()) go func() { if log { - createAPIReadLog(modelName, int(GetID(m)), rowsCount, map[string]string{"id": urlParts[2]}, &s.User, r) + createAPIReadLog(modelName, int(GetID(m)), rowsCount, map[string]string{"id": urlParts[0]}, &s.User, r) } }() } else { diff --git a/d_api_schema.go b/d_api_schema.go index 28377316..d7778fee 100644 --- a/d_api_schema.go +++ b/d_api_schema.go @@ -3,13 +3,11 @@ package uadmin import ( "encoding/json" "net/http" - "strings" ) func dAPISchemaHandler(w http.ResponseWriter, r *http.Request, s *Session) { - urlParts := strings.Split(r.URL.Path, "/") - model, _ := NewModel(urlParts[0], false) - modelName := GetDB().Config.NamingStrategy.ColumnName("", model.Type().Name()) + modelName := r.Context().Value(CKey("modelName")).(string) + model, _ := NewModel(modelName, false) params := getURLArgs(r) // Check permission @@ -40,7 +38,7 @@ func dAPISchemaHandler(w http.ResponseWriter, r *http.Request, s *Session) { return } - schema, _ := getSchema(urlParts[0]) + schema, _ := getSchema(modelName) // Get Language lang := r.URL.Query().Get("language") From c54b29de4de1e1e1f65d75d141e1c9198e961624 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 8 Dec 2022 00:17:34 +0400 Subject: [PATCH 22/92] OpenAPI schema exports only REST-like APIs from dAPI --- d_api_helper.go | 18 -- openapi.go | 465 +++++++++++++++++++++++++----------------------- 2 files changed, 238 insertions(+), 245 deletions(-) diff --git a/d_api_helper.go b/d_api_helper.go index 62ce8239..37f64420 100644 --- a/d_api_helper.go +++ b/d_api_helper.go @@ -66,24 +66,6 @@ func getURLArgs(r *http.Request) map[string]string { return params } -// type dbScanner struct { -// Value interface{} -// } - -// func (d *dbScanner) Scan(src interface{}) error { -// d.Value = src -// return nil -// } - -// func makeResultReceiver(length int) []interface{} { -// result := make([]interface{}, 0, length) -// for i := 0; i < length; i++ { -// current := dbScanner{} -// result = append(result, ¤t.Value) -// } -// return result -// } - func getFilters(r *http.Request, params map[string]string, tableName string, schema *ModelSchema) (query string, args []interface{}) { qParts := []string{} args = []interface{}{} diff --git a/openapi.go b/openapi.go index f83d0d16..24bf68ea 100644 --- a/openapi.go +++ b/openapi.go @@ -32,7 +32,7 @@ func GenerateOpenAPISchema() { fields := map[string]*openapi.SchemaObject{} required := []string{} parameters := []openapi.Parameter{} - writeParameters := []openapi.Parameter{} + writeParameters := map[string]*openapi.SchemaObject{} // Add tag to schema if it doesn't exist tag := "Other" @@ -327,73 +327,66 @@ func GenerateOpenAPISchema() { continue } - writeParameters = append(writeParameters, func() openapi.Parameter { - return openapi.Parameter{ - Name: func() string { - if v.Fields[i].Type == cFK { - return "_" + v.Fields[i].ColumnName + "_id" - } else { - return "_" + v.Fields[i].ColumnName - } - }(), - In: "query", - Description: "Set value for " + v.Fields[i].DisplayName, - Schema: func() *openapi.SchemaObject { - switch v.Fields[i].Type { - case cSTRING: - fallthrough - case cCODE: - fallthrough - case cEMAIL: - fallthrough - case cHTML: - fallthrough - case cLINK: - fallthrough - case cMULTILINGUAL: - fallthrough - case cPASSWORD: - return &openapi.SchemaObject{ - Type: "string", - } - case cFILE: - fallthrough - case cIMAGE: - return &openapi.SchemaObject{ - Type: "string", - Format: "binary", - } - case cFK: - fallthrough - case cLIST: - fallthrough - case cMONEY: - return &openapi.SchemaObject{ - Type: "integer", - } - case cNUMBER: - fallthrough - case cPROGRESSBAR: - return &openapi.SchemaObject{ - Type: "number", - } - case cBOOL: - return &openapi.SchemaObject{ - Type: "boolean", - } - case cDATE: - return &openapi.SchemaObject{ - Type: "string", - } - default: - return &openapi.SchemaObject{ - Type: "string", - } - } - }(), + writeParameterName := func() string { + if v.Fields[i].Type == cFK { + return "_" + v.Fields[i].ColumnName + "_id" + } else { + return "_" + v.Fields[i].ColumnName } - }(), - ) + }() + writeParameters[writeParameterName] = func() *openapi.SchemaObject { + switch v.Fields[i].Type { + case cSTRING: + fallthrough + case cCODE: + fallthrough + case cEMAIL: + fallthrough + case cHTML: + fallthrough + case cLINK: + fallthrough + case cMULTILINGUAL: + fallthrough + case cPASSWORD: + return &openapi.SchemaObject{ + Type: "string", + } + case cFILE: + fallthrough + case cIMAGE: + return &openapi.SchemaObject{ + Type: "string", + Format: "binary", + } + case cFK: + fallthrough + case cLIST: + fallthrough + case cMONEY: + return &openapi.SchemaObject{ + Type: "integer", + } + case cNUMBER: + fallthrough + case cPROGRESSBAR: + return &openapi.SchemaObject{ + Type: "number", + } + case cBOOL: + return &openapi.SchemaObject{ + Type: "boolean", + } + case cDATE: + return &openapi.SchemaObject{ + Type: "string", + } + default: + return &openapi.SchemaObject{ + Type: "string", + } + } + }() // Add required fields if v.Fields[i].Required { @@ -403,11 +396,13 @@ func GenerateOpenAPISchema() { // Add dAPI paths // Read one - s.Paths[fmt.Sprintf("/api/d/%s/read/{id}", v.ModelName)] = openapi.Path{ - Summary: "Read one " + v.DisplayName, - Description: "Read one " + v.DisplayName, + s.Paths[fmt.Sprintf("/api/d/%s/{id}", v.ModelName)] = openapi.Path{ + Summary: "Single record operations for " + v.DisplayName, + Description: "Single record operations for " + v.DisplayName, Get: &openapi.Operation{ - Tags: []string{tag}, + Tags: []string{tag}, + Summary: "Read one record from " + v.DisplayName, + Description: "Read one record from " + v.DisplayName, Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " record", @@ -424,105 +419,78 @@ func GenerateOpenAPISchema() { }, }, }, - }, - Parameters: []openapi.Parameter{ - { - Ref: "#/components/parameters/PathID", - }, - { - Ref: "#/components/parameters/deleted", - }, - { - Ref: "#/components/parameters/m2m", - }, - { - Ref: "#/components/parameters/preload", - }, - { - Ref: "#/components/parameters/stat", + Parameters: []openapi.Parameter{ + { + Ref: "#/components/parameters/PathID", + }, + { + Ref: "#/components/parameters/deleted", + }, + { + Ref: "#/components/parameters/m2m", + }, + { + Ref: "#/components/parameters/preload", + }, + { + Ref: "#/components/parameters/stat", + }, }, }, - } - // Read Many - s.Paths[fmt.Sprintf("/api/d/%s/read", v.ModelName)] = openapi.Path{ - Summary: "Read many " + v.DisplayName, - Description: "Read many " + v.DisplayName, - Get: &openapi.Operation{ - Tags: []string{tag}, + Put: &openapi.Operation{ + Tags: []string{tag}, + Summary: "Edit one record from " + v.DisplayName, + Description: "Edit one record from " + v.DisplayName, + RequestBody: &openapi.RequestBody{ + Content: map[string]openapi.MediaType{ + "multipart/form-data": { + Schema: &openapi.SchemaObject{ + Type: "Object", + Properties: writeParameters, + }, + }, + }, + }, Responses: map[string]openapi.Response{ "200": { - Description: v.DisplayName + " records", + Description: v.DisplayName + " record edited", Content: map[string]openapi.MediaType{ "application/json": { Schema: &openapi.SchemaObject{ Type: "object", Properties: map[string]*openapi.SchemaObject{ - "result": { - Type: "array", - Items: &openapi.SchemaObject{Ref: "#/components/schemas/" + v.Name}, - }, - "status": {Type: "string"}, + "rows_count": {Type: "integer"}, + "status": {Type: "string"}, }, }, }, }, }, }, - }, - Parameters: append(parameters, []openapi.Parameter{ - { - Ref: "#/components/parameters/limit", - }, - { - Ref: "#/components/parameters/offset", - }, - { - Ref: "#/components/parameters/order", - }, - { - Ref: "#/components/parameters/fields", - }, - { - Ref: "#/components/parameters/groupBy", - }, - { - Ref: "#/components/parameters/deleted", - }, - { - Ref: "#/components/parameters/join", - }, - { - Ref: "#/components/parameters/m2m", - }, - { - Ref: "#/components/parameters/q", - }, - { - Ref: "#/components/parameters/stat", - }, - { - Ref: "#/components/parameters/or", + Parameters: []openapi.Parameter{ + { + Ref: "#/components/parameters/PathID", + }, + { + Ref: "#/components/parameters/CSRF", + }, + { + Ref: "#/components/parameters/stat", + }, }, - }...), - } - // Add One - s.Paths[fmt.Sprintf("/api/d/%s/add", v.ModelName)] = openapi.Path{ - Summary: "Add one " + v.DisplayName, - Description: "Add one " + v.DisplayName, - Post: &openapi.Operation{ - Tags: []string{tag}, + }, + Delete: &openapi.Operation{ + Tags: []string{tag}, + Summary: "Delete one " + v.DisplayName, + Description: "Delete one " + v.DisplayName, Responses: map[string]openapi.Response{ "200": { - Description: v.DisplayName + " record added", + Description: v.DisplayName + " record deleted", Content: map[string]openapi.MediaType{ "application/json": { Schema: &openapi.SchemaObject{ Type: "object", Properties: map[string]*openapi.SchemaObject{ - "id": { - Type: "array", - Items: &openapi.SchemaObject{Type: "integer"}, - }, "rows_count": {Type: "integer"}, "status": {Type: "string"}, }, @@ -531,65 +499,108 @@ func GenerateOpenAPISchema() { }, }, }, - }, - Parameters: append([]openapi.Parameter{ - { - Ref: "#/components/parameters/CSRF", - }, - { - Ref: "#/components/parameters/stat", + Parameters: []openapi.Parameter{ + { + Ref: "#/components/parameters/PathID", + }, + { + Ref: "#/components/parameters/CSRF", + }, + { + Ref: "#/components/parameters/stat", + }, }, - }, writeParameters...), + }, } - // Edit One - s.Paths[fmt.Sprintf("/api/d/%s/edit/{id}", v.ModelName)] = openapi.Path{ - Summary: "Edit one " + v.DisplayName, - Description: "Edit one " + v.DisplayName, - Post: &openapi.Operation{ - Tags: []string{tag}, + // Read Many + s.Paths[fmt.Sprintf("/api/d/%s", v.ModelName)] = openapi.Path{ + Summary: "Add one and multi-record operations for " + v.DisplayName, + Description: "Add one and multi-record operations for " + v.DisplayName, + Get: &openapi.Operation{ + Tags: []string{tag}, + Summary: "Read many records from " + v.DisplayName, + Description: "Read many records from " + v.DisplayName, Responses: map[string]openapi.Response{ "200": { - Description: v.DisplayName + " record edited", + Description: v.DisplayName + " records", Content: map[string]openapi.MediaType{ "application/json": { Schema: &openapi.SchemaObject{ Type: "object", Properties: map[string]*openapi.SchemaObject{ - "rows_count": {Type: "integer"}, - "status": {Type: "string"}, + "result": { + Type: "array", + Items: &openapi.SchemaObject{Ref: "#/components/schemas/" + v.Name}, + }, + "status": {Type: "string"}, }, }, }, }, }, }, + Parameters: append(parameters, []openapi.Parameter{ + { + Ref: "#/components/parameters/limit", + }, + { + Ref: "#/components/parameters/offset", + }, + { + Ref: "#/components/parameters/order", + }, + { + Ref: "#/components/parameters/fields", + }, + { + Ref: "#/components/parameters/groupBy", + }, + { + Ref: "#/components/parameters/deleted", + }, + { + Ref: "#/components/parameters/join", + }, + { + Ref: "#/components/parameters/m2m", + }, + { + Ref: "#/components/parameters/q", + }, + { + Ref: "#/components/parameters/stat", + }, + { + Ref: "#/components/parameters/or", + }, + }...), }, - Parameters: append([]openapi.Parameter{ - { - Ref: "#/components/parameters/PathID", - }, - { - Ref: "#/components/parameters/CSRF", - }, - { - Ref: "#/components/parameters/stat", - }, - }, writeParameters...), - } - // Edit Many - s.Paths[fmt.Sprintf("/api/d/%s/edit", v.ModelName)] = openapi.Path{ - Summary: "Edit many " + v.DisplayName, - Description: "Edit many " + v.DisplayName, Post: &openapi.Operation{ - Tags: []string{tag}, + Tags: []string{tag}, + Summary: "Add one " + v.DisplayName, + Description: "Add one " + v.DisplayName, + RequestBody: &openapi.RequestBody{ + Content: map[string]openapi.MediaType{ + "multipart/form-data": { + Schema: &openapi.SchemaObject{ + Type: "Object", + Properties: writeParameters, + }, + }, + }, + }, Responses: map[string]openapi.Response{ "200": { - Description: v.DisplayName + " records edited", + Description: v.DisplayName + " record added", Content: map[string]openapi.MediaType{ "application/json": { Schema: &openapi.SchemaObject{ Type: "object", Properties: map[string]*openapi.SchemaObject{ + "id": { + Type: "array", + Items: &openapi.SchemaObject{Type: "integer"}, + }, "rows_count": {Type: "integer"}, "status": {Type: "string"}, }, @@ -598,28 +609,32 @@ func GenerateOpenAPISchema() { }, }, }, - }, - Parameters: append([]openapi.Parameter{ - { - Ref: "#/components/parameters/PathID", - }, - { - Ref: "#/components/parameters/CSRF", + Parameters: []openapi.Parameter{ + { + Ref: "#/components/parameters/CSRF", + }, + { + Ref: "#/components/parameters/stat", + }, }, - { - Ref: "#/components/parameters/stat", + }, + Put: &openapi.Operation{ + Tags: []string{tag}, + Summary: "Edit many " + v.DisplayName, + Description: "Edit many " + v.DisplayName, + RequestBody: &openapi.RequestBody{ + Content: map[string]openapi.MediaType{ + "multipart/form-data": { + Schema: &openapi.SchemaObject{ + Type: "Object", + Properties: writeParameters, + }, + }, + }, }, - }, append(writeParameters, parameters...)...), - } - // Delete One - s.Paths[fmt.Sprintf("/api/d/%s/delete/{id}", v.ModelName)] = openapi.Path{ - Summary: "Delete one " + v.DisplayName, - Description: "Delete one " + v.DisplayName, - Post: &openapi.Operation{ - Tags: []string{tag}, Responses: map[string]openapi.Response{ "200": { - Description: v.DisplayName + " record deleted", + Description: v.DisplayName + " records edited", Content: map[string]openapi.MediaType{ "application/json": { Schema: &openapi.SchemaObject{ @@ -633,25 +648,22 @@ func GenerateOpenAPISchema() { }, }, }, + Parameters: append([]openapi.Parameter{ + { + Ref: "#/components/parameters/PathID", + }, + { + Ref: "#/components/parameters/CSRF", + }, + { + Ref: "#/components/parameters/stat", + }, + }, parameters...), }, - Parameters: []openapi.Parameter{ - { - Ref: "#/components/parameters/PathID", - }, - { - Ref: "#/components/parameters/CSRF", - }, - { - Ref: "#/components/parameters/stat", - }, - }, - } - // Delete Many - s.Paths[fmt.Sprintf("/api/d/%s/delete", v.ModelName)] = openapi.Path{ - Summary: "Delete many " + v.DisplayName, - Description: "Delete many " + v.DisplayName, - Post: &openapi.Operation{ - Tags: []string{tag}, + Delete: &openapi.Operation{ + Tags: []string{tag}, + Summary: "Delete many " + v.DisplayName, + Description: "Delete many " + v.DisplayName, Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " records deleted", @@ -668,17 +680,16 @@ func GenerateOpenAPISchema() { }, }, }, + Parameters: append([]openapi.Parameter{ + { + Ref: "#/components/parameters/CSRF", + }, + { + Ref: "#/components/parameters/stat", + }, + }, parameters...), }, - Parameters: append([]openapi.Parameter{ - { - Ref: "#/components/parameters/CSRF", - }, - { - Ref: "#/components/parameters/stat", - }, - }, parameters...), } - s.Components.Schemas[v.Name] = openapi.SchemaObject{ Type: "object", Properties: fields, From 822db2834efbb6ac1c95be0831735dfdbe3b0c4d Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 8 Dec 2022 00:33:38 +0400 Subject: [PATCH 23/92] improve OpenAPI operation names --- openapi.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/openapi.go b/openapi.go index 24bf68ea..e399a316 100644 --- a/openapi.go +++ b/openapi.go @@ -401,8 +401,8 @@ func GenerateOpenAPISchema() { Description: "Single record operations for " + v.DisplayName, Get: &openapi.Operation{ Tags: []string{tag}, - Summary: "Read one record from " + v.DisplayName, - Description: "Read one record from " + v.DisplayName, + Summary: "Read one " + v.DisplayName + " record", + Description: "Read one " + v.DisplayName + " record", Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " record", @@ -439,8 +439,8 @@ func GenerateOpenAPISchema() { }, Put: &openapi.Operation{ Tags: []string{tag}, - Summary: "Edit one record from " + v.DisplayName, - Description: "Edit one record from " + v.DisplayName, + Summary: "Edit one " + v.DisplayName + " record", + Description: "Edit one " + v.DisplayName + " record", RequestBody: &openapi.RequestBody{ Content: map[string]openapi.MediaType{ "multipart/form-data": { @@ -481,8 +481,8 @@ func GenerateOpenAPISchema() { }, Delete: &openapi.Operation{ Tags: []string{tag}, - Summary: "Delete one " + v.DisplayName, - Description: "Delete one " + v.DisplayName, + Summary: "Delete one " + v.DisplayName + " record", + Description: "Delete one " + v.DisplayName + " record", Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " record deleted", @@ -518,8 +518,8 @@ func GenerateOpenAPISchema() { Description: "Add one and multi-record operations for " + v.DisplayName, Get: &openapi.Operation{ Tags: []string{tag}, - Summary: "Read many records from " + v.DisplayName, - Description: "Read many records from " + v.DisplayName, + Summary: "Read many " + v.DisplayName + " record", + Description: "Read many " + v.DisplayName + " record", Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " records", @@ -577,8 +577,8 @@ func GenerateOpenAPISchema() { }, Post: &openapi.Operation{ Tags: []string{tag}, - Summary: "Add one " + v.DisplayName, - Description: "Add one " + v.DisplayName, + Summary: "Add one " + v.DisplayName + " record", + Description: "Add one " + v.DisplayName + " record", RequestBody: &openapi.RequestBody{ Content: map[string]openapi.MediaType{ "multipart/form-data": { @@ -620,8 +620,8 @@ func GenerateOpenAPISchema() { }, Put: &openapi.Operation{ Tags: []string{tag}, - Summary: "Edit many " + v.DisplayName, - Description: "Edit many " + v.DisplayName, + Summary: "Edit many " + v.DisplayName + " record", + Description: "Edit many " + v.DisplayName + " record", RequestBody: &openapi.RequestBody{ Content: map[string]openapi.MediaType{ "multipart/form-data": { @@ -662,8 +662,8 @@ func GenerateOpenAPISchema() { }, Delete: &openapi.Operation{ Tags: []string{tag}, - Summary: "Delete many " + v.DisplayName, - Description: "Delete many " + v.DisplayName, + Summary: "Delete many " + v.DisplayName + " record", + Description: "Delete many " + v.DisplayName + " record", Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " records deleted", From 1e065f9438eed0fa2e98048a9a3e0e26734196ee Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 8 Dec 2022 01:01:10 +0400 Subject: [PATCH 24/92] fix type in OpenAPI schema --- openapi.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openapi.go b/openapi.go index e399a316..d4930c91 100644 --- a/openapi.go +++ b/openapi.go @@ -445,7 +445,7 @@ func GenerateOpenAPISchema() { Content: map[string]openapi.MediaType{ "multipart/form-data": { Schema: &openapi.SchemaObject{ - Type: "Object", + Type: "object", Properties: writeParameters, }, }, @@ -583,7 +583,7 @@ func GenerateOpenAPISchema() { Content: map[string]openapi.MediaType{ "multipart/form-data": { Schema: &openapi.SchemaObject{ - Type: "Object", + Type: "object", Properties: writeParameters, }, }, @@ -626,7 +626,7 @@ func GenerateOpenAPISchema() { Content: map[string]openapi.MediaType{ "multipart/form-data": { Schema: &openapi.SchemaObject{ - Type: "Object", + Type: "object", Properties: writeParameters, }, }, From 0081c18b2c059ac8e055418173c11443bfe781ec Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 8 Dec 2022 01:17:40 +0400 Subject: [PATCH 25/92] fix type in OpenAPI schema --- openapi.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/openapi.go b/openapi.go index d4930c91..2d969d08 100644 --- a/openapi.go +++ b/openapi.go @@ -518,8 +518,8 @@ func GenerateOpenAPISchema() { Description: "Add one and multi-record operations for " + v.DisplayName, Get: &openapi.Operation{ Tags: []string{tag}, - Summary: "Read many " + v.DisplayName + " record", - Description: "Read many " + v.DisplayName + " record", + Summary: "Read many " + v.DisplayName + " records", + Description: "Read many " + v.DisplayName + " records", Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " records", @@ -620,8 +620,8 @@ func GenerateOpenAPISchema() { }, Put: &openapi.Operation{ Tags: []string{tag}, - Summary: "Edit many " + v.DisplayName + " record", - Description: "Edit many " + v.DisplayName + " record", + Summary: "Edit many " + v.DisplayName + " records", + Description: "Edit many " + v.DisplayName + " records", RequestBody: &openapi.RequestBody{ Content: map[string]openapi.MediaType{ "multipart/form-data": { @@ -649,9 +649,6 @@ func GenerateOpenAPISchema() { }, }, Parameters: append([]openapi.Parameter{ - { - Ref: "#/components/parameters/PathID", - }, { Ref: "#/components/parameters/CSRF", }, @@ -662,8 +659,8 @@ func GenerateOpenAPISchema() { }, Delete: &openapi.Operation{ Tags: []string{tag}, - Summary: "Delete many " + v.DisplayName + " record", - Description: "Delete many " + v.DisplayName + " record", + Summary: "Delete many " + v.DisplayName + " records", + Description: "Delete many " + v.DisplayName + " records", Responses: map[string]openapi.Response{ "200": { Description: v.DisplayName + " records deleted", From 868d5f201047d0054d3ebd945f5ccad8e51c976c Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 8 Dec 2022 01:26:50 +0400 Subject: [PATCH 26/92] enable business logic for dAPI add --- d_api_add.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/d_api_add.go b/d_api_add.go index a2e20662..1eff0702 100644 --- a/d_api_add.go +++ b/d_api_add.go @@ -154,13 +154,13 @@ func dAPIAddHandler(w http.ResponseWriter, r *http.Request, s *Session) { } } // Execute business logic - // if _, ok := model.Addr().Interface().(saver); ok { - // for _, id := range createdIDs { - // model, _ = NewModel(modelName, false) - // Get(model.Addr().Interface(), "id = ?", id) - // model.Addr().Interface().(saver).Save() - // } - // } + if _, ok := model.Addr().Interface().(saver); ok { + for _, id := range createdIDs { + model, _ = NewModel(modelName, false) + Get(model.Addr().Interface(), "id = ?", id) + model.Addr().Interface().(saver).Save() + } + } } else { // Error: Unknown format ReturnJSON(w, r, map[string]interface{}{ From da29c302731126f790c99e65b5c164fab4ef29b7 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 8 Dec 2022 21:29:27 +0400 Subject: [PATCH 27/92] Add a custom email sender handler --- global.go | 8 ++++++++ send_email.go | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/global.go b/global.go index 044e8ce9..9e5f6cde 100644 --- a/global.go +++ b/global.go @@ -418,6 +418,14 @@ Regards, {WEBSITE} Support ` +// CustomEmailHandler allows to customize or even override the default email sending method. +// The return of of the function is a boolean and an error. All parameters are of pointers to +// allow customization of the parameters that will be passed to the default email sender. +// - The boolean will determine whether to proceed or not. If true, the process will proceed with +// the default method of sending the email +// - The error will be reported to Trail as type uadmin.ERROR +var CustomEmailHandler func(to, cc, bcc *[]string, subject, body *string, attachments ...*string) (bool, error) + // Private Global Variables // Regex var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") diff --git a/send_email.go b/send_email.go index d105498e..65131fa1 100644 --- a/send_email.go +++ b/send_email.go @@ -14,6 +14,20 @@ import ( // SendEmail sends email using system configured variables func SendEmail(to, cc, bcc []string, subject, body string, attachments ...string) (err error) { + if CustomEmailHandler != nil { + attachmentsPointers := make([]*string, len(attachments)) + for i := range attachments { + attachmentsPointers[i] = &attachments[i] + } + var proceed bool + proceed, err = CustomEmailHandler(&to, &cc, &bcc, &subject, &body, attachmentsPointers...) + if err != nil { + Trail(ERROR, "Error in CustomEmailHandler. %s", err) + if !proceed { + return + } + } + } if EmailFrom == "" || EmailUsername == "" || EmailPassword == "" || EmailSMTPServer == "" || EmailSMTPServerPort == 0 { errMsg := "Email not sent because email global variables are not set" Trail(WARNING, errMsg) From d73b671eb6d38829de503741430ff906d2ace560 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 12 Dec 2022 08:38:50 +0400 Subject: [PATCH 28/92] add CORS support for API --- admin.go | 5 +++++ cors_handler.go | 37 +++++++++++++++++++++++++++++++++++++ d_api.go | 5 +++++ d_api_add.go | 3 ++- global.go | 13 +++++++++++++ register.go | 6 +++++- 6 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 cors_handler.go diff --git a/admin.go b/admin.go index 8456fbf9..1f11fb44 100644 --- a/admin.go +++ b/admin.go @@ -180,6 +180,11 @@ func ReturnJSON(w http.ResponseWriter, r *http.Request, v interface{}) { w.Write(b) return } + + if CustomizeJSON != nil { + b = CustomizeJSON(w, r, v, b) + } + w.Write(b) } diff --git a/cors_handler.go b/cors_handler.go new file mode 100644 index 00000000..d2c00ae5 --- /dev/null +++ b/cors_handler.go @@ -0,0 +1,37 @@ +package uadmin + +import ( + "net/http" + "strings" +) + +func CORSHandler(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // Add CORS headers + if len(AllowedCORSOrigins) == 0 { + if r.Header.Get("Origin") != "" { + w.Header().Add("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Add("Access-Control-Allow-Credentials", "true") + } else { + w.Header().Add("Access-Control-Allow-Origin", "*") + } + } else { + w.Header().Add("Access-Control-Allow-Origin", strings.Join(AllowedCORSOrigins, ", ")) + w.Header().Add("Access-Control-Allow-Credentials", "true") + } + + // Handle preflight requests + if r.Method == http.MethodOptions { + // Allow all known methods + w.Header().Add("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, HEAD") + + // Allow requested headers + w.Header().Add("Access-Control-Allow-Headers", r.Header.Get("Access-Control-Request-Headers")) + + return + } + + // run the handler + f(w, r) + } +} diff --git a/d_api.go b/d_api.go index ea9b9a70..be8ba959 100644 --- a/d_api.go +++ b/d_api.go @@ -114,6 +114,11 @@ func dAPIHandler(w http.ResponseWriter, r *http.Request, s *Session) { r.ParseForm() } + // Add Custom headers + for k, v := range CustomDAPIHeaders { + w.Header().Add(k, v) + } + r.URL.Path = strings.TrimPrefix(r.URL.Path, RootURL+"api/d") r.URL.Path = strings.TrimPrefix(r.URL.Path, "/") r.URL.Path = strings.TrimSuffix(r.URL.Path, "/") diff --git a/d_api_add.go b/d_api_add.go index 1eff0702..619093fc 100644 --- a/d_api_add.go +++ b/d_api_add.go @@ -92,7 +92,8 @@ func dAPIAddHandler(w http.ResponseWriter, r *http.Request, s *Session) { argsPlaceHolder = append(argsPlaceHolder, "?") } - db = db.Exec("INSERT INTO "+tableName+" ("+q[i]+") VALUES ("+strings.Join(argsPlaceHolder, ",")+")", args[i]...) + SQL := "INSERT INTO " + tableName + " (" + q[i] + ") VALUES (" + strings.Join(argsPlaceHolder, ",") + ")" + db = db.Exec(SQL, args[i]...) rowsCount += db.RowsAffected } id := []int{} diff --git a/global.go b/global.go index 9e5f6cde..03b9e127 100644 --- a/global.go +++ b/global.go @@ -1,6 +1,7 @@ package uadmin import ( + "net/http" "os" "regexp" ) @@ -426,6 +427,18 @@ Regards, // - The error will be reported to Trail as type uadmin.ERROR var CustomEmailHandler func(to, cc, bcc *[]string, subject, body *string, attachments ...*string) (bool, error) +// CustomDAPIHeaders are extra handlers that would be added to dAPI responses +var CustomDAPIHeaders = map[string]string{} + +// EnableDAPICORS controller whether dAPI is uses CORS protocol to allow cross-origan requests +var EnableDAPICORS bool + +// AllowedCORSOrigins is a list of allowed CORS origins +var AllowedCORSOrigins []string + +// CustomizeJSON is a function to allow customization of JSON returns +var CustomizeJSON func(http.ResponseWriter, *http.Request, interface{}, []byte) []byte + // Private Global Variables // Regex var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") diff --git a/register.go b/register.go index 2cf25253..caea0246 100644 --- a/register.go +++ b/register.go @@ -371,7 +371,11 @@ func registerHandlers() { } // dAPI handler - http.HandleFunc(RootURL+"api/", Handler(apiHandler)) + if EnableDAPICORS { + http.HandleFunc(RootURL+"api/", CORSHandler(Handler(apiHandler))) + } else { + http.HandleFunc(RootURL+"api/", Handler(apiHandler)) + } handlersRegistered = true } From 15180a9c2e7c4d9aec402fe5bde119f7848145dd Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 12 Dec 2022 09:10:19 +0400 Subject: [PATCH 29/92] support multiple domains in CORS --- cors_handler.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cors_handler.go b/cors_handler.go index d2c00ae5..24cb2bd6 100644 --- a/cors_handler.go +++ b/cors_handler.go @@ -16,8 +16,21 @@ func CORSHandler(f func(w http.ResponseWriter, r *http.Request)) func(w http.Res w.Header().Add("Access-Control-Allow-Origin", "*") } } else { - w.Header().Add("Access-Control-Allow-Origin", strings.Join(AllowedCORSOrigins, ", ")) + // allowedOrigin := false + // for i := range AllowedCORSOrigins { + // if strings.EqualFold(r.Header.Get("Origin"), AllowedCORSOrigins[i]) { + // allowedOrigin = true + // break + // } + // } + // if allowedOrigin { + // w.Header().Add("Access-Control-Allow-Origin", r.Header.Get("Origin")) + // w.Header().Add("Access-Control-Allow-Credentials", "true") + // } else { + w.Header().Add("Access-Control-Allow-Origin", strings.Join(AllowedCORSOrigins, "|")) w.Header().Add("Access-Control-Allow-Credentials", "true") + // } + } // Handle preflight requests From b49258f27489d18f3d7f134ab2c2c7825d7eb5ab Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 12 Dec 2022 09:20:21 +0400 Subject: [PATCH 30/92] support multiple domains in CORS --- cors_handler.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/cors_handler.go b/cors_handler.go index 24cb2bd6..162cf6a2 100644 --- a/cors_handler.go +++ b/cors_handler.go @@ -16,20 +16,20 @@ func CORSHandler(f func(w http.ResponseWriter, r *http.Request)) func(w http.Res w.Header().Add("Access-Control-Allow-Origin", "*") } } else { - // allowedOrigin := false - // for i := range AllowedCORSOrigins { - // if strings.EqualFold(r.Header.Get("Origin"), AllowedCORSOrigins[i]) { - // allowedOrigin = true - // break - // } - // } - // if allowedOrigin { - // w.Header().Add("Access-Control-Allow-Origin", r.Header.Get("Origin")) - // w.Header().Add("Access-Control-Allow-Credentials", "true") - // } else { - w.Header().Add("Access-Control-Allow-Origin", strings.Join(AllowedCORSOrigins, "|")) - w.Header().Add("Access-Control-Allow-Credentials", "true") - // } + allowedOrigin := false + for i := range AllowedCORSOrigins { + if strings.EqualFold(r.Header.Get("Origin"), AllowedCORSOrigins[i]) { + allowedOrigin = true + break + } + } + if allowedOrigin { + w.Header().Add("Access-Control-Allow-Origin", r.Header.Get("Origin")) + w.Header().Add("Access-Control-Allow-Credentials", "true") + } else { + w.Header().Add("Access-Control-Allow-Origin", strings.Join(AllowedCORSOrigins, "|")) + w.Header().Add("Access-Control-Allow-Credentials", "true") + } } From ac9ab128e0182837ad13e1879f9f9e574e825aeb Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 12 Dec 2022 21:20:01 +0400 Subject: [PATCH 31/92] Reset password stores the OTP time --- d_api_reset_password.go | 2 +- forgot_password_handler.go | 34 ++------------------ otp.go | 19 +++++++++++ password_reset_handler.go | 2 +- user.go | 65 +++++++++++++++++++++++++++++++++++--- 5 files changed, 85 insertions(+), 37 deletions(-) diff --git a/d_api_reset_password.go b/d_api_reset_password.go index 9cc79ae6..567e7f02 100644 --- a/d_api_reset_password.go +++ b/d_api_reset_password.go @@ -128,7 +128,7 @@ func dAPIResetPasswordHandler(w http.ResponseWriter, r *http.Request, s *Session } // check OTP - if !user.VerifyOTP(otp) { + if !user.VerifyOTPAtPasswordReset(otp) { incrementInvalidLogins(r) w.WriteHeader(401) ReturnJSON(w, r, map[string]interface{}{ diff --git a/forgot_password_handler.go b/forgot_password_handler.go index 0d99b1ce..18c70f41 100644 --- a/forgot_password_handler.go +++ b/forgot_password_handler.go @@ -2,7 +2,6 @@ package uadmin import ( "fmt" - "net" "net/http" "strings" ) @@ -27,37 +26,10 @@ func forgotPasswordHandler(u *User, r *http.Request, link string, msg string) er ` } - // Check if the host name is in the allowed hosts list - allowed := false - var host string - var allowedHost string - var err error - if host, _, err = net.SplitHostPort(GetHostName(r)); err != nil { - host = r.Host + link, err := u.GeneratePasswordResetLink(r, link) + if err != nil { + return err } - for _, v := range strings.Split(AllowedHosts, ",") { - if allowedHost, _, err = net.SplitHostPort(v); err != nil { - allowedHost = v - } - if allowedHost == host { - allowed = true - break - } - } - host = GetHostName(r) - if !allowed { - Trail(CRITICAL, "Reset password request for host: (%s) which is not in AllowedHosts settings", host) - return nil - } - - schema := GetSchema(r) - if link == "" { - link = "{SCHEMA}://{HOST}" + RootURL + "resetpassword?u={USER_ID}&key={OTP}" - } - link = strings.ReplaceAll(link, "{SCHEMA}", schema) - link = strings.ReplaceAll(link, "{HOST}", host) - link = strings.ReplaceAll(link, "{USER_ID}", fmt.Sprint(u.ID)) - link = strings.ReplaceAll(link, "{OTP}", u.GetOTP()) msg = strings.ReplaceAll(msg, "{NAME}", u.String()) msg = strings.ReplaceAll(msg, "{WEBSITE}", SiteName) diff --git a/otp.go b/otp.go index b3b17d3c..e170f8a6 100644 --- a/otp.go +++ b/otp.go @@ -54,6 +54,25 @@ func verifyOTP(pass, seed string, digits int, algorithm string, skew uint, perio return valid } +func verifyOTPAt(pass, seed string, digits int, algorithm string, skew uint, period uint, t time.Time) bool { + algo := getOTPAlgorithm(strings.ToLower(algorithm)) + opts := totp.ValidateOpts{ + Algorithm: algo, + Digits: otp.Digits(digits), + Skew: skew, + Period: period, + } + + pass = fmt.Sprintf("%0"+fmt.Sprintf("%d.%d", digits, digits)+"s", pass) + + valid, err := totp.ValidateCustom(pass, seed, t.UTC(), opts) + if err != nil { + Trail(ERROR, "Unable to verify OTP. %s", err) + return false + } + return valid +} + func generateOTPSeed(digits int, algorithm string, skew uint, period uint, user *User) (secret string, imagePath string) { algo := getOTPAlgorithm(strings.ToLower(algorithm)) diff --git a/password_reset_handler.go b/password_reset_handler.go index b6f686d6..8910a81a 100644 --- a/password_reset_handler.go +++ b/password_reset_handler.go @@ -42,7 +42,7 @@ func passwordResetHandler(w http.ResponseWriter, r *http.Request) { return } otpCode := r.FormValue("key") - if !user.VerifyOTP(otpCode) { + if !user.VerifyOTPAtPasswordReset(otpCode) { go func() { log := &Log{} if r.Form.Get("password") != "" { diff --git a/user.go b/user.go index 342daffe..3cc59806 100644 --- a/user.go +++ b/user.go @@ -2,6 +2,8 @@ package uadmin import ( "fmt" + "net" + "net/http" "strings" "time" ) @@ -21,10 +23,11 @@ type User struct { UserGroupID uint Photo string `uadmin:"image"` //Language []Language `gorm:"many2many:user_languages" listExclude:"true"` - LastLogin *time.Time `uadmin:"read_only"` - ExpiresOn *time.Time - OTPRequired bool - OTPSeed string `uadmin:"list_exclude;hidden;read_only"` + LastLogin *time.Time `uadmin:"read_only"` + ExpiresOn *time.Time + OTPRequired bool + OTPSeed string `uadmin:"list_exclude;hidden;read_only"` + PasswordReset *time.Time } // String return string @@ -250,3 +253,57 @@ func (u *User) GetOTP() string { func (u *User) VerifyOTP(pass string) bool { return verifyOTP(pass, u.OTPSeed, OTPDigits, OTPAlgorithm, OTPSkew, OTPPeriod) } + +func (u *User) VerifyOTPAtPasswordReset(pass string) bool { + // Password reset link is valid for 24 hours + if u.PasswordReset == nil || u.PasswordReset.Before(time.Now().AddDate(0, 0, -1)) { + return false + } + return verifyOTPAt(pass, u.OTPSeed, OTPDigits, OTPAlgorithm, OTPSkew, OTPPeriod, *u.PasswordReset) +} + +func (u *User) GeneratePasswordResetLink(r *http.Request, link string) (string, error) { + // Check if the host name is in the allowed hosts list + allowed := false + var host string + var allowedHost string + var err error + if host, _, err = net.SplitHostPort(GetHostName(r)); err != nil { + host = r.Host + } + for _, v := range strings.Split(AllowedHosts, ",") { + if allowedHost, _, err = net.SplitHostPort(v); err != nil { + allowedHost = v + } + if allowedHost == host { + allowed = true + break + } + } + host = GetHostName(r) + if !allowed { + Trail(CRITICAL, "Reset password request for host: (%s) which is not in AllowedHosts settings", host) + return "", fmt.Errorf("Reset password request for host: (%s) which is not in AllowedHosts settings", host) + } + + schema := GetSchema(r) + if link == "" { + link = "{SCHEMA}://{HOST}" + RootURL + "resetpassword?u={USER_ID}&key={OTP}" + } + link = strings.ReplaceAll(link, "{SCHEMA}", schema) + link = strings.ReplaceAll(link, "{HOST}", host) + link = strings.ReplaceAll(link, "{USER_ID}", fmt.Sprint(u.ID)) + link = strings.ReplaceAll(link, "{EMAIL}", fmt.Sprint(u.Email)) + link = strings.ReplaceAll(link, "{OTP}", u.GeneratePasswordResetOTP()) + + return link, nil +} + +func (u *User) GeneratePasswordResetOTP() string { + // Set the date time for the password reset + now := time.Now() + u.PasswordReset = &now + Save(u) + + return u.GetOTP() +} From bc90095a861ce2812fa95a148be8d487098f8205 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Fri, 23 Dec 2022 09:25:23 +0400 Subject: [PATCH 32/92] Add custom handler for login response --- d_api_login.go | 8 ++++++-- global.go | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/d_api_login.go b/d_api_login.go index 551669bb..608b90f2 100644 --- a/d_api_login.go +++ b/d_api_login.go @@ -46,7 +46,7 @@ func dAPILoginHandler(w http.ResponseWriter, r *http.Request, s *Session) { Preload(&s.User) jwt := SetSessionCookie(w, r, s) - ReturnJSON(w, r, map[string]interface{}{ + res := map[string]interface{}{ "status": "ok", "session": s.Key, "jwt": jwt, @@ -57,5 +57,9 @@ func dAPILoginHandler(w http.ResponseWriter, r *http.Request, s *Session) { "group_name": s.User.UserGroup.GroupName, "admin": s.User.Admin, }, - }) + } + if CustomDAPILoginHandler != nil { + res = CustomDAPILoginHandler(r, &s.User, res) + } + ReturnJSON(w, r, res) } diff --git a/global.go b/global.go index 03b9e127..9ef4a3d2 100644 --- a/global.go +++ b/global.go @@ -439,6 +439,10 @@ var AllowedCORSOrigins []string // CustomizeJSON is a function to allow customization of JSON returns var CustomizeJSON func(http.ResponseWriter, *http.Request, interface{}, []byte) []byte +// CustomDAPILoginHandler is a function that can provide extra information +// in the login return +var CustomDAPILoginHandler func(*http.Request, *User, map[string]interface{}) map[string]interface{} + // Private Global Variables // Regex var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") From fb8533f6b2a692bdb22b1d742e53c6011304fe49 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Fri, 23 Dec 2022 10:07:54 +0400 Subject: [PATCH 33/92] Add description to write parameters --- openapi.go | 29 ++++++++++++++++++++--------- openapi/generate_schema.go | 2 +- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/openapi.go b/openapi.go index 2d969d08..857f60f5 100644 --- a/openapi.go +++ b/openapi.go @@ -350,40 +350,51 @@ func GenerateOpenAPISchema() { fallthrough case cPASSWORD: return &openapi.SchemaObject{ - Type: "string", + Type: "string", + Description: v.Fields[i].Help, } case cFILE: fallthrough case cIMAGE: return &openapi.SchemaObject{ - Type: "string", - Format: "binary", + Type: "string", + Format: "binary", + Description: v.Fields[i].Help, } case cFK: - fallthrough + return &openapi.SchemaObject{ + Type: "integer", + Description: "Foreign key to " + v.Fields[i].TypeName + ". " + v.Fields[i].Help, + } case cLIST: fallthrough case cMONEY: return &openapi.SchemaObject{ - Type: "integer", + Type: "integer", + Description: v.Fields[i].Help, } case cNUMBER: fallthrough case cPROGRESSBAR: return &openapi.SchemaObject{ - Type: "number", + Type: "number", + Description: v.Fields[i].Help, } case cBOOL: return &openapi.SchemaObject{ - Type: "boolean", + Type: "string", + Enum: []interface{}{"", "0", "1"}, + Description: v.Fields[i].Help, } case cDATE: return &openapi.SchemaObject{ - Type: "string", + Type: "string", + Description: v.Fields[i].Help, } default: return &openapi.SchemaObject{ - Type: "string", + Type: "string", + Description: v.Fields[i].Help, } } }() diff --git a/openapi/generate_schema.go b/openapi/generate_schema.go index 46225210..83a8d76e 100644 --- a/openapi/generate_schema.go +++ b/openapi/generate_schema.go @@ -344,7 +344,7 @@ func GenerateBaseSchema() *Schema { Enum: []interface{}{"", "true", "false"}, }, Examples: map[string]Example{ - "getDeleted": { + "getStats": { Summary: "An example of a query that measures the execution time", Value: "$stat=1", }, From 4c742e35fa67a8ae4172b4364910ed04964a7f65 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 17 Jan 2023 06:17:23 +0400 Subject: [PATCH 34/92] BUG FIX: fix dAPI add custom saver --- d_api_edit.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/d_api_edit.go b/d_api_edit.go index 33978034..7d5989e0 100644 --- a/d_api_edit.go +++ b/d_api_edit.go @@ -154,8 +154,8 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) { // Execute business logic if _, ok := model.Addr().Interface().(saver); ok { - for i := 0; i < modelArray.Len(); i++ { - id := GetID(modelArray.Index(i)) + for i := 0; i < modelArray.Elem().Len(); i++ { + id := GetID(modelArray.Elem().Index(i)) model, _ = NewModel(modelName, false) Get(model.Addr().Interface(), "id = ?", id) model.Addr().Interface().(saver).Save() From c80c86b2df01ff278740849ac81b5b6c8867eff1 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 24 Jan 2023 13:36:31 +0400 Subject: [PATCH 35/92] BUG FIX: no preload for custom schema --- d_api_read.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/d_api_read.go b/d_api_read.go index 12cf94ce..cee8a71d 100644 --- a/d_api_read.go +++ b/d_api_read.go @@ -173,7 +173,7 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { } } // Preload - if params["$preload"] == "1" || params["$preload"] == "true" { + if !customSchema && (params["$preload"] == "1" || params["$preload"] == "true") { mList := reflect.ValueOf(m) for i := 0; i < mList.Elem().Len(); i++ { Preload(mList.Elem().Index(i).Addr().Interface()) From 86df2b12623fd4b53cd25f9fb4046224b9644c7a Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 24 Jan 2023 13:51:06 +0400 Subject: [PATCH 36/92] add database error handeling for dAPI read --- d_api_read.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/d_api_read.go b/d_api_read.go index cee8a71d..4603078a 100644 --- a/d_api_read.go +++ b/d_api_read.go @@ -9,6 +9,7 @@ import ( func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { var rowsCount int64 + var err error urlParts := strings.Split(r.URL.Path, "/") modelName := r.Context().Value(CKey("modelName")).(string) @@ -129,10 +130,10 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { if Database.Type == "mysql" { db := GetDB() if !customSchema { - db.Raw(SQL, args...).Scan(m) + err = db.Raw(SQL, args...).Scan(m).Error } else { var rec []map[string]interface{} - db.Raw(SQL, args...).Scan(&rec) + err = db.Raw(SQL, args...).Scan(&rec).Error m = rec } if a, ok := m.([]map[string]interface{}); ok { @@ -144,10 +145,10 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { db := GetDB().Begin() db.Exec("PRAGMA case_sensitive_like=ON;") if !customSchema { - db.Raw(SQL, args...).Scan(m) + err = db.Raw(SQL, args...).Scan(m).Error } else { var rec []map[string]interface{} - db.Raw(SQL, args...).Scan(&rec) + err = db.Raw(SQL, args...).Scan(&rec).Error m = rec } db.Exec("PRAGMA case_sensitive_like=OFF;") @@ -160,10 +161,10 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { } else if Database.Type == "postgres" { db := GetDB() if !customSchema { - db.Raw(SQL, args...).Scan(m) + err = db.Raw(SQL, args...).Scan(m).Error } else { var rec []map[string]interface{} - db.Raw(SQL, args...).Scan(&rec) + err = db.Raw(SQL, args...).Scan(&rec).Error m = rec } if a, ok := m.([]map[string]interface{}); ok { @@ -172,6 +173,16 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { rowsCount = int64(reflect.ValueOf(m).Elem().Len()) } } + + // Check for errors + if err != nil { + w.WriteHeader(400) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "Error in query. " + err.Error(), + }) + } + // Preload if !customSchema && (params["$preload"] == "1" || params["$preload"] == "true") { mList := reflect.ValueOf(m) From 60e8c5a2c1c807d45050e8416f2b091ec0dc9f04 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 24 Jan 2023 13:54:42 +0400 Subject: [PATCH 37/92] add database error handeling for add and edit --- d_api_add.go | 10 +++++++++- d_api_edit.go | 11 ++++++++++- d_api_read.go | 3 ++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/d_api_add.go b/d_api_add.go index 619093fc..b5b682bc 100644 --- a/d_api_add.go +++ b/d_api_add.go @@ -105,7 +105,15 @@ func dAPIAddHandler(w http.ResponseWriter, r *http.Request, s *Session) { db = db.Raw("SELECT lastval() AS lastid") } db.Table(tableName).Pluck("lastid", &id) - db.Commit() + err := db.Commit().Error + if err != nil { + w.WriteHeader(400) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "Error in update query. " + err.Error(), + }) + return + } if db.Error != nil { ReturnJSON(w, r, map[string]interface{}{ diff --git a/d_api_edit.go b/d_api_edit.go index 7d5989e0..0fc67a47 100644 --- a/d_api_edit.go +++ b/d_api_edit.go @@ -140,7 +140,16 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) { } } } - db.Commit() + err = db.Commit().Error + + if err != nil { + w.WriteHeader(400) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "Error in update query. " + err.Error(), + }) + return + } returnDAPIJSON(w, r, map[string]interface{}{ "status": "ok", diff --git a/d_api_read.go b/d_api_read.go index 4603078a..772b78ef 100644 --- a/d_api_read.go +++ b/d_api_read.go @@ -179,8 +179,9 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { w.WriteHeader(400) ReturnJSON(w, r, map[string]interface{}{ "status": "error", - "err_msg": "Error in query. " + err.Error(), + "err_msg": "Error in read query. " + err.Error(), }) + return } // Preload From 6a3a41d88a504dee5bf31153404a14a7aef41231 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 25 Jan 2023 14:52:50 +0400 Subject: [PATCH 38/92] FullMediaURL --- d_api_read.go | 17 +++++++++++++++++ global.go | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/d_api_read.go b/d_api_read.go index 772b78ef..a275c91a 100644 --- a/d_api_read.go +++ b/d_api_read.go @@ -195,6 +195,23 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { // Process M2M getQueryM2M(params, m, customSchema, modelName) + // Process Full Media URL + if !customSchema && FullMediaURL { + for i := 0; i < reflect.ValueOf(m).Elem().Len(); i++ { + // Search for media fields + record := reflect.ValueOf(m).Elem().Index(i) + for j := range schema.Fields { + if schema.Fields[j].Type == cIMAGE || schema.Fields[j].Type == cFILE { + // Check if there is a file + if record.FieldByName(schema.Fields[j].Name).String() != "" && record.FieldByName(schema.Fields[j].Name).String()[0] == '/' { + record.FieldByName(schema.Fields[j].Name).SetString(GetSchema(r) + "://" + GetHostName(r) + record.FieldByName(schema.Fields[j].Name).String()) + } + + } + } + } + } + returnDAPIJSON(w, r, map[string]interface{}{ "status": "ok", "result": m, diff --git a/global.go b/global.go index 9ef4a3d2..607968e9 100644 --- a/global.go +++ b/global.go @@ -443,6 +443,10 @@ var CustomizeJSON func(http.ResponseWriter, *http.Request, interface{}, []byte) // in the login return var CustomDAPILoginHandler func(*http.Request, *User, map[string]interface{}) map[string]interface{} +// FullMediaURL allows uAdmin to send you full path URL instead on relative +// path for dAPI read requests +var FullMediaURL = false + // Private Global Variables // Regex var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") From 9d58aeb07470fd2d819a93d2e8c73342d3f05c12 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 25 Jan 2023 17:56:06 +0400 Subject: [PATCH 39/92] Mask passwords in API --- d_api_read.go | 9 ++++++--- global.go | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/d_api_read.go b/d_api_read.go index a275c91a..1ba4bac6 100644 --- a/d_api_read.go +++ b/d_api_read.go @@ -196,17 +196,20 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { getQueryM2M(params, m, customSchema, modelName) // Process Full Media URL - if !customSchema && FullMediaURL { + // Mask passwords + if !customSchema { for i := 0; i < reflect.ValueOf(m).Elem().Len(); i++ { // Search for media fields record := reflect.ValueOf(m).Elem().Index(i) for j := range schema.Fields { - if schema.Fields[j].Type == cIMAGE || schema.Fields[j].Type == cFILE { + if FullMediaURL && (schema.Fields[j].Type == cIMAGE || schema.Fields[j].Type == cFILE) { // Check if there is a file if record.FieldByName(schema.Fields[j].Name).String() != "" && record.FieldByName(schema.Fields[j].Name).String()[0] == '/' { record.FieldByName(schema.Fields[j].Name).SetString(GetSchema(r) + "://" + GetHostName(r) + record.FieldByName(schema.Fields[j].Name).String()) } - + } + if MaskPasswordInAPI && schema.Fields[j].Type == cPASSWORD { + record.FieldByName(schema.Fields[j].Name).SetString("***") } } } diff --git a/global.go b/global.go index 607968e9..c1209cd5 100644 --- a/global.go +++ b/global.go @@ -447,6 +447,9 @@ var CustomDAPILoginHandler func(*http.Request, *User, map[string]interface{}) ma // path for dAPI read requests var FullMediaURL = false +// MaskPasswordInAPI will replace any password fields with an asterisk mask +var MaskPasswordInAPI = true + // Private Global Variables // Regex var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") From cddda6e5f13b29dc8252b9ad8e8ea87c5be7f83b Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 25 Jan 2023 17:57:12 +0400 Subject: [PATCH 40/92] Make OTP a password field --- user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user.go b/user.go index 3cc59806..b4865f5b 100644 --- a/user.go +++ b/user.go @@ -26,7 +26,7 @@ type User struct { LastLogin *time.Time `uadmin:"read_only"` ExpiresOn *time.Time OTPRequired bool - OTPSeed string `uadmin:"list_exclude;hidden;read_only"` + OTPSeed string `uadmin:"list_exclude;hidden;read_only;password"` PasswordReset *time.Time } From 0f3b48624a1cd9002a101cddcb07a007f2be2d2f Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 26 Jan 2023 16:25:43 +0400 Subject: [PATCH 41/92] password masking and Full media URL for read one --- d_api_read.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/d_api_read.go b/d_api_read.go index 1ba4bac6..3392c075 100644 --- a/d_api_read.go +++ b/d_api_read.go @@ -241,6 +241,22 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { Preload(m.Interface()) } + // Process Full Media URL + // Mask passwords + // Search for media fields + record := m.Elem() + for j := range schema.Fields { + if FullMediaURL && (schema.Fields[j].Type == cIMAGE || schema.Fields[j].Type == cFILE) { + // Check if there is a file + if record.FieldByName(schema.Fields[j].Name).String() != "" && record.FieldByName(schema.Fields[j].Name).String()[0] == '/' { + record.FieldByName(schema.Fields[j].Name).SetString(GetSchema(r) + "://" + GetHostName(r) + record.FieldByName(schema.Fields[j].Name).String()) + } + } + if MaskPasswordInAPI && schema.Fields[j].Type == cPASSWORD { + record.FieldByName(schema.Fields[j].Name).SetString("***") + } + } + returnDAPIJSON(w, r, map[string]interface{}{ "status": "ok", "result": i, From 3e581a729ed916ff31a89f0a77552cb342ebd523 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 1 Feb 2023 20:03:45 +0800 Subject: [PATCH 42/92] send 400 in dAPI edit if there is a database error --- d_api_edit.go | 1 + 1 file changed, 1 insertion(+) diff --git a/d_api_edit.go b/d_api_edit.go index 0fc67a47..f4437377 100644 --- a/d_api_edit.go +++ b/d_api_edit.go @@ -106,6 +106,7 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) { db.Model(model.Interface()).Where(q, args...).Scan(modelArray.Interface()) db = db.Model(model.Interface()).Where(q, args...).Updates(writeMap) if db.Error != nil { + w.WriteHeader(400) ReturnJSON(w, r, map[string]interface{}{ "status": "error", "err_msg": "Unable to update database. " + db.Error.Error(), From 94595fc3c3c648b436bcf1369e26c91629a14378 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 2 Feb 2023 01:11:11 +0800 Subject: [PATCH 43/92] BUG FIX: business logic on edit one read again before saving --- d_api_edit.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/d_api_edit.go b/d_api_edit.go index f4437377..05db97ff 100644 --- a/d_api_edit.go +++ b/d_api_edit.go @@ -216,8 +216,11 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) { } // Execute business logic - if modelSaver, ok := m.Interface().(saver); ok { - modelSaver.Save() + if _, ok := m.Interface().(saver); ok { + db = GetDB() + m, _ = NewModel(modelName, true) + db.Model(model.Interface()).Where("id = ?", urlParts[0]).Scan(m.Interface()) + m.Interface().(saver).Save() } returnDAPIJSON(w, r, map[string]interface{}{ From b9f301e0679472b3b21cff1ce09fb85edeaf30f3 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 9 Feb 2023 19:57:29 +0800 Subject: [PATCH 44/92] Stop email sending if custom handler sends false fo4 proceed --- send_email.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/send_email.go b/send_email.go index 65131fa1..fb2f1bf4 100644 --- a/send_email.go +++ b/send_email.go @@ -23,9 +23,9 @@ func SendEmail(to, cc, bcc []string, subject, body string, attachments ...string proceed, err = CustomEmailHandler(&to, &cc, &bcc, &subject, &body, attachmentsPointers...) if err != nil { Trail(ERROR, "Error in CustomEmailHandler. %s", err) - if !proceed { - return - } + } + if !proceed { + return } } if EmailFrom == "" || EmailUsername == "" || EmailPassword == "" || EmailSMTPServer == "" || EmailSMTPServerPort == 0 { From c1aadf7e535068079271a4abaf7ca9538f0abe8e Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 13 Feb 2023 23:52:18 +0800 Subject: [PATCH 45/92] Add CORS handler for static and media --- register.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/register.go b/register.go index caea0246..ccff496f 100644 --- a/register.go +++ b/register.go @@ -363,8 +363,13 @@ func registerHandlers() { if !DisableAdminUI { // Handler for uAdmin, static and media http.HandleFunc(RootURL, Handler(mainHandler)) - http.HandleFunc("/static/", Handler(StaticHandler)) - http.HandleFunc("/media/", Handler(mediaHandler)) + if EnableDAPICORS { + http.HandleFunc("/media/", CORSHandler(StaticHandler)) + http.HandleFunc("/media/", CORSHandler(mediaHandler)) + } else { + http.HandleFunc("/static/", Handler(StaticHandler)) + http.HandleFunc("/media/", Handler(mediaHandler)) + } // api handler http.HandleFunc(RootURL+"revertHandler/", Handler(revertLogHandler)) From 2eb92c3b13271e049352b33deed7f5ceb3139163 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 14 Feb 2023 08:28:17 +0800 Subject: [PATCH 46/92] Add CORS handler for static and media --- register.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/register.go b/register.go index ccff496f..9bdb3f03 100644 --- a/register.go +++ b/register.go @@ -364,7 +364,7 @@ func registerHandlers() { // Handler for uAdmin, static and media http.HandleFunc(RootURL, Handler(mainHandler)) if EnableDAPICORS { - http.HandleFunc("/media/", CORSHandler(StaticHandler)) + http.HandleFunc("/static/", CORSHandler(StaticHandler)) http.HandleFunc("/media/", CORSHandler(mediaHandler)) } else { http.HandleFunc("/static/", Handler(StaticHandler)) From b67b2edbcdce006367b229464537b26326d67c70 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 20 Feb 2023 13:15:19 +0400 Subject: [PATCH 47/92] m2m filter --- d_api_helper.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ db.go | 3 +++ 2 files changed, 51 insertions(+) diff --git a/d_api_helper.go b/d_api_helper.go index 37f64420..0d956945 100644 --- a/d_api_helper.go +++ b/d_api_helper.go @@ -126,6 +126,11 @@ func getFilters(r *http.Request, params map[string]string, tableName string, sch qParts = append(qParts, "("+strings.Join(orQParts, " OR ")+")") args = append(args, orArgs...) } + } else if isM2MField(k, schema) { + // M2M filter + Trail(DEBUG, "M2M: %s", k) + qParts = append(qParts, getM2MQueryOperator(k, schema)) + args = append(args, getM2MQueryArg(k, v, schema)...) } else { qParts = append(qParts, getQueryOperator(r, k, tableName)) args = append(args, getQueryArg(k, v)...) @@ -147,6 +152,49 @@ func getFilters(r *http.Request, params map[string]string, tableName string, sch return query, args } +func isM2MField(v string, schema *ModelSchema) bool { + f := schema.FieldByColumnName(v) + if f == nil { + return false + } + + return f.Type == cM2M +} + +func getM2MQueryOperator(v string, schema *ModelSchema) string { + return "id IN (?)" +} + +func getM2MQueryArg(k, v string, schema *ModelSchema) []interface{} { + f := schema.FieldByColumnName(k) + t1 := schema.ModelName + t2 := strings.ToLower(f.TypeName) + SQL := sqlDialect[Database.Type]["selectM2MT2"] + SQL = strings.ReplaceAll(SQL, "{TABLE1}", t1) + SQL = strings.ReplaceAll(SQL, "{TABLE2}", t2) + + type M2MTable struct { + Table1ID uint `gorm:"column:table1_id"` + Table2ID uint `gorm:"column:table2_id"` + } + + values := []M2MTable{} + + err := GetDB().Raw(SQL, strings.Split(v, ",")).Scan(&values).Error + if err != nil { + Trail(ERROR, "Unable to get M2M args. %s", err) + return []interface{}{} + } + + returnArgs := make([]interface{}, len(values)) + + for i := range values { + returnArgs[i] = values[i].Table1ID + } + + return []interface{}{returnArgs} +} + func getQueryOperator(r *http.Request, v string, tableName string) string { // Determine if the query is negated n := len(v) > 0 && v[0] == '!' diff --git a/db.go b/db.go index 9dbbb254..8241c466 100644 --- a/db.go +++ b/db.go @@ -39,12 +39,14 @@ var sqlDialect = map[string]map[string]string{ "selectM2M": "SELECT `table2_id` FROM `{TABLE1}_{TABLE2}` WHERE table1_id=?;", "deleteM2M": "DELETE FROM `{TABLE1}_{TABLE2}` WHERE `table1_id`=?;", "insertM2M": "INSERT INTO `{TABLE1}_{TABLE2}` VALUES (?, ?);", + "selectM2MT2": "SELECT DISTINCT `table1_id` FROM `{TABLE1}_{TABLE2}` WHERE table2_id IN (?);", }, "postgres": { "createM2MTable": `CREATE TABLE "{TABLE1}_{TABLE2}" ("table1_id" BIGINT NOT NULL, "table2_id" BIGINT NOT NULL, PRIMARY KEY ("table1_id","table2_id"))`, "selectM2M": `SELECT "table2_id" FROM "{TABLE1}_{TABLE2}" WHERE table1_id=?;`, "deleteM2M": `DELETE FROM "{TABLE1}_{TABLE2}" WHERE "table1_id"=?;`, "insertM2M": `INSERT INTO "{TABLE1}_{TABLE2}" VALUES (?, ?);`, + "selectM2MT2": "SELECT DISTINCT `table1_id` FROM `{TABLE1}_{TABLE2}` WHERE table2_id IN (?);", }, "sqlite": { //"createM2MTable": "CREATE TABLE `{TABLE1}_{TABLE2}` (`{TABLE1}_id` INTEGER NOT NULL,`{TABLE2}_id` INTEGER NOT NULL, PRIMARY KEY(`{TABLE1}_id`,`{TABLE2}_id`));", @@ -52,6 +54,7 @@ var sqlDialect = map[string]map[string]string{ "selectM2M": "SELECT `table2_id` FROM `{TABLE1}_{TABLE2}` WHERE table1_id=?;", "deleteM2M": "DELETE FROM `{TABLE1}_{TABLE2}` WHERE `table1_id`=?;", "insertM2M": "INSERT INTO `{TABLE1}_{TABLE2}` VALUES (?, ?);", + "selectM2MT2": "SELECT DISTINCT `table1_id` FROM `{TABLE1}_{TABLE2}` WHERE table2_id IN (?);", }, } From 3189c19a3c64939e58b6f246855c6b06d956a680 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 20 Feb 2023 13:37:44 +0400 Subject: [PATCH 48/92] update excelize --- d_api_helper.go | 1 - export.go | 15 +++- go.mod | 33 +++---- go.sum | 230 +++++++++++------------------------------------- 4 files changed, 76 insertions(+), 203 deletions(-) diff --git a/d_api_helper.go b/d_api_helper.go index 0d956945..caf6005e 100644 --- a/d_api_helper.go +++ b/d_api_helper.go @@ -128,7 +128,6 @@ func getFilters(r *http.Request, params map[string]string, tableName string, sch } } else if isM2MField(k, schema) { // M2M filter - Trail(DEBUG, "M2M: %s", k) qParts = append(qParts, getM2MQueryOperator(k, schema)) args = append(args, getM2MQueryArg(k, v, schema)...) } else { diff --git a/export.go b/export.go index fdb0ea5a..767fce89 100644 --- a/export.go +++ b/export.go @@ -10,7 +10,7 @@ import ( "strings" "time" - // import upportd image formats to allow exporting + // import unsupported image formats to allow exporting // images to excel _ "image/gif" _ "image/jpeg" @@ -300,7 +300,18 @@ func exportHandler(w http.ResponseWriter, r *http.Request, session *Session) { } file.SetRowHeight(sheetName, i+2, 100) file.SetColWidth(sheetName, colName, colName, 25) - file.AddPicture(sheetName, cellName, a.Index(i).Field(c).String()[1:], `{"autofit": true, "print_obj": true, "lock_aspect_ratio": true, "locked": false, "positioning": "oneCell", "x_scale":5.0, "y_scale":5.0}`) + True := true + False := false + graphicsOptions := excelize.GraphicOptions{ + AutoFit: true, + PrintObject: &True, + LockAspectRatio: true, + Locked: &False, + Positioning: "oneCell", + ScaleX: 5.0, + ScaleY: 5.0, + } + file.AddPicture(sheetName, cellName, a.Index(i).Field(c).String()[1:], &graphicsOptions) file.SetCellStyle(sheetName, cellName, cellName, bodyStyle) } else if schema.FieldByName(t.Field(c).Name).Type == cCODE { file.SetCellValue(sheetName, cellName, a.Index(i).Field(c).Interface()) diff --git a/go.mod b/go.mod index 409f1962..4c2ac87f 100644 --- a/go.mod +++ b/go.mod @@ -5,30 +5,25 @@ go 1.17 require ( github.com/jinzhu/inflection v1.0.0 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 - github.com/pquerna/otp v1.3.0 + github.com/pquerna/otp v1.4.0 github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e github.com/uadmin/rrd v0.0.0-20200219090641-e438da1b7640 - github.com/xuri/excelize/v2 v2.6.1 - golang.org/x/crypto v0.2.0 - golang.org/x/mod v0.7.0 - golang.org/x/net v0.2.0 - gorm.io/driver/mysql v1.4.4 - gorm.io/driver/postgres v1.4.5 - gorm.io/driver/sqlite v1.4.3 - gorm.io/gorm v1.24.1 + github.com/xuri/excelize/v2 v2.7.0 + golang.org/x/crypto v0.6.0 + golang.org/x/mod v0.8.0 + golang.org/x/net v0.7.0 + gorm.io/driver/mysql v1.4.7 + gorm.io/driver/postgres v1.4.8 + gorm.io/driver/sqlite v1.4.4 + gorm.io/gorm v1.24.5 ) require ( github.com/boombuler/barcode v1.0.1 // indirect - github.com/go-sql-driver/mysql v1.6.0 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.13.0 // indirect - github.com/jackc/pgio v1.0.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.1 // indirect - github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgtype v1.12.0 // indirect - github.com/jackc/pgx/v4 v4.17.2 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.3.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect @@ -36,6 +31,6 @@ require ( github.com/richardlehane/msoleps v1.0.3 // indirect github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect - golang.org/x/sys v0.2.0 // indirect - golang.org/x/text v0.4.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect ) diff --git a/go.sum b/go.sum index 93ba2e38..9a1dc4dc 100644 --- a/go.sum +++ b/go.sum @@ -1,96 +1,30 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= -github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 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/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= -github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= -github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= -github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA= +github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= @@ -98,160 +32,94 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.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/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs= -github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= github.com/uadmin/rrd v0.0.0-20200219090641-e438da1b7640 h1:A8ZPAW0kgZQ9MaqZRJyq9Zb39Zsf84G2Ypafszsttb8= github.com/uadmin/rrd v0.0.0-20200219090641-e438da1b7640/go.mod h1:Xo1H4x3+D6gR2/pDDHOLe3uvF7Y59Sro7ErzHnDuLfs= github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c= github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/excelize/v2 v2.6.1 h1:ICBdtw803rmhLN3zfvyEGH3cwSmZv+kde7LhTDT659k= -github.com/xuri/excelize/v2 v2.6.1/go.mod h1:tL+0m6DNwSXj/sILHbQTYsLi9IF4TW59H2EF3Yrx1AU= +github.com/xuri/excelize/v2 v2.7.0 h1:Hri/czwyRCW6f6zrCDWXcXKshlq4xAZNpNOpdfnFhEw= +github.com/xuri/excelize/v2 v2.7.0/go.mod h1:ebKlRoS+rGyLMyUx3ErBECXs/HNYqyj+PbkkKRK5vSI= github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M= github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE= -golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE= -golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= +golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 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.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/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.4.4 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ= -gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM= -gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc= -gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg= -gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= -gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y= +gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc= +gorm.io/driver/postgres v1.4.8 h1:NDWizaclb7Q2aupT0jkwK8jx1HVCNzt+PQ8v/VnxviA= +gorm.io/driver/postgres v1.4.8/go.mod h1:O9MruWGNLUBUWVYfWuBClpf3HeGjOoybY0SNmCs3wsw= +gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc= +gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= -gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= -gorm.io/gorm v1.24.1 h1:CgvzRniUdG67hBAzsxDGOAuq4Te1osVMYsa1eQbd4fs= -gorm.io/gorm v1.24.1/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE= +gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= From 5f8e585635abfc0fc560d86de2cc7e89fc7ccac9 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 22 Feb 2023 09:47:52 +0400 Subject: [PATCH 49/92] limit bcrypt to 72 bytes --- auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth.go b/auth.go index 9c093efb..681fab37 100644 --- a/auth.go +++ b/auth.go @@ -84,7 +84,7 @@ func GenerateBase32(length int) string { // hashPass Generates a hash from a password and salt func hashPass(pass string) string { password := []byte(pass + Salt) - hash, err := bcrypt.GenerateFromPassword(password, bcryptDiff) + hash, err := bcrypt.GenerateFromPassword(password[:72], bcryptDiff) if err != nil { Trail(ERROR, "uadmin.auth.hashPass.GenerateFromPassword: %s", err) return "" From a87d30194b4316bd52b72e12ad813cea598fef1a Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 22 Feb 2023 09:48:54 +0400 Subject: [PATCH 50/92] limit bcrypt to 72 bytes --- auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth.go b/auth.go index 681fab37..bf5602b0 100644 --- a/auth.go +++ b/auth.go @@ -772,7 +772,7 @@ func GetSchema(r *http.Request) string { func verifyPassword(hash string, plain string) error { password := []byte(plain + Salt) hashedPassword := []byte(hash) - return bcrypt.CompareHashAndPassword(hashedPassword, password) + return bcrypt.CompareHashAndPassword(hashedPassword[:72], password[:72]) } // sanitizeFileName is a function to sanitize file names to pretect From 111558e161f558e3f9e32272c53259835ae2badf Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 22 Feb 2023 09:55:37 +0400 Subject: [PATCH 51/92] limit bcrypt to 72 bytes --- auth.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/auth.go b/auth.go index bf5602b0..870fa936 100644 --- a/auth.go +++ b/auth.go @@ -84,6 +84,9 @@ func GenerateBase32(length int) string { // hashPass Generates a hash from a password and salt func hashPass(pass string) string { password := []byte(pass + Salt) + if len(password) > 72 { + password = password[:72] + } hash, err := bcrypt.GenerateFromPassword(password[:72], bcryptDiff) if err != nil { Trail(ERROR, "uadmin.auth.hashPass.GenerateFromPassword: %s", err) @@ -772,6 +775,12 @@ func GetSchema(r *http.Request) string { func verifyPassword(hash string, plain string) error { password := []byte(plain + Salt) hashedPassword := []byte(hash) + if len(hashedPassword) > 72 { + hashedPassword = hashedPassword[:72] + } + if len(password) > 72 { + password = password[:72] + } return bcrypt.CompareHashAndPassword(hashedPassword[:72], password[:72]) } From 86bead52362dc8d3a8f9f89b901b4aef4f33a41d Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 22 Feb 2023 09:57:45 +0400 Subject: [PATCH 52/92] limit bcrypt to 72 bytes --- auth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth.go b/auth.go index 870fa936..2740eb0b 100644 --- a/auth.go +++ b/auth.go @@ -87,7 +87,7 @@ func hashPass(pass string) string { if len(password) > 72 { password = password[:72] } - hash, err := bcrypt.GenerateFromPassword(password[:72], bcryptDiff) + hash, err := bcrypt.GenerateFromPassword(password, bcryptDiff) if err != nil { Trail(ERROR, "uadmin.auth.hashPass.GenerateFromPassword: %s", err) return "" @@ -781,7 +781,7 @@ func verifyPassword(hash string, plain string) error { if len(password) > 72 { password = password[:72] } - return bcrypt.CompareHashAndPassword(hashedPassword[:72], password[:72]) + return bcrypt.CompareHashAndPassword(hashedPassword, password) } // sanitizeFileName is a function to sanitize file names to pretect From 000e5e8bff82a38fc3d8d8c02071de46d8756c13 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 27 Feb 2023 16:07:15 +0400 Subject: [PATCH 53/92] make trail the first registered model --- register.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/register.go b/register.go index 9bdb3f03..fa7586c1 100644 --- a/register.go +++ b/register.go @@ -80,6 +80,17 @@ func Register(m ...interface{}) { // Setup languages initializeLanguage() + // check if trail dashboard menu item is added + if Count([]DashboardMenu{}, "menu_name = ?", "Trail") == 0 { + dashboard := DashboardMenu{ + MenuName: "Trail", + URL: "trail", + Hidden: false, + Cat: "System", + } + Save(&dashboard) + } + // Store models in Model global variable // and initialize the dashboard dashboardMenus := []DashboardMenu{} @@ -142,17 +153,6 @@ func Register(m ...interface{}) { } } - // check if trail dashboard menu item is added - if Count([]DashboardMenu{}, "menu_name = ?", "Trail") == 0 { - dashboard := DashboardMenu{ - MenuName: "Trail", - URL: "trail", - Hidden: false, - Cat: "System", - } - Save(&dashboard) - } - // Check if encrypt key is there or generate it if _, err := os.Stat(".key"); os.IsNotExist(err) && os.Getenv("UADMIN_KEY") == "" { EncryptKey = generateByteArray(32) From f6cd6f8fcf472c9a41d5d69a9f22e8a1f466de59 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 28 Feb 2023 00:27:57 +0400 Subject: [PATCH 54/92] BUGFIX: get table columns in dAPI join --- d_api_read.go | 2 +- global.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/d_api_read.go b/d_api_read.go index 3392c075..9b863901 100644 --- a/d_api_read.go +++ b/d_api_read.go @@ -69,7 +69,7 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { if f != "" { SQL = strings.Replace(SQL, "{FIELDS}", f, -1) } else { - SQL = strings.Replace(SQL, "{FIELDS}", "*", -1) + SQL = strings.Replace(SQL, "{FIELDS}", tableName+".*", -1) } join := getQueryJoin(r, params, tableName) diff --git a/global.go b/global.go index c1209cd5..a69642a9 100644 --- a/global.go +++ b/global.go @@ -81,7 +81,7 @@ const cEMAIL = "email" const cM2M = "m2m" // Version number as per Semantic Versioning 2.0.0 (semver.org) -const Version = "0.9.2" +const Version = "0.9.5" // VersionCodeName is the cool name we give to versions with significant changes. // This name should always be a bug's name starting from A-Z them revolving back. From 8749560b0fa6be5a0f7c91af7edd0bc52dcb39bf Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 28 Feb 2023 19:24:20 +0400 Subject: [PATCH 55/92] Load data reads before writing --- load_initial_data.go | 1 + 1 file changed, 1 insertion(+) diff --git a/load_initial_data.go b/load_initial_data.go index d8deb0a0..f67a5c4c 100644 --- a/load_initial_data.go +++ b/load_initial_data.go @@ -103,6 +103,7 @@ func loadInitialData() error { // Save records for i := 0; i < modelArray.Elem().Len(); i++ { + Get(modelArray.Elem().Index(i).Addr().Interface(), "id = ?", GetID(modelArray.Elem().Index(i))) err = Save(modelArray.Elem().Index(i).Addr().Interface()) if err != nil { return fmt.Errorf("loadInitialData.Save: Error in %s[%d]. %s", table, i, err) From aa0574e28a34376f692db6fe0e92b131095c17b8 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 28 Feb 2023 22:29:15 +0400 Subject: [PATCH 56/92] Load data reads before writing --- load_initial_data.go | 1 + 1 file changed, 1 insertion(+) diff --git a/load_initial_data.go b/load_initial_data.go index f67a5c4c..f81e7dc5 100644 --- a/load_initial_data.go +++ b/load_initial_data.go @@ -104,6 +104,7 @@ func loadInitialData() error { // Save records for i := 0; i < modelArray.Elem().Len(); i++ { Get(modelArray.Elem().Index(i).Addr().Interface(), "id = ?", GetID(modelArray.Elem().Index(i))) + json.Unmarshal(buf, modelArray.Interface()) err = Save(modelArray.Elem().Index(i).Addr().Interface()) if err != nil { return fmt.Errorf("loadInitialData.Save: Error in %s[%d]. %s", table, i, err) From 497f449cb0b69a26e29da0c99930f99b75624acb Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 2 Mar 2023 10:07:36 +0400 Subject: [PATCH 57/92] update error message no unknown DB in mysql --- db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db.go b/db.go index 8241c466..c6f99601 100644 --- a/db.go +++ b/db.go @@ -238,7 +238,7 @@ func GetDB() *gorm.DB { }) // Check if the error is DB doesn't exist and create it - if err != nil && err.Error() == "Error 1049: Unknown database '"+Database.Name+"'" { + if err != nil && strings.Contains(err.Error(), "Unknown database '"+Database.Name+"'") { err = createDB() if err == nil { From 151ed8c6c199080aadafc7fa98c14ff9abb8da94 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 2 Mar 2023 10:41:27 +0400 Subject: [PATCH 58/92] set dates to now if not set in mysql --- db.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/db.go b/db.go index c6f99601..876010f6 100644 --- a/db.go +++ b/db.go @@ -440,6 +440,9 @@ func All(a interface{}) (err error) { // Save saves the object in the database func Save(a interface{}) (err error) { encryptRecord(a) + if Database.Type == "mysql" { + a = fixDates(a) + } TimeMetric("uadmin/db/duration", 1000, func() { err = db.Save(a).Error for fmt.Sprint(err) == "database is locked" { @@ -459,6 +462,20 @@ func Save(a interface{}) (err error) { return nil } +func fixDates(a interface{}) interface{} { + value := reflect.ValueOf(a).Elem() + timeType := reflect.TypeOf(time.Now()) + timeValue := reflect.ValueOf(time.Now()) + for i := 0; i < value.NumField(); i++ { + if value.Field(i).Type() == timeType { + if value.Interface().(time.Time).IsZero() { + value.Field(i).Set(timeValue) + } + } + } + return value.Addr().Interface() +} + func customSave(m interface{}) (err error) { a := m t := reflect.TypeOf(a) From 59f4ba047ff0d3c8dc830c6ae883e8b6ac8edbc3 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 2 Mar 2023 11:39:40 +0400 Subject: [PATCH 59/92] set dates to now if not set in mysql --- db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db.go b/db.go index 876010f6..8be2b5c5 100644 --- a/db.go +++ b/db.go @@ -468,7 +468,7 @@ func fixDates(a interface{}) interface{} { timeValue := reflect.ValueOf(time.Now()) for i := 0; i < value.NumField(); i++ { if value.Field(i).Type() == timeType { - if value.Interface().(time.Time).IsZero() { + if value.Field(i).Interface().(time.Time).IsZero() { value.Field(i).Set(timeValue) } } From 7dd4f2b4f9ebe19094f8074a702809cb9c72f13a Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Sat, 4 Mar 2023 20:23:04 +0400 Subject: [PATCH 60/92] add global pre and post handler for dAPI --- d_api.go | 13 +++++++++++++ d_api_helper.go | 14 +++++++++++++- d_api_reset_password.go | 9 ++++++++- global.go | 24 ++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/d_api.go b/d_api.go index be8ba959..537e5d04 100644 --- a/d_api.go +++ b/d_api.go @@ -238,6 +238,9 @@ func dAPIHandler(w http.ResponseWriter, r *http.Request, s *Session) { // Route the request to the correct handler based on the command if command == "read" { // check if there is a prequery + if APIPreQueryReadHandler != nil && !APIPreQueryReadHandler(w, r) { + return + } if preQuery, ok := model.(APIPreQueryReader); ok && !preQuery.APIPreQueryRead(w, r) { } else { dAPIReadHandler(w, r, s) @@ -245,6 +248,10 @@ func dAPIHandler(w http.ResponseWriter, r *http.Request, s *Session) { return } if command == "add" { + // check if there is a prequery + if APIPreQueryAddHandler != nil && !APIPreQueryAddHandler(w, r) { + return + } if preQuery, ok := model.(APIPreQueryAdder); ok && !preQuery.APIPreQueryAdd(w, r) { } else { dAPIAddHandler(w, r, s) @@ -253,6 +260,9 @@ func dAPIHandler(w http.ResponseWriter, r *http.Request, s *Session) { } if command == "edit" { // check if there is a prequery + if APIPreQueryEditHandler != nil && !APIPreQueryEditHandler(w, r) { + return + } if preQuery, ok := model.(APIPreQueryEditor); ok && !preQuery.APIPreQueryEdit(w, r) { } else { dAPIEditHandler(w, r, s) @@ -261,6 +271,9 @@ func dAPIHandler(w http.ResponseWriter, r *http.Request, s *Session) { } if command == "delete" { // check if there is a prequery + if APIPreQueryDeleteHandler != nil && !APIPreQueryDeleteHandler(w, r) { + return + } if preQuery, ok := model.(APIPreQueryDeleter); ok && !preQuery.APIPreQueryDelete(w, r) { } else { dAPIDeleteHandler(w, r, s) diff --git a/d_api_helper.go b/d_api_helper.go index caf6005e..9bcb7b59 100644 --- a/d_api_helper.go +++ b/d_api_helper.go @@ -650,6 +650,9 @@ func returnDAPIJSON(w http.ResponseWriter, r *http.Request, a map[string]interfa if model != nil { if command == "read" { + if APIPostQueryReadHandler != nil && !APIPostQueryReadHandler(w, r, a) { + return nil + } if postQuery, ok := model.(APIPostQueryReader); ok { if !postQuery.APIPostQueryRead(w, r, a) { return nil @@ -657,6 +660,9 @@ func returnDAPIJSON(w http.ResponseWriter, r *http.Request, a map[string]interfa } } if command == "add" { + if APIPostQueryAddHandler != nil && !APIPostQueryAddHandler(w, r, a) { + return nil + } if postQuery, ok := model.(APIPostQueryAdder); ok { if !postQuery.APIPostQueryAdd(w, r, a) { return nil @@ -664,6 +670,9 @@ func returnDAPIJSON(w http.ResponseWriter, r *http.Request, a map[string]interfa } } if command == "edit" { + if APIPostQueryEditHandler != nil && !APIPostQueryEditHandler(w, r, a) { + return nil + } if postQuery, ok := model.(APIPostQueryEditor); ok { if !postQuery.APIPostQueryEdit(w, r, a) { return nil @@ -671,6 +680,9 @@ func returnDAPIJSON(w http.ResponseWriter, r *http.Request, a map[string]interfa } } if command == "delete" { + if APIPostQueryDeleteHandler != nil && !APIPostQueryDeleteHandler(w, r, a) { + return nil + } if postQuery, ok := model.(APIPostQueryDeleter); ok { if !postQuery.APIPostQueryDelete(w, r, a) { return nil @@ -684,7 +696,7 @@ func returnDAPIJSON(w http.ResponseWriter, r *http.Request, a map[string]interfa } } } - // if command == "schema" { + // if command == "method" { /* TODO: Add post query for methods if postQuery, ok := model.(APIPostQueryMethoder); ok { diff --git a/d_api_reset_password.go b/d_api_reset_password.go index 567e7f02..6d1c6dd9 100644 --- a/d_api_reset_password.go +++ b/d_api_reset_password.go @@ -1,6 +1,7 @@ package uadmin import ( + "fmt" "net/http" "time" ) @@ -52,9 +53,15 @@ func dAPIResetPasswordHandler(w http.ResponseWriter, r *http.Request, s *Session // check if the user exists and active if user.ID == 0 || (user.ExpiresOn != nil && user.ExpiresOn.After(time.Now())) { w.WriteHeader(404) + identifier := "email" + identifierVal := email + if uid != "" { + identifier = "uid" + identifierVal = uid + } ReturnJSON(w, r, map[string]interface{}{ "status": "error", - "err_msg": "email or uid do not match any active user", + "err_msg": fmt.Sprintf("%s: '%s' do not match any active user", identifier, identifierVal), }) // log the request go func() { diff --git a/global.go b/global.go index a69642a9..58ddd803 100644 --- a/global.go +++ b/global.go @@ -450,6 +450,30 @@ var FullMediaURL = false // MaskPasswordInAPI will replace any password fields with an asterisk mask var MaskPasswordInAPI = true +// APIPreQueryReadHandler is a function that runs before all dAPI read requests +var APIPreQueryReadHandler func(http.ResponseWriter, *http.Request) bool + +// APIPostQueryReadHandler is a function that runs after all dAPI read requests +var APIPostQueryReadHandler func(http.ResponseWriter, *http.Request, map[string]interface{}) bool + +// APIPreQueryAddHandler is a function that runs before all dAPI add requests +var APIPreQueryAddHandler func(http.ResponseWriter, *http.Request) bool + +// APIPostQueryAddHandler is a function that runs after all dAPI add requests +var APIPostQueryAddHandler func(http.ResponseWriter, *http.Request, map[string]interface{}) bool + +// APIPreQueryEditHandler is a function that runs before all dAPI edit requests +var APIPreQueryEditHandler func(http.ResponseWriter, *http.Request) bool + +// APIPostQueryEditHandler is a function that runs after all dAPI edit requests +var APIPostQueryEditHandler func(http.ResponseWriter, *http.Request, map[string]interface{}) bool + +// APIPreQueryDeleteHandler is a function that runs before all dAPI delete requests +var APIPreQueryDeleteHandler func(http.ResponseWriter, *http.Request) bool + +// APIPostQueryDeleteHandler is a function that runs after all dAPI delete requests +var APIPostQueryDeleteHandler func(http.ResponseWriter, *http.Request, map[string]interface{}) bool + // Private Global Variables // Regex var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") From 4919943d00defce68a0e66c0869f2ab4804d263f Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Sat, 4 Mar 2023 21:01:56 +0400 Subject: [PATCH 61/92] apply global pre read for dAPI --- d_api_read.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/d_api_read.go b/d_api_read.go index 9b863901..cb357a64 100644 --- a/d_api_read.go +++ b/d_api_read.go @@ -94,6 +94,12 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { args = append(args, lmArgs...) } } + if r.Context().Value(CKey("WHERE")) != nil { + if q != "" { + q += " AND " + } + q += r.Context().Value(CKey("WHERE")).(string) + } if q != "" { SQL += " WHERE " + q } @@ -228,7 +234,11 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { } else if len(urlParts) == 1 { // Read One m, _ := NewModel(modelName, true) - Get(m.Interface(), "id = ?", urlParts[0]) + q := "id = ?" + if r.Context().Value(CKey("WHERE")) != nil { + q += " AND " + r.Context().Value(CKey("WHERE")).(string) + } + Get(m.Interface(), q, urlParts[0]) rowsCount = 0 var i interface{} From 662fa05f416b02421e1f267589c10568dc84b2f2 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 6 Mar 2023 12:54:08 +0400 Subject: [PATCH 62/92] fix mysql date pointer for zero value --- db.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/db.go b/db.go index 8be2b5c5..06c1c673 100644 --- a/db.go +++ b/db.go @@ -464,14 +464,22 @@ func Save(a interface{}) (err error) { func fixDates(a interface{}) interface{} { value := reflect.ValueOf(a).Elem() - timeType := reflect.TypeOf(time.Now()) - timeValue := reflect.ValueOf(time.Now()) + now := time.Now() + timeType := reflect.TypeOf(now) + timePointerType := reflect.TypeOf(&now) + timeValue := reflect.ValueOf(now) + timePointerValue := reflect.ValueOf(now) for i := 0; i < value.NumField(); i++ { if value.Field(i).Type() == timeType { if value.Field(i).Interface().(time.Time).IsZero() { value.Field(i).Set(timeValue) } + } else if value.Field(i).Type() == timePointerType { + if value.Field(i).Interface() != nil && value.Field(i).Interface().(*time.Time).IsZero() { + value.Field(i).Set(timePointerValue) + } } + } return value.Addr().Interface() } From 49f019dccaf4ba21f7a23d29c12a963eb35c80cb Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 6 Mar 2023 13:01:27 +0400 Subject: [PATCH 63/92] office space lease migration --- db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db.go b/db.go index 06c1c673..70b8b796 100644 --- a/db.go +++ b/db.go @@ -468,7 +468,7 @@ func fixDates(a interface{}) interface{} { timeType := reflect.TypeOf(now) timePointerType := reflect.TypeOf(&now) timeValue := reflect.ValueOf(now) - timePointerValue := reflect.ValueOf(now) + timePointerValue := reflect.ValueOf(&now) for i := 0; i < value.NumField(); i++ { if value.Field(i).Type() == timeType { if value.Field(i).Interface().(time.Time).IsZero() { From 2beb5fa97338a7e070bc222bfb6297b8ad93664c Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 6 Mar 2023 13:24:33 +0400 Subject: [PATCH 64/92] fix mysql date pointer for zero value --- db.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/db.go b/db.go index 70b8b796..f0f1b06c 100644 --- a/db.go +++ b/db.go @@ -475,8 +475,10 @@ func fixDates(a interface{}) interface{} { value.Field(i).Set(timeValue) } } else if value.Field(i).Type() == timePointerType { - if value.Field(i).Interface() != nil && value.Field(i).Interface().(*time.Time).IsZero() { - value.Field(i).Set(timePointerValue) + if !value.Field(i).IsNil() { + if value.Field(i).Interface().(*time.Time).IsZero() { + value.Field(i).Set(timePointerValue) + } } } From ae051797d2dc500e45812f6f1a17d96e8aba38e0 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 7 Mar 2023 07:52:53 +0400 Subject: [PATCH 65/92] BUG FIX: get session from POST-like methods --- auth.go | 2 +- check_csrf.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/auth.go b/auth.go index 2740eb0b..850acdf7 100644 --- a/auth.go +++ b/auth.go @@ -580,7 +580,7 @@ func getSession(r *http.Request) string { if r.Method == "GET" && r.FormValue("session") != "" { return r.FormValue("session") } - if r.Method == "POST" { + if r.Method != "POST" { err := r.ParseMultipartForm(2 << 10) if err != nil { r.ParseForm() diff --git a/check_csrf.go b/check_csrf.go index 64f9c57c..99660e94 100644 --- a/check_csrf.go +++ b/check_csrf.go @@ -48,6 +48,7 @@ Where you replace `MY_SESSION_KEY` with the session key. */ func CheckCSRF(r *http.Request) bool { token := getCSRFToken(r) + Trail(DEBUG, "token: %s", token) if token != "" && token == getSession(r) { return false } From cdc9764f2e9e60f54b9ab600873e343eb50fb149 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 7 Mar 2023 07:54:16 +0400 Subject: [PATCH 66/92] BUG FIX: get session from POST-like methods --- auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth.go b/auth.go index 850acdf7..8de9190a 100644 --- a/auth.go +++ b/auth.go @@ -580,7 +580,7 @@ func getSession(r *http.Request) string { if r.Method == "GET" && r.FormValue("session") != "" { return r.FormValue("session") } - if r.Method != "POST" { + if r.Method != "GET" { err := r.ParseMultipartForm(2 << 10) if err != nil { r.ParseForm() From 2879f2d84959368c7fd854de985692150862c3f4 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 7 Mar 2023 07:55:51 +0400 Subject: [PATCH 67/92] BUG FIX: get session from POST-like methods --- check_csrf.go | 1 - 1 file changed, 1 deletion(-) diff --git a/check_csrf.go b/check_csrf.go index 99660e94..64f9c57c 100644 --- a/check_csrf.go +++ b/check_csrf.go @@ -48,7 +48,6 @@ Where you replace `MY_SESSION_KEY` with the session key. */ func CheckCSRF(r *http.Request) bool { token := getCSRFToken(r) - Trail(DEBUG, "token: %s", token) if token != "" && token == getSession(r) { return false } From 9982762575f5de6a5cbe7ee6a1f62de07866f565 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 7 Mar 2023 17:05:49 +0400 Subject: [PATCH 68/92] prelogin handler --- auth.go | 3 +++ global.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/auth.go b/auth.go index 8de9190a..7258ac2e 100644 --- a/auth.go +++ b/auth.go @@ -239,6 +239,9 @@ func getSessionFromRequest(r *http.Request) *Session { // Login return *User and a bool for Is OTP Required func Login(r *http.Request, username string, password string) (*Session, bool) { + if PreLoginHandler != nil { + PreLoginHandler(r, username, password) + } // Get the user from DB user := User{} Get(&user, "username = ?", username) diff --git a/global.go b/global.go index 58ddd803..441ac6eb 100644 --- a/global.go +++ b/global.go @@ -474,6 +474,9 @@ var APIPreQueryDeleteHandler func(http.ResponseWriter, *http.Request) bool // APIPostQueryDeleteHandler is a function that runs after all dAPI delete requests var APIPostQueryDeleteHandler func(http.ResponseWriter, *http.Request, map[string]interface{}) bool +// PreLoginHandler is a function that runs after all dAPI delete requests +var PreLoginHandler func(r *http.Request, username string, password string) + // Private Global Variables // Regex var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") From d03674577002c8be9e6d84a6c5e624396d5aa5de Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 3 Apr 2023 11:28:45 +0400 Subject: [PATCH 69/92] Adjust folder permissions to 0755 --- cmd/uadmin/copy.go | 6 +++--- cmd/uadmin/main.go | 2 +- crop_image_handler_test.go | 2 +- generate_translation.go | 4 ++-- otp.go | 2 +- process_upload.go | 4 ++-- server_test.go | 2 +- upload_image_handler.go | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cmd/uadmin/copy.go b/cmd/uadmin/copy.go index a86087b7..dabf2c10 100644 --- a/cmd/uadmin/copy.go +++ b/cmd/uadmin/copy.go @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2018 otiai10 +# Copyright (c) 2018 otiai10 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -57,7 +57,7 @@ func copy(src, dest string, info os.FileInfo) error { // and file permission. func fcopy(src, dest string) error { - if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil { + if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil { return err } @@ -86,7 +86,7 @@ func fcopy(src, dest string) error { // and pass everything to "copy" recursively. func dcopy(srcdir, destdir string) error { - if err := os.MkdirAll(destdir, os.FileMode(0744)); err != nil { + if err := os.MkdirAll(destdir, 0755); err != nil { return err } diff --git a/cmd/uadmin/main.go b/cmd/uadmin/main.go index 7be1c3f7..9b75b726 100644 --- a/cmd/uadmin/main.go +++ b/cmd/uadmin/main.go @@ -88,7 +88,7 @@ func main() { for _, v := range folderList { dst = filepath.Join(ex, v) if _, err = os.Stat(dst); os.IsNotExist(err) { - err = os.MkdirAll(dst, os.FileMode(0744)) + err = os.MkdirAll(dst, 0755) if err != nil { uadmin.Trail(uadmin.WARNING, "Unable to create \"%s\" folder: %s", v, err) } else { diff --git a/crop_image_handler_test.go b/crop_image_handler_test.go index eea1541f..5ccfe0a8 100644 --- a/crop_image_handler_test.go +++ b/crop_image_handler_test.go @@ -31,7 +31,7 @@ func (t *UAdminTests) TestCropImageHandler() { } } - os.MkdirAll("./media/user", 0744) + os.MkdirAll("./media/user", 0755) // Save to iamge.png f1, _ := os.OpenFile("./media/user/image_raw.png", os.O_WRONLY|os.O_CREATE, 0600) diff --git a/generate_translation.go b/generate_translation.go index 7aa860a8..cd08abd2 100644 --- a/generate_translation.go +++ b/generate_translation.go @@ -65,7 +65,7 @@ func syncCustomTranslation(path string) map[string]int { group := pathParts[0] name := pathParts[1] - os.MkdirAll("./static/i18n/"+group+"/", 0744) + os.MkdirAll("./static/i18n/"+group+"/", 0755) fileName := "./static/i18n/" + group + "/" + name + ".en.json" langMap := map[string]string{} if _, err = os.Stat(fileName); os.IsNotExist(err) { @@ -159,7 +159,7 @@ func syncModelTranslation(m ModelSchema) map[string]int { pkgName = strings.Split(pkgName, ".")[0] // Get the model's original language file - err = os.MkdirAll("./static/i18n/"+pkgName+"/", 0744) + err = os.MkdirAll("./static/i18n/"+pkgName+"/", 0755) if err != nil { Trail(ERROR, "generateTranslation error creating folder (./static/i18n/"+pkgName+"/). %v", err) diff --git a/otp.go b/otp.go index e170f8a6..13946b72 100644 --- a/otp.go +++ b/otp.go @@ -88,7 +88,7 @@ func generateOTPSeed(digits int, algorithm string, skew uint, period uint, user key, _ := totp.Generate(opts) img, _ := key.Image(250, 250) - os.MkdirAll("./media/otp/", 0744) + os.MkdirAll("./media/otp/", 0755) fName := "./media/otp/" + key.Secret() + ".png" for _, err := os.Stat(fName); os.IsExist(err); { diff --git a/process_upload.go b/process_upload.go index 73484757..94b379d0 100644 --- a/process_upload.go +++ b/process_upload.go @@ -65,7 +65,7 @@ func processUpload(r *http.Request, f *F, modelName string, session *Session, s uploadTo = f.UploadTo } if _, err = os.Stat("." + uploadTo); os.IsNotExist(err) { - err = os.MkdirAll("."+uploadTo, os.ModePerm) + err = os.MkdirAll("."+uploadTo, 0755) if err != nil { Trail(ERROR, "processForm.MkdirAll. %s", err) return "" @@ -103,7 +103,7 @@ func processUpload(r *http.Request, f *F, modelName string, session *Session, s // Sanitize the file name fName = pathName + path.Clean(fName) - err = os.MkdirAll(pathName, os.ModePerm) + err = os.MkdirAll(pathName, 0755) if err != nil { Trail(ERROR, "processForm.MkdirAll. unable to create folder for uploaded file. %s", err) return "" diff --git a/server_test.go b/server_test.go index e1de492d..e137c1bd 100644 --- a/server_test.go +++ b/server_test.go @@ -174,7 +174,7 @@ func TestRunner(t *testing.T) { t.Run(dbSetup.Name+"=GroupPermissions", func(t *testing.T) { uTest.TestGroupPermission() }) - t.Run(dbSetup.Name+"=HomeHamdler", func(t *testing.T) { + t.Run(dbSetup.Name+"=HomeHandler", func(t *testing.T) { uTest.TestHomeHandler() }) t.Run(dbSetup.Name+"=Language", func(t *testing.T) { diff --git a/upload_image_handler.go b/upload_image_handler.go index 2f103c55..9d05c486 100644 --- a/upload_image_handler.go +++ b/upload_image_handler.go @@ -20,7 +20,7 @@ func UploadImageHandler(w http.ResponseWriter, r *http.Request, session *Session } folderPath = "./media/htmlimages/" + GenerateBase64(24) + "/" } - os.MkdirAll(folderPath, 0744) + os.MkdirAll(folderPath, 0755) fileName := strings.Replace(f.Filename, "/", " ", -1) From a681eab76e16a85ab2bb5f350d43ac906aa2d1e1 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 3 Apr 2023 11:32:17 +0400 Subject: [PATCH 70/92] make version 0.9.6 --- global.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.go b/global.go index 441ac6eb..3577ed5c 100644 --- a/global.go +++ b/global.go @@ -81,7 +81,7 @@ const cEMAIL = "email" const cM2M = "m2m" // Version number as per Semantic Versioning 2.0.0 (semver.org) -const Version = "0.9.5" +const Version = "0.9.6" // VersionCodeName is the cool name we give to versions with significant changes. // This name should always be a bug's name starting from A-Z them revolving back. From 365febbf42989279f7d074e98de29df84eba20f9 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 3 Apr 2023 12:42:46 +0400 Subject: [PATCH 71/92] Add AutoMigrater Interface --- db.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/db.go b/db.go index f0f1b06c..e0dae6d7 100644 --- a/db.go +++ b/db.go @@ -73,6 +73,10 @@ type DBSettings struct { Timezone string `json:"timezone"` } +type AutoMigrater interface { + AutoMigrate() bool +} + // initializeDB opens the connection the DB func initializeDB(a ...interface{}) { // Open the connection the the DB @@ -80,6 +84,11 @@ func initializeDB(a ...interface{}) { // Migrate schema for i, model := range a { + if autoMigrate, ok := model.(AutoMigrater); ok { + if !autoMigrate.AutoMigrate() { + continue + } + } Trail(INFO, "Initializing DB: [%s%d/%d%s]", colors.FGGreenB, i+1, len(a), colors.FGNormal) err := db.AutoMigrate(model) if err != nil { From 842655583bdba104154b99c71ca9ac3ed8b8fd6f Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Mon, 24 Apr 2023 20:59:28 +0800 Subject: [PATCH 72/92] Add Post File Upload Handler --- global.go | 3 +++ process_upload.go | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/global.go b/global.go index 441ac6eb..555d8f36 100644 --- a/global.go +++ b/global.go @@ -477,6 +477,9 @@ var APIPostQueryDeleteHandler func(http.ResponseWriter, *http.Request, map[strin // PreLoginHandler is a function that runs after all dAPI delete requests var PreLoginHandler func(r *http.Request, username string, password string) +// PreLoginHandler is a function that runs after all dAPI delete requests +var PostUploadHandler func(filePath string) string + // Private Global Variables // Regex var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") diff --git a/process_upload.go b/process_upload.go index 73484757..39ce2268 100644 --- a/process_upload.go +++ b/process_upload.go @@ -255,5 +255,9 @@ func processUpload(r *http.Request, f *F, modelName string, session *Session, s os.RemoveAll(strings.Join(oldFileParts[0:len(oldFileParts)-1], "/")) } + if PostUploadHandler != nil { + val = PostUploadHandler(val) + } + return val } From 53a0e9685c33b489e4879b586edd9f3e7c06664c Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 26 Apr 2023 01:36:41 +0800 Subject: [PATCH 73/92] Change Post upload handler signature --- global.go | 2 +- process_upload.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/global.go b/global.go index f96784b8..2eb87adb 100644 --- a/global.go +++ b/global.go @@ -478,7 +478,7 @@ var APIPostQueryDeleteHandler func(http.ResponseWriter, *http.Request, map[strin var PreLoginHandler func(r *http.Request, username string, password string) // PreLoginHandler is a function that runs after all dAPI delete requests -var PostUploadHandler func(filePath string) string +var PostUploadHandler func(filePath string, modelName string, f *F) string // Private Global Variables // Regex diff --git a/process_upload.go b/process_upload.go index d5cae606..53bb1a99 100644 --- a/process_upload.go +++ b/process_upload.go @@ -256,7 +256,7 @@ func processUpload(r *http.Request, f *F, modelName string, session *Session, s } if PostUploadHandler != nil { - val = PostUploadHandler(val) + val = PostUploadHandler(val, modelName, f) } return val From 0d15f549217ded78cf89c45213a21e7512170164 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Sat, 29 Apr 2023 22:26:30 +0800 Subject: [PATCH 74/92] return 403 for failed CSRF --- d_api_add.go | 1 + d_api_delete.go | 1 + d_api_edit.go | 1 + d_api_logout.go | 2 +- d_api_method.go | 1 + 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/d_api_add.go b/d_api_add.go index b5b682bc..e8ef728a 100644 --- a/d_api_add.go +++ b/d_api_add.go @@ -20,6 +20,7 @@ func dAPIAddHandler(w http.ResponseWriter, r *http.Request, s *Session) { // Check CSRF if CheckCSRF(r) { + w.WriteHeader(http.StatusForbidden) ReturnJSON(w, r, map[string]interface{}{ "status": "error", "err_msg": "Failed CSRF protection.", diff --git a/d_api_delete.go b/d_api_delete.go index a69676c4..da766790 100644 --- a/d_api_delete.go +++ b/d_api_delete.go @@ -18,6 +18,7 @@ func dAPIDeleteHandler(w http.ResponseWriter, r *http.Request, s *Session) { // Check CSRF if CheckCSRF(r) { + w.WriteHeader(http.StatusForbidden) ReturnJSON(w, r, map[string]interface{}{ "status": "error", "err_msg": "Failed CSRF protection.", diff --git a/d_api_edit.go b/d_api_edit.go index 05db97ff..1cfe012e 100644 --- a/d_api_edit.go +++ b/d_api_edit.go @@ -16,6 +16,7 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) { // Check CSRF if CheckCSRF(r) { + w.WriteHeader(http.StatusForbidden) ReturnJSON(w, r, map[string]interface{}{ "status": "error", "err_msg": "Failed CSRF protection.", diff --git a/d_api_logout.go b/d_api_logout.go index 30eb7506..1dd276c0 100644 --- a/d_api_logout.go +++ b/d_api_logout.go @@ -13,7 +13,7 @@ func dAPILogoutHandler(w http.ResponseWriter, r *http.Request, s *Session) { } if CheckCSRF(r) { - w.WriteHeader(http.StatusUnauthorized) + w.WriteHeader(http.StatusForbidden) ReturnJSON(w, r, map[string]interface{}{ "status": "error", "err_msg": "Missing CSRF token", diff --git a/d_api_method.go b/d_api_method.go index 7f60a544..48d9b18e 100644 --- a/d_api_method.go +++ b/d_api_method.go @@ -24,6 +24,7 @@ func dAPIMethodHandler(w http.ResponseWriter, r *http.Request, s *Session) { } if CheckCSRF(r) { + w.WriteHeader(http.StatusForbidden) ReturnJSON(w, r, map[string]interface{}{ "status": "error", "err_msg": "Failed CSRF protection.", From 5d33a30c2c3a58dcdb708272f44b26f2a0bd041f Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Wed, 3 May 2023 00:13:32 +0800 Subject: [PATCH 75/92] change settings uploads to media by default --- params_to_instance.go | 60 +++++++++++++++++++++++++++++++++++++++++++ setting_handler.go | 2 +- 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 params_to_instance.go diff --git a/params_to_instance.go b/params_to_instance.go new file mode 100644 index 00000000..dfeaee8c --- /dev/null +++ b/params_to_instance.go @@ -0,0 +1,60 @@ +package uadmin + +// func setParams(params map[string]string, m reflect.Value, schema ModelSchema) (reflect.Value, error) { +// paramMap := map[string]interface{}{} +// for k, v := range params { +// key := k +// if key == "" { +// continue +// } +// if key[0] == '_' { +// key = key[1:] +// } +// f := schema.FieldByColumnName(key) +// if f != nil { +// key = f.Name +// } +// paramMap[key] = v + +// // fix value for numbers +// if f.Type == cNUMBER { +// if strings.HasPrefix(f.TypeName, "float") { +// paramMap[key], _ = strconv.ParseFloat(v, 64) +// } else if strings.HasPrefix(f.TypeName, "uint") { +// paramMap[key], _ = strconv.ParseUint(v, 10, 64) +// } else if strings.HasPrefix(f.TypeName, "int") { +// paramMap[key], _ = strconv.ParseInt(v, 10, 64) +// } +// } else if f.Type == cBOOL { +// if paramMap[key] == "true" || paramMap[key] == "1" { +// paramMap[key] = true +// } else { +// paramMap[key] = false +// } +// } else if f.Type == cLIST { +// paramMap[key], _ = strconv.ParseInt(v, 10, 64) +// } else if f.Type == cDATE { + +// } +// } + +// buf, _ := json.Marshal(params) +// var err error +// if m.Kind() == reflect.Pointer { +// err = json.Unmarshal(buf, m.Interface()) +// } else if m.Kind() == reflect.Struct { +// err = json.Unmarshal(buf, m.Addr().Interface()) +// } + +// return m, err +// } + +// func parseDate(v string) interface{} { +// if v == "" || v == "null" { +// return nil +// } +// dt, err := time.Parse("2006-05-04T15:02:01Z", v) +// if err != nil { +// return dt +// } +// } diff --git a/setting_handler.go b/setting_handler.go index 20b0a108..e6721849 100644 --- a/setting_handler.go +++ b/setting_handler.go @@ -63,7 +63,7 @@ func settingsHandler(w http.ResponseWriter, r *http.Request, session *Session) { schema, _ := getSchema(s) schema.FieldByName(sParts[1]) - f := F{Name: s.Code, Type: tMap[s.DataType], UploadTo: "/static/settings/"} + f := F{Name: s.Code, Type: tMap[s.DataType], UploadTo: "/media/settings/"} val := processUpload(r, &f, "setting", session, &schema) if val == "" { From 21d3b5134ad7b29b71bafc80440449c97f4a24ae Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 11 May 2023 12:47:36 +0800 Subject: [PATCH 76/92] cache for media and increase cache age for static --- media_handler.go | 37 +++++++++++++++++++++++++++++-------- static_handler.go | 2 +- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/media_handler.go b/media_handler.go index 719e1c38..b2078e9a 100644 --- a/media_handler.go +++ b/media_handler.go @@ -1,7 +1,6 @@ package uadmin import ( - "io" "net/http" "os" "path" @@ -15,18 +14,40 @@ func mediaHandler(w http.ResponseWriter, r *http.Request) { return } - r.URL.Path = strings.TrimPrefix(r.URL.Path, "/media/") - file, err := os.Open("./media/" + path.Clean(r.URL.Path)) + // r.URL.Path = strings.TrimPrefix(r.URL.Path, "/media/") + // file, err := os.Open("./media/" + path.Clean(r.URL.Path)) + // if err != nil { + // pageErrorHandler(w, r, session) + // return + // } + // io.Copy(w, file) + // file.Close() + + fName := path.Clean(r.URL.Path) + + f, err := os.Open("." + fName) if err != nil { - pageErrorHandler(w, r, session) + w.WriteHeader(404) + return + } + defer f.Close() + stat, err := os.Stat("." + fName) + if err != nil || stat.IsDir() { + w.WriteHeader(404) return } - io.Copy(w, file) - file.Close() + modTime := stat.ModTime() + if RetainMediaVersions { + w.Header().Add("Cache-Control", "private, max-age=604800") + } else { + w.Header().Add("Cache-Control", "private, max-age=3600") + } + + http.ServeContent(w, r, "."+fName, modTime, f) // Delete the file if exported to excel - if strings.HasPrefix(r.URL.Path, "export/") { - filePart := strings.TrimPrefix(r.URL.Path, "export/") + if strings.HasPrefix(fName, "/media/export/") { + filePart := strings.TrimPrefix(fName, "/media/export/") filePart = path.Clean(filePart) if filePart != "" && !strings.HasSuffix(filePart, "index.html") { os.Remove("./media/export/" + filePart) diff --git a/static_handler.go b/static_handler.go index 02260a9a..1b1ac833 100644 --- a/static_handler.go +++ b/static_handler.go @@ -71,7 +71,7 @@ func StaticHandler(w http.ResponseWriter, r *http.Request) { return } modTime = stat.ModTime() - w.Header().Add("Cache-Control", "private, max-age=3600") + w.Header().Add("Cache-Control", "private, max-age=604800") } else { modTime = time.Now() } From 8e46093f5ee64165f68d335f6a7dca5d54f3c5d8 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 11 May 2023 13:48:52 +0800 Subject: [PATCH 77/92] upgrade version to 0.10.0 --- global.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/global.go b/global.go index 2eb87adb..e2a9b377 100644 --- a/global.go +++ b/global.go @@ -81,7 +81,7 @@ const cEMAIL = "email" const cM2M = "m2m" // Version number as per Semantic Versioning 2.0.0 (semver.org) -const Version = "0.9.6" +const Version = "0.10.0" // VersionCodeName is the cool name we give to versions with significant changes. // This name should always be a bug's name starting from A-Z them revolving back. @@ -90,7 +90,8 @@ const Version = "0.9.6" // 0.7.0 Catterpiller // 0.8.0 Dragonfly // 0.9.0 Gnat -const VersionCodeName = "Gnat" +// 0.10.0 Gnat +const VersionCodeName = "Housefly" // Public Global Variables From ac0a5970c6520eab10864b58e58e3f5726fef2d8 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 25 May 2023 06:21:02 +0400 Subject: [PATCH 78/92] add compress JSON setting --- admin.go | 13 ++++++++++--- check_csrf.go | 2 +- generate_translation.go | 2 +- global.go | 3 +++ setting.go | 15 +++++++++++++++ 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/admin.go b/admin.go index 1f11fb44..bb6904d9 100644 --- a/admin.go +++ b/admin.go @@ -154,7 +154,7 @@ func toSnakeCase(str string) string { // JSONMarshal Generates JSON format from an object func JSONMarshal(v interface{}, safeEncoding bool) ([]byte, error) { // b, err := json.Marshal(v) - b, err := json.MarshalIndent(v, "", " ") + b, err := jsonMarash(v) if safeEncoding { b = bytes.Replace(b, []byte("\\u003c"), []byte("<"), -1) @@ -164,19 +164,26 @@ func JSONMarshal(v interface{}, safeEncoding bool) ([]byte, error) { return b, err } +func jsonMarash(v interface{}) ([]byte, error) { + if CompressJSON { + return json.Marshal(v) + } + return json.MarshalIndent(v, "", " ") +} + // ReturnJSON returns json to the client func ReturnJSON(w http.ResponseWriter, r *http.Request, v interface{}) { // Set content type in header w.Header().Set("Content-Type", "application/json") // Marshal content - b, err := json.MarshalIndent(v, "", " ") + b, err := jsonMarash(v) if err != nil { response := map[string]interface{}{ "status": "error", "error_msg": fmt.Sprintf("unable to encode JSON. %s", err), } - b, _ = json.MarshalIndent(response, "", " ") + b, _ = jsonMarash(response) w.Write(b) return } diff --git a/check_csrf.go b/check_csrf.go index 64f9c57c..fcb45f45 100644 --- a/check_csrf.go +++ b/check_csrf.go @@ -40,7 +40,7 @@ If you you call this API: It will return an error message and the system will create a CRITICAL level log with details about the possible attack. To make the request -work, `x-csrf-token` paramtere should be added. +work, `x-csrf-token` parameter should be added. http://0.0.0.0:8080/myapi/?x-csrf-token=MY_SESSION_KEY diff --git a/generate_translation.go b/generate_translation.go index cd08abd2..33c4d6c8 100644 --- a/generate_translation.go +++ b/generate_translation.go @@ -168,7 +168,7 @@ func syncModelTranslation(m ModelSchema) map[string]int { fileName := "./static/i18n/" + pkgName + "/" + m.ModelName + ".en.json" - // Check if the fist doesn't exist and create it + // Check if the first doesn't exist and create it if _, err = os.Stat(fileName); os.IsNotExist(err) { buf, _ = json.MarshalIndent(structLang, "", " ") err = ioutil.WriteFile(fileName, buf, 0644) diff --git a/global.go b/global.go index 3577ed5c..9a068a2b 100644 --- a/global.go +++ b/global.go @@ -477,6 +477,9 @@ var APIPostQueryDeleteHandler func(http.ResponseWriter, *http.Request, map[strin // PreLoginHandler is a function that runs after all dAPI delete requests var PreLoginHandler func(r *http.Request, username string, password string) +// CompressJSON is a variable that allows the user to reduce the size of JSON responses +var CompressJSON = false + // Private Global Variables // Regex var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") diff --git a/setting.go b/setting.go index 0b86a1af..e42e1ba6 100644 --- a/setting.go +++ b/setting.go @@ -287,6 +287,8 @@ func (s *Setting) ApplyValue() { Logo = v.(string) case "uAdmin.FavIcon": FavIcon = v.(string) + case "uAdmin.CompressJSON": + CompressJSON = v.(bool) } } @@ -821,6 +823,19 @@ func syncSystemSettings() { DataType: t.File(), Help: "the fav icon that shows on uAdmin UI", }, + { + Name: "Compress JSON", + Value: func(v bool) string { + n := 0 + if v { + n = 1 + } + return fmt.Sprint(n) + }(CompressJSON), + DefaultValue: "0", + DataType: t.Boolean(), + Help: "Compress JSON allows the system to reduce the size of json responses", + }, } // Prepare uAdmin Settings From 40ceef039024e2b05abb308126f869766354bf0c Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 25 May 2023 06:40:19 +0400 Subject: [PATCH 79/92] omit deletedAt in json response --- model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model.go b/model.go index 7f886063..c178e5ff 100644 --- a/model.go +++ b/model.go @@ -8,5 +8,5 @@ import ( // in any other struct to make it a model for uadmin type Model struct { ID uint `gorm:"primary_key"` - DeletedAt gorm.DeletedAt `sql:"index"` + DeletedAt gorm.DeletedAt `sql:"index" json:",omitempty"` } From b3cd4d24c8f8de1c67f7587a87bb7e0a401d97a3 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 25 May 2023 06:42:27 +0400 Subject: [PATCH 80/92] omit deletedAt in json response --- model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model.go b/model.go index c178e5ff..acae1e8b 100644 --- a/model.go +++ b/model.go @@ -8,5 +8,5 @@ import ( // in any other struct to make it a model for uadmin type Model struct { ID uint `gorm:"primary_key"` - DeletedAt gorm.DeletedAt `sql:"index" json:",omitempty"` + DeletedAt gorm.DeletedAt `sql:"index" json:"-"` } From 736e7904158f07fd3d3ff8a315be4e34d4925362 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 25 May 2023 07:27:26 +0400 Subject: [PATCH 81/92] omit all zero value structs in JSON --- admin.go | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/admin.go b/admin.go index c6ed85ae..20df79b1 100644 --- a/admin.go +++ b/admin.go @@ -164,11 +164,53 @@ func JSONMarshal(v interface{}, safeEncoding bool) ([]byte, error) { return b, err } +func nullZeroValueStructs(record map[string]interface{}) map[string]interface{} { + for k := range record { + switch v := record[k].(type) { + case map[string]interface{}: + if id, ok := v["ID"].(float64); ok && id == 0 { + record[k] = nil + } else if id, ok := v["id"].(float64); ok && id == 0 { + record[k] = nil + } else { + record[k] = nullZeroValueStructs(v) + } + } + } + return record +} + +func removeZeroValueStructs(buf []byte) []byte { + response := map[string]interface{}{} + json.Unmarshal(buf, &response) + if _, ok := response["result"].([]interface{}); !ok { + return buf + } + val := response["result"].([]interface{}) + var record map[string]interface{} + for i := range val { + record = val[i].(map[string]interface{}) + record = nullZeroValueStructs(record) + val[i] = record + } + response["result"] = val + buf, _ = json.Marshal(response) + return buf +} + func jsonMarshal(v interface{}) ([]byte, error) { + var buf []byte + var err error if CompressJSON { - return json.Marshal(v) + buf, err = json.Marshal(v) + if err == nil { + buf = removeZeroValueStructs(buf) + } + } else { + buf, err = json.MarshalIndent(v, "", " ") } - return json.MarshalIndent(v, "", " ") + + return buf, err } // ReturnJSON returns json to the client From 58751ade1ed35a1aa45343140f3982fccf82e3da Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 25 May 2023 08:59:39 +0400 Subject: [PATCH 82/92] omit all zero value structs in JSON for read one --- admin.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/admin.go b/admin.go index 20df79b1..f7b6f863 100644 --- a/admin.go +++ b/admin.go @@ -183,6 +183,11 @@ func nullZeroValueStructs(record map[string]interface{}) map[string]interface{} func removeZeroValueStructs(buf []byte) []byte { response := map[string]interface{}{} json.Unmarshal(buf, &response) + if val, ok := response["result"].(map[string]interface{}); ok { + val = nullZeroValueStructs(val) + buf, _ = json.Marshal(val) + return buf + } if _, ok := response["result"].([]interface{}); !ok { return buf } From 3b2c5338c0343cfc99ccdcf5011d853f3dce4d8a Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Thu, 25 May 2023 18:15:24 +0400 Subject: [PATCH 83/92] omit all zero value structs in JSON for read one --- admin.go | 2 +- global.go | 3 +++ setting.go | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/admin.go b/admin.go index f7b6f863..ed2a5880 100644 --- a/admin.go +++ b/admin.go @@ -208,7 +208,7 @@ func jsonMarshal(v interface{}) ([]byte, error) { var err error if CompressJSON { buf, err = json.Marshal(v) - if err == nil { + if err == nil && RemoveZeroValueJSON { buf = removeZeroValueStructs(buf) } } else { diff --git a/global.go b/global.go index 5ebef507..b55800bd 100644 --- a/global.go +++ b/global.go @@ -484,6 +484,9 @@ var PostUploadHandler func(filePath string, modelName string, f *F) string // CompressJSON is a variable that allows the user to reduce the size of JSON responses var CompressJSON = false +// CompressJSON is a variable that allows the user to reduce the size of JSON responses +var RemoveZeroValueJSON = false + // Private Global Variables // Regex var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") diff --git a/setting.go b/setting.go index e42e1ba6..bf5fc771 100644 --- a/setting.go +++ b/setting.go @@ -289,6 +289,8 @@ func (s *Setting) ApplyValue() { FavIcon = v.(string) case "uAdmin.CompressJSON": CompressJSON = v.(bool) + case "uAdmin.RemoveZeroValueJSON": + RemoveZeroValueJSON = v.(bool) } } @@ -836,6 +838,19 @@ func syncSystemSettings() { DataType: t.Boolean(), Help: "Compress JSON allows the system to reduce the size of json responses", }, + { + Name: "Remove Zero Value JSON", + Value: func(v bool) string { + n := 0 + if v { + n = 1 + } + return fmt.Sprint(n) + }(RemoveZeroValueJSON), + DefaultValue: "0", + DataType: t.Boolean(), + Help: "Compress JSON allows the system to reduce the size of json responses", + }, } // Prepare uAdmin Settings From dea1d9c3f0dd2015f821f093a34f74bc396c8f22 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Fri, 2 Jun 2023 10:13:42 +0400 Subject: [PATCH 84/92] BUG FIX: return empty string if the object is nil --- representation.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/representation.go b/representation.go index 8373bafe..6bf25512 100644 --- a/representation.go +++ b/representation.go @@ -19,7 +19,10 @@ func GetID(m reflect.Value) uint { func GetString(a interface{}) string { str, ok := a.(fmt.Stringer) if ok { - return str.String() + if a != nil { + return str.String() + } + return "" } t := reflect.TypeOf(a) v := reflect.ValueOf(a) From 3e0abee2c080b7260ff83f640a2ced8ab6249615 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 27 Jun 2023 14:02:33 +0400 Subject: [PATCH 85/92] return 404 when dAPI read one doesn't find a record --- d_api_read.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/d_api_read.go b/d_api_read.go index cb357a64..8ab946e5 100644 --- a/d_api_read.go +++ b/d_api_read.go @@ -245,6 +245,8 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) { if int(GetID(m)) != 0 { i = m.Interface() rowsCount = 1 + } else { + w.WriteHeader(404) } if params["$preload"] == "1" || params["$preload"] == "true" { From 1c93616f4855e45dc7339940ab1b68f1fa1551ce Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Tue, 27 Jun 2023 14:04:50 +0400 Subject: [PATCH 86/92] change the version to v0.10.1 --- global.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.go b/global.go index b55800bd..16497d76 100644 --- a/global.go +++ b/global.go @@ -81,7 +81,7 @@ const cEMAIL = "email" const cM2M = "m2m" // Version number as per Semantic Versioning 2.0.0 (semver.org) -const Version = "0.10.0" +const Version = "0.10.1" // VersionCodeName is the cool name we give to versions with significant changes. // This name should always be a bug's name starting from A-Z them revolving back. From 97dc027bb6d3bce7392679c4a3af60955d6fb348 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Fri, 7 Jul 2023 10:01:51 +0400 Subject: [PATCH 87/92] JWT does not need session for CSRF --- auth.go | 240 ++++++++++++++++++++++++++------------------------ check_csrf.go | 3 + 2 files changed, 130 insertions(+), 113 deletions(-) diff --git a/auth.go b/auth.go index 7258ac2e..eae748cc 100644 --- a/auth.go +++ b/auth.go @@ -575,140 +575,154 @@ func getSessionByKey(key string) *Session { return &s } -func getSession(r *http.Request) string { - key, err := r.Cookie("session") - if err == nil && key != nil { - return key.Value +func getJWT(r *http.Request) string { + // JWT + if r.Header.Get("Authorization") == "" { + return "" } - if r.Method == "GET" && r.FormValue("session") != "" { - return r.FormValue("session") + if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer") { + return "" } - if r.Method != "GET" { - err := r.ParseMultipartForm(2 << 10) - if err != nil { - r.ParseForm() - } - if r.FormValue("session") != "" { - return r.FormValue("session") - } + + jwt := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + jwtParts := strings.Split(jwt, ".") + + if len(jwtParts) != 3 { + return "" } - // JWT - if r.Header.Get("Authorization") != "" { - if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer") { - jwt := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") - jwtParts := strings.Split(jwt, ".") - if len(jwtParts) != 3 { - return "" - } + jHeader, err := base64.RawURLEncoding.WithPadding(base64.NoPadding).DecodeString(jwtParts[0]) + if err != nil { + return "" + } + jPayload, err := base64.RawURLEncoding.WithPadding(base64.NoPadding).DecodeString(jwtParts[1]) + if err != nil { + return "" + } - jHeader, err := base64.RawURLEncoding.WithPadding(base64.NoPadding).DecodeString(jwtParts[0]) - if err != nil { - return "" + header := map[string]interface{}{} + err = json.Unmarshal(jHeader, &header) + if err != nil { + return "" + } + + // Get data from payload + payload := map[string]interface{}{} + err = json.Unmarshal(jPayload, &payload) + if err != nil { + return "" + } + + // Verify issuer + if iss, ok := payload["iss"].(string); ok { + if iss != JWTIssuer { + accepted := false + for _, fiss := range AcceptedJWTIssuers { + if fiss == iss { + accepted = true + break + } } - jPayload, err := base64.RawURLEncoding.WithPadding(base64.NoPadding).DecodeString(jwtParts[1]) - if err != nil { + if !accepted { return "" } + } + } else { + return "" + } - header := map[string]interface{}{} - err = json.Unmarshal(jHeader, &header) - if err != nil { - return "" + // verify audience + if aud, ok := payload["aud"].(string); ok { + if aud != JWTIssuer { + return "" + } + } else if aud, ok := payload["aud"].([]string); ok { + accepted := false + for _, audItem := range aud { + if audItem == JWTIssuer { + accepted = true + break } + } + if !accepted { + return "" + } + } else { + return "" + } - // Get data from payload - payload := map[string]interface{}{} - err = json.Unmarshal(jPayload, &payload) - if err != nil { - return "" - } + // if there is no subject, return empty session + if _, ok := payload["sub"].(string); !ok { + return "" + } - // Verify issuer - if iss, ok := payload["iss"].(string); ok { - if iss != JWTIssuer { - accepted := false - for _, fiss := range AcceptedJWTIssuers { - if fiss == iss { - accepted = true - break - } - } - if !accepted { - return "" - } - } - } else { - return "" - } + sub := payload["sub"].(string) + user := User{} + Get(&user, "username = ?", sub) - // verify audience - if aud, ok := payload["aud"].(string); ok { - if aud != JWTIssuer { - return "" - } - } else if aud, ok := payload["aud"].([]string); ok { - accepted := false - for _, audItem := range aud { - if audItem == JWTIssuer { - accepted = true - break - } - } - if !accepted { - return "" - } - } else { - return "" - } + if user.ID == 0 { + return "" + } - // if there is no subject, return empty session - if _, ok := payload["sub"].(string); !ok { - return "" - } + session := user.GetActiveSession() + if session == nil { + return "" + } - sub := payload["sub"].(string) - user := User{} - Get(&user, "username = ?", sub) + // TODO: verify exp - if user.ID == 0 { - return "" - } + // Verify the signature + alg := "HS256" + if v, ok := header["alg"].(string); ok { + alg = v + } + if _, ok := header["typ"]; ok { + if v, ok := header["typ"].(string); !ok || v != "JWT" { + return "" + } + } + switch alg { + case "HS256": + // TODO: allow third party JWT signature authentication + hash := hmac.New(sha256.New, []byte(JWT+session.Key)) + hash.Write([]byte(jwtParts[0] + "." + jwtParts[1])) + token := hash.Sum(nil) + b64Token := base64.RawURLEncoding.EncodeToString(token) + if b64Token != jwtParts[2] { + return "" + } + default: + // For now, only support HMAC-SHA256 + return "" + } + return session.Key - session := user.GetActiveSession() - if session == nil { - return "" - } +} - // TODO: verify exp +func getSession(r *http.Request) string { + // First, try JWT + if val := getJWT(r); val != "" { + return val + } - // Verify the signature - alg := "HS256" - if v, ok := header["alg"].(string); ok { - alg = v - } - if _, ok := header["typ"]; ok { - if v, ok := header["typ"].(string); !ok || v != "JWT" { - return "" - } - } - switch alg { - case "HS256": - // TODO: allow third party JWT signature authentication - hash := hmac.New(sha256.New, []byte(JWT+session.Key)) - hash.Write([]byte(jwtParts[0] + "." + jwtParts[1])) - token := hash.Sum(nil) - b64Token := base64.RawURLEncoding.EncodeToString(token) - if b64Token != jwtParts[2] { - return "" - } - default: - // For now, only support HMAC-SHA256 - return "" - } - return session.Key + // Then try session + key, err := r.Cookie("session") + if err == nil && key != nil { + return key.Value + } + if r.Method == "GET" && r.FormValue("session") != "" { + return r.FormValue("session") + } + if r.Method != "GET" { + err := r.ParseMultipartForm(2 << 10) + if err != nil { + r.ParseForm() + } + if r.FormValue("session") != "" { + return r.FormValue("session") } } + return "" } diff --git a/check_csrf.go b/check_csrf.go index fcb45f45..5036244f 100644 --- a/check_csrf.go +++ b/check_csrf.go @@ -47,6 +47,9 @@ work, `x-csrf-token` parameter should be added. Where you replace `MY_SESSION_KEY` with the session key. */ func CheckCSRF(r *http.Request) bool { + if getJWT(r) != "" { + return false + } token := getCSRFToken(r) if token != "" && token == getSession(r) { return false From 71ff8369060798d3e91d0fe9608351ca50114dc8 Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Sat, 8 Jul 2023 21:17:38 +0400 Subject: [PATCH 88/92] OpenID Connect for SSO --- auth.go | 249 +++++++++++++++++-- d_api_auth.go | 4 + d_api_openid_cert_handler.go | 45 ++++ d_api_openid_login.go | 71 ++++++ global.go | 3 + go.mod | 1 + go.sum | 2 + login_handler.go | 7 + main_handler.go | 5 + openid_config_handler.go | 50 ++++ register.go | 4 + templates/uadmin/default/login.html | 11 + templates/uadmin/default/openid_concent.html | 79 ++++++ 13 files changed, 516 insertions(+), 15 deletions(-) create mode 100644 d_api_openid_cert_handler.go create mode 100644 d_api_openid_login.go create mode 100644 openid_config_handler.go create mode 100644 templates/uadmin/default/openid_concent.html diff --git a/auth.go b/auth.go index eae748cc..8a57fa52 100644 --- a/auth.go +++ b/auth.go @@ -4,12 +4,16 @@ import ( "context" "encoding/base64" "encoding/json" + "fmt" + "io" "math/big" "net" + "os" "path" "crypto/hmac" "crypto/rand" + "crypto/rsa" "crypto/sha256" "math" "net/http" @@ -17,6 +21,7 @@ import ( "strings" "time" + "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) @@ -36,6 +41,8 @@ var JWT = "" // used to identify the as JWT audience. var JWTIssuer = "" +var JWTAlgo = "HS256" //"RS256" + // AcceptedJWTIssuers is a list of accepted JWT issuers. By default the // local JWTIssuer is accepted. To accept other issuers, add them to // this list @@ -157,15 +164,20 @@ func createJWT(r *http.Request, s *Session) string { if !isValidSession(r, s) { return "" } + alg := JWTAlgo + aud := JWTIssuer + if r.Context().Value(CKey("aud")) != nil { + aud = r.Context().Value(CKey("aud")).(string) + } header := map[string]interface{}{ - "alg": "HS256", + "alg": alg, "typ": "JWT", } payload := map[string]interface{}{ "sub": s.User.Username, "iat": s.LastLogin.Unix(), "iss": JWTIssuer, - "aud": JWTIssuer, + "aud": aud, } if s.ExpiresOn != nil { payload["exp"] = s.ExpiresOn.Unix() @@ -176,16 +188,44 @@ func createJWT(r *http.Request, s *Session) string { payload = CustomJWT(r, s, payload) } - jHeader, _ := json.Marshal(header) - jPayload, _ := json.Marshal(payload) - b64Header := base64.RawURLEncoding.EncodeToString(jHeader) - b64Payload := base64.RawURLEncoding.EncodeToString(jPayload) + if alg == "HS256" { + jHeader, _ := json.Marshal(header) + jPayload, _ := json.Marshal(payload) + b64Header := base64.RawURLEncoding.EncodeToString(jHeader) + b64Payload := base64.RawURLEncoding.EncodeToString(jPayload) + + hash := hmac.New(sha256.New, []byte(JWT+s.Key)) + hash.Write([]byte(b64Header + "." + b64Payload)) + signature := hash.Sum(nil) + b64Signature := base64.RawURLEncoding.EncodeToString(signature) + return b64Header + "." + b64Payload + "." + b64Signature + } else if alg == "RS256" { + buf, err := os.ReadFile(".jwt-rsa-private.pem") + if err != nil { + return "" + } + key, err := jwt.ParseRSAPrivateKeyFromPEM(buf) + if err != nil { + return "" + } + header["kid"] = "1" + token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(payload)) + + for k, v := range header { + token.Header[k] = v + } + + tokenRaw, err := token.SignedString(key) + + if err != nil { + return "" + } + return tokenRaw + } else { + Trail(ERROR, "Unknown algorithm for JWT (%s)", alg) + return "" + } - hash := hmac.New(sha256.New, []byte(JWT+s.Key)) - hash.Write([]byte(b64Header + "." + b64Payload)) - signature := hash.Sum(nil) - b64Signature := base64.RawURLEncoding.EncodeToString(signature) - return b64Header + "." + b64Payload + "." + b64Signature } func isValidSession(r *http.Request, s *Session) bool { @@ -584,8 +624,8 @@ func getJWT(r *http.Request) string { return "" } - jwt := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") - jwtParts := strings.Split(jwt, ".") + jwtToken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + jwtParts := strings.Split(jwtToken, ".") if len(jwtParts) != 3 { return "" @@ -614,10 +654,12 @@ func getJWT(r *http.Request) string { } // Verify issuer + SSOLogin := false if iss, ok := payload["iss"].(string); ok { if iss != JWTIssuer { accepted := false for _, fiss := range AcceptedJWTIssuers { + Trail(DEBUG, "fiss:%s, iss:%s", fiss, iss) if fiss == iss { accepted = true break @@ -626,6 +668,7 @@ func getJWT(r *http.Request) string { if !accepted { return "" } + SSOLogin = true } } else { return "" @@ -660,12 +703,31 @@ func getJWT(r *http.Request) string { user := User{} Get(&user, "username = ?", sub) - if user.ID == 0 { + if user.ID == 0 && SSOLogin { + user := User{ + Username: sub, + FirstName: sub, + Active: true, + Admin: true, + RemoteAccess: true, + Password: GenerateBase64(64), + } + user.Save() + } else if user.ID == 0 { return "" } session := user.GetActiveSession() - if session == nil { + if session == nil && SSOLogin { + session = &Session{ + UserID: user.ID, + Active: true, + LoginTime: time.Now(), + IP: GetRemoteIP(r), + } + session.GenerateKey() + session.Save() + } else if session == nil { return "" } @@ -681,6 +743,7 @@ func getJWT(r *http.Request) string { return "" } } + // verify signature switch alg { case "HS256": // TODO: allow third party JWT signature authentication @@ -691,20 +754,176 @@ func getJWT(r *http.Request) string { if b64Token != jwtParts[2] { return "" } + case "RS256": + if !verifyRSA(jwtToken, SSOLogin) { + return "" + } default: // For now, only support HMAC-SHA256 return "" } + return session.Key } +var jwtIssuerCerts = map[[2]string][]byte{} + +func getJWTRSAPublicKeySSO(jwtToken *jwt.Token) *rsa.PublicKey { + iss, err := jwtToken.Claims.GetIssuer() + if err != nil { + return nil + } + + kid, _ := jwtToken.Header["kid"].(string) + if kid == "" { + return nil + } + + if val, ok := jwtIssuerCerts[[2]string{iss, kid}]; ok { + cert, _ := jwt.ParseRSAPublicKeyFromPEM(val) + return cert + } + + res, err := http.Get(iss + "/.well-known/openid-configuration") + if err != nil { + return nil + } + + if res.StatusCode != 200 { + return nil + } + + buf, err := io.ReadAll(res.Body) + if err != nil { + return nil + } + + obj := map[string]interface{}{} + err = json.Unmarshal(buf, &obj) + if err != nil { + return nil + } + + crtURL := "" + if val, ok := obj["jwks_uri"].(string); !ok || val == "" { + return nil + } else { + crtURL = val + } + + res, err = http.Get(crtURL) + if err != nil { + return nil + } + + if res.StatusCode != 200 { + return nil + } + + buf, err = io.ReadAll(res.Body) + if err != nil { + return nil + } + + certObj := map[string][]map[string]string{} + err = json.Unmarshal(buf, &certObj) + if err != nil { + return nil + } + + if val, ok := certObj["keys"]; !ok || len(val) == 0 { + return nil + } + + var cert map[string]string + for i := range certObj["keys"] { + if certObj["keys"][i]["kid"] == kid { + cert = certObj["keys"][i] + break + } + } + + if cert == nil { + return nil + } + + N := new(big.Int) + buf, _ = base64.RawURLEncoding.DecodeString(cert["n"]) + N = N.SetBytes(buf) + + E := new(big.Int) + buf, _ = base64.RawURLEncoding.DecodeString(cert["e"]) + E = E.SetBytes(buf) + publicCert := rsa.PublicKey{ + N: N, + E: int(E.Int64()), + } + + Trail(DEBUG, publicCert) + + return &publicCert +} + +func getJWTRSAPublicKeyLocal(jwtToken *jwt.Token) *rsa.PublicKey { + pubKeyPEM, err := os.ReadFile(".jwt-rsa-public.pem") + if err != nil { + return nil + } + + pubKey, err := jwt.ParseRSAPublicKeyFromPEM(pubKeyPEM) + if err != nil { + return nil + } + + return pubKey +} + +func verifyRSA(token string, SSOLogin bool) bool { + tok, err := jwt.Parse(token, func(jwtToken *jwt.Token) (interface{}, error) { + if _, ok := jwtToken.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected method: %s", jwtToken.Header["alg"]) + } + + var pubKey *rsa.PublicKey + + if SSOLogin { + pubKey = getJWTRSAPublicKeySSO(jwtToken) + } else { + pubKey = getJWTRSAPublicKeyLocal(jwtToken) + } + + if pubKey == nil { + return nil, fmt.Errorf("Unable to load local public key") + } + + return pubKey, nil + }) + if err != nil { + return false + } + + _, ok := tok.Claims.(jwt.MapClaims) + if !ok || !tok.Valid { + return false + } + + return true +} + func getSession(r *http.Request) string { // First, try JWT if val := getJWT(r); val != "" { return val } + if r.URL.Query().Get("access-token") != "" { + r.Header.Add("Authorization", "Bearer "+r.URL.Query().Get("access-token")) + if val := getJWT(r); val != "" { + return val + } + } + // Then try session key, err := r.Cookie("session") if err == nil && key != nil { diff --git a/d_api_auth.go b/d_api_auth.go index 6481486c..02c2af2d 100644 --- a/d_api_auth.go +++ b/d_api_auth.go @@ -31,6 +31,10 @@ func dAPIAuthHandler(w http.ResponseWriter, r *http.Request, s *Session) { dAPIResetPasswordHandler(w, r, s) case "changepassword": dAPIChangePasswordHandler(w, r, s) + case "openidlogin": + dAPIOpenIDLoginHandler(w, r, s) + case "certs": + dAPIOpenIDCertHandler(w, r) default: w.WriteHeader(http.StatusNotFound) ReturnJSON(w, r, map[string]interface{}{ diff --git a/d_api_openid_cert_handler.go b/d_api_openid_cert_handler.go new file mode 100644 index 00000000..9b82e788 --- /dev/null +++ b/d_api_openid_cert_handler.go @@ -0,0 +1,45 @@ +package uadmin + +import ( + "encoding/base64" + "math/big" + "net/http" + "os" + + "github.com/golang-jwt/jwt/v5" +) + +func dAPIOpenIDCertHandler(w http.ResponseWriter, r *http.Request) { + buf, err := os.ReadFile(".jwt-rsa-public.pem") + if err != nil { + w.WriteHeader(404) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "Unable to load public certificate", + }) + return + } + cert, err := jwt.ParseRSAPublicKeyFromPEM(buf) + if err != nil { + w.WriteHeader(404) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "Unable to parse public certificate", + }) + return + } + obj := map[string][]map[string]string{ + "keys": { + { + "kid": "1", + "use": "sig", + "kty": "RSA", + "alg": "RS256", + "e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(cert.E)).Bytes()), + "n": base64.RawURLEncoding.EncodeToString(cert.N.Bytes()), + }, + }, + } + + ReturnJSON(w, r, obj) +} diff --git a/d_api_openid_login.go b/d_api_openid_login.go new file mode 100644 index 00000000..50bbc067 --- /dev/null +++ b/d_api_openid_login.go @@ -0,0 +1,71 @@ +package uadmin + +import ( + "context" + "net/http" + "strings" +) + +func dAPIOpenIDLoginHandler(w http.ResponseWriter, r *http.Request, s *Session) { + _ = s + redirectURI := r.FormValue("redirect_uri") + + if r.Method == "GET" { + if session := IsAuthenticated(r); session != nil { + Preload(session, "User") + c := map[string]interface{}{ + "SiteName": SiteName, + "Language": getLanguage(r), + "RootURL": RootURL, + "Logo": Logo, + "user": s.User, + "OpenIDWebsiteURL": redirectURI, + } + RenderHTML(w, r, "./templates/uadmin/"+Theme+"/openid_concent.html", c) + return + } + + http.Redirect(w, r, RootURL+"login/?next="+RootURL+"api/d/auth/openidlogin?"+r.URL.Query().Encode(), 303) + return + } + + if s == nil { + w.WriteHeader(http.StatusUnauthorized) + ReturnJSON(w, r, map[string]interface{}{ + "status": "error", + "err_msg": "Invalid credentials", + }) + return + } + + // Preload the user to get the group name + Preload(&s.User) + + ctx := context.WithValue(r.Context(), CKey("aud"), getAUD(redirectURI)) + r = r.WithContext(ctx) + jwt := createJWT(r, s) + + http.Redirect(w, r, redirectURI+"?access-token="+jwt, 303) + +} + +func getAUD(URL string) string { + aud := "" + + if strings.HasPrefix(URL, "https://") { + aud = "https://" + URL = strings.TrimPrefix(URL, "https://") + } + + if strings.HasPrefix(URL, "http://") { + aud = "http://" + URL = strings.TrimPrefix(URL, "http://") + } + + if strings.Contains(URL, "/") { + URL = URL[:strings.Index(URL, "/")] + aud += URL + } + + return aud +} diff --git a/global.go b/global.go index 16497d76..e923b301 100644 --- a/global.go +++ b/global.go @@ -487,6 +487,9 @@ var CompressJSON = false // CompressJSON is a variable that allows the user to reduce the size of JSON responses var RemoveZeroValueJSON = false +// SSOURL enables SSO using OpenID Connect +var SSOURL = "" + // Private Global Variables // Regex var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") diff --git a/go.mod b/go.mod index 4c2ac87f..f9aa324a 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( require ( github.com/boombuler/barcode v1.0.1 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.3.0 // indirect diff --git a/go.sum b/go.sum index 9a1dc4dc..5dea4bf6 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ 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/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= diff --git a/login_handler.go b/login_handler.go index 19053791..7173dcfc 100644 --- a/login_handler.go +++ b/login_handler.go @@ -19,6 +19,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { Password string Logo string FavIcon string + SSOURL string } c := Context{} @@ -27,6 +28,12 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { c.Language = getLanguage(r) c.Logo = Logo c.FavIcon = FavIcon + c.SSOURL = SSOURL + + if session := IsAuthenticated(r); session != nil { + session = session.User.GetActiveSession() + SetSessionCookie(w, r, session) + } if r.Method == cPOST { if r.FormValue("save") == "Send Request" { diff --git a/main_handler.go b/main_handler.go index e17bef0e..3b83a637 100644 --- a/main_handler.go +++ b/main_handler.go @@ -41,6 +41,7 @@ func mainHandler(w http.ResponseWriter, r *http.Request) { // This session is preloaded with a user session := IsAuthenticated(r) if session == nil { + Trail(DEBUG, "no auth, Login page") loginHandler(w, r) return } @@ -80,6 +81,10 @@ func mainHandler(w http.ResponseWriter, r *http.Request) { settingsHandler(w, r, session) return } + if URLParts[0] == "login" { + loginHandler(w, r) + return + } listHandler(w, r, session) return } else if len(URLParts) == 2 { diff --git a/openid_config_handler.go b/openid_config_handler.go new file mode 100644 index 00000000..f22d2e4a --- /dev/null +++ b/openid_config_handler.go @@ -0,0 +1,50 @@ +package uadmin + +import "net/http" + +func JWTConfigHandler(w http.ResponseWriter, r *http.Request) { + data := map[string]interface{}{ + "issuer": JWTIssuer, + "authorization_endpoint": JWTIssuer + "/api/d/auth/openidlogin", + "token_endpoint": "", + "userinfo_endpoint": JWTIssuer + "/api/d/auth/userinfo", + "jwks_uri": JWTIssuer + "/api/d/auth/certs", + "scopes_supported": []string{ + "openid", + "email", + "profile", + }, + "response_types_supported": []string{ + "code", + "token", + "id_token", + "code token", + "code id_token", + "token id_token", + "code token id_token", + "none", + }, + "subject_types_supported": []string{ + "public", + }, + "id_token_signing_alg_values_supported": []string{ + "RS256", + }, + "claims_supported": []string{ + "aud", + "email", + "email_verified", + "exp", + "family_name", + "given_name", + "iat", + "iss", + "locale", + "name", + "picture", + "sub", + }, + } + + ReturnJSON(w, r, data) +} diff --git a/register.go b/register.go index fa7586c1..a90d7bd4 100644 --- a/register.go +++ b/register.go @@ -382,5 +382,9 @@ func registerHandlers() { http.HandleFunc(RootURL+"api/", Handler(apiHandler)) } + if !DisableDAPIAuth { + http.HandleFunc(RootURL+".well-known/openid-configuration/", Handler(JWTConfigHandler)) + } + handlersRegistered = true } diff --git a/templates/uadmin/default/login.html b/templates/uadmin/default/login.html index dca60b1b..76ca0f0d 100644 --- a/templates/uadmin/default/login.html +++ b/templates/uadmin/default/login.html @@ -95,6 +95,7 @@

{{Tf "uadmin/system" .La Forgot Password + {{if .SSOURL}}SSO Login{{end}}
{{if .ErrExists}}
@@ -150,6 +151,16 @@

+ + +
+ +
+ +
+
+
+ +
+
+
+ +
+
+
+ + + + +

+ Click Continue +

+

+ to login to {{.OpenIDWebsiteURL}} as {{.user.Username}} +

+
+
+
+ +
+ + +
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + \ No newline at end of file From 4f7e266154f8a2cca4ae3dd2e61fb79a4f011d7d Mon Sep 17 00:00:00 2001 From: Abdullah Alrasheed Date: Fri, 21 Jul 2023 12:09:40 +0400 Subject: [PATCH 89/92] updates for SSO for browser compatibility --- auth.go | 3 --- d_api_openid_login.go | 2 ++ login_handler.go | 3 +++ main_handler.go | 1 - templates/uadmin/default/login.html | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/auth.go b/auth.go index 8a57fa52..cb083b83 100644 --- a/auth.go +++ b/auth.go @@ -659,7 +659,6 @@ func getJWT(r *http.Request) string { if iss != JWTIssuer { accepted := false for _, fiss := range AcceptedJWTIssuers { - Trail(DEBUG, "fiss:%s, iss:%s", fiss, iss) if fiss == iss { accepted = true break @@ -860,8 +859,6 @@ func getJWTRSAPublicKeySSO(jwtToken *jwt.Token) *rsa.PublicKey { E: int(E.Int64()), } - Trail(DEBUG, publicCert) - return &publicCert } diff --git a/d_api_openid_login.go b/d_api_openid_login.go index 50bbc067..fa2e5f1d 100644 --- a/d_api_openid_login.go +++ b/d_api_openid_login.go @@ -10,6 +10,8 @@ func dAPIOpenIDLoginHandler(w http.ResponseWriter, r *http.Request, s *Session) _ = s redirectURI := r.FormValue("redirect_uri") + Trail(DEBUG, "HERE") + if r.Method == "GET" { if session := IsAuthenticated(r); session != nil { Preload(session, "User") diff --git a/login_handler.go b/login_handler.go index 7173dcfc..f87dadfc 100644 --- a/login_handler.go +++ b/login_handler.go @@ -33,6 +33,9 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { if session := IsAuthenticated(r); session != nil { session = session.User.GetActiveSession() SetSessionCookie(w, r, session) + if r.URL.Query().Get("next") != "" { + http.Redirect(w, r, r.URL.Query().Get("next"), 303) + } } if r.Method == cPOST { diff --git a/main_handler.go b/main_handler.go index 3b83a637..e9c4095a 100644 --- a/main_handler.go +++ b/main_handler.go @@ -41,7 +41,6 @@ func mainHandler(w http.ResponseWriter, r *http.Request) { // This session is preloaded with a user session := IsAuthenticated(r) if session == nil { - Trail(DEBUG, "no auth, Login page") loginHandler(w, r) return } diff --git a/templates/uadmin/default/login.html b/templates/uadmin/default/login.html index 76ca0f0d..081a4d82 100644 --- a/templates/uadmin/default/login.html +++ b/templates/uadmin/default/login.html @@ -154,7 +154,7 @@