From c68c01af99504827f730f378816ec4114bb3d32a Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Wed, 16 Nov 2022 16:16:16 +0400
Subject: [PATCH 001/109] reformat m2m sql
---
check_csrf.go | 5 -----
d_api_add.go | 4 +---
d_api_edit.go | 15 ++++-----------
db.go | 35 ++++++++++++++---------------------
rate_limit.go | 3 +--
sql_injection.go | 6 ------
6 files changed, 20 insertions(+), 48 deletions(-)
diff --git a/check_csrf.go b/check_csrf.go
index 2cae65af..b24bf3e0 100644
--- a/check_csrf.go
+++ b/check_csrf.go
@@ -1,7 +1,6 @@
package uadmin
import (
- "net"
"net/http"
)
@@ -48,7 +47,6 @@ work, `x-csrf-token` paramtere should be added.
Where you replace `MY_SESSION_KEY` with the session key.
*/
func CheckCSRF(r *http.Request) bool {
- var err error
if r.FormValue("x-csrf-token") != "" && r.FormValue("x-csrf-token") == getSession(r) {
return false
}
@@ -57,9 +55,6 @@ func CheckCSRF(r *http.Request) bool {
user = &User{}
}
ip := GetRemoteIP(r)
- if ip, _, err = net.SplitHostPort(ip); err != nil {
- ip = GetRemoteIP(r)
- }
Trail(CRITICAL, "Request failed Anti-CSRF protection from user:%s IP:%s", user.Username, ip)
return true
diff --git a/d_api_add.go b/d_api_add.go
index d4865d43..41bf8792 100644
--- a/d_api_add.go
+++ b/d_api_add.go
@@ -137,9 +137,7 @@ func dAPIAddHandler(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)
- sql = strings.Replace(sql, "{TABLE1_ID}", fmt.Sprint(createdIDs[i]), -1)
- sql = strings.Replace(sql, "{TABLE2_ID}", id, -1)
- db = db.Exec(sql)
+ db = db.Exec(sql, createdIDs[i], id)
}
}
}
diff --git a/d_api_edit.go b/d_api_edit.go
index 734cbdac..7c1206d9 100644
--- a/d_api_edit.go
+++ b/d_api_edit.go
@@ -2,7 +2,6 @@ package uadmin
import (
"encoding/json"
- "fmt"
"net/http"
"reflect"
"strings"
@@ -128,8 +127,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)
- sql = strings.Replace(sql, "{TABLE1_ID}", fmt.Sprint(GetID(modelArray.Elem().Index(i))), -1)
- db = db.Exec(sql)
+ db = db.Exec(sql, GetID(modelArray.Elem().Index(i)))
if v == "" {
continue
@@ -140,9 +138,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)
- sql = strings.Replace(sql, "{TABLE1_ID}", fmt.Sprint(GetID(modelArray.Elem().Index(i))), -1)
- sql = strings.Replace(sql, "{TABLE2_ID}", id, -1)
- db = db.Exec(sql)
+ db = db.Exec(sql, GetID(modelArray.Elem().Index(i)), id)
}
}
}
@@ -182,8 +178,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)
- sql = strings.Replace(sql, "{TABLE1_ID}", urlParts[2], -1)
- db = db.Exec(sql)
+ db = db.Exec(sql, urlParts[2])
if v == "" {
continue
@@ -194,9 +189,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)
- sql = strings.Replace(sql, "{TABLE1_ID}", urlParts[2], -1)
- sql = strings.Replace(sql, "{TABLE2_ID}", id, -1)
- db = db.Exec(sql)
+ db = db.Exec(sql, urlParts[2], id)
}
}
db.Commit()
diff --git a/db.go b/db.go
index f9d6cf61..9dbbb254 100644
--- a/db.go
+++ b/db.go
@@ -36,22 +36,22 @@ var db *gorm.DB
var sqlDialect = map[string]map[string]string{
"mysql": {
"createM2MTable": "CREATE TABLE `{TABLE1}_{TABLE2}` (`table1_id` int(10) unsigned NOT NULL, `table2_id` int(10) unsigned NOT NULL, PRIMARY KEY (`table1_id`,`table2_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;",
- "selectM2M": "SELECT `table2_id` FROM `{TABLE1}_{TABLE2}` WHERE table1_id={TABLE1_ID};",
- "deleteM2M": "DELETE FROM `{TABLE1}_{TABLE2}` WHERE `table1_id`={TABLE1_ID};",
- "insertM2M": "INSERT INTO `{TABLE1}_{TABLE2}` VALUES ({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 (?, ?);",
},
"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={TABLE1_ID};`,
- "deleteM2M": `DELETE FROM "{TABLE1}_{TABLE2}" WHERE "table1_id"={TABLE1_ID};`,
- "insertM2M": `INSERT INTO "{TABLE1}_{TABLE2}" VALUES ({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 (?, ?);`,
},
"sqlite": {
//"createM2MTable": "CREATE TABLE `{TABLE1}_{TABLE2}` (`{TABLE1}_id` INTEGER NOT NULL,`{TABLE2}_id` INTEGER NOT NULL, PRIMARY KEY(`{TABLE1}_id`,`{TABLE2}_id`));",
"createM2MTable": "CREATE TABLE `{TABLE1}_{TABLE2}` (`table1_id` INTEGER NOT NULL,`table2_id` INTEGER NOT NULL, PRIMARY KEY(`table1_id`,`table2_id`));",
- "selectM2M": "SELECT `table2_id` FROM `{TABLE1}_{TABLE2}` WHERE table1_id={TABLE1_ID};",
- "deleteM2M": "DELETE FROM `{TABLE1}_{TABLE2}` WHERE `table1_id`={TABLE1_ID};",
- "insertM2M": "INSERT INTO `{TABLE1}_{TABLE2}` VALUES ({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 (?, ?);",
},
}
@@ -257,9 +257,6 @@ func GetDB() *gorm.DB {
os.Exit(2)
}
- // Set collate
- // db = db.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci")
-
// Temp solution for 0 foreign key
err = db.Exec("SET PERSIST FOREIGN_KEY_CHECKS=0;").Error
if err != nil {
@@ -477,13 +474,12 @@ func customSave(m interface{}) (err error) {
sql := sqlDialect[Database.Type]["deleteM2M"]
sql = strings.Replace(sql, "{TABLE1}", table1, -1)
sql = strings.Replace(sql, "{TABLE2}", table2, -1)
- sql = strings.Replace(sql, "{TABLE1_ID}", fmt.Sprint(GetID(value)), -1)
TimeMetric("uadmin/db/duration", 1000, func() {
- err = db.Exec(sql).Error
+ err = db.Exec(sql, GetID(value)).Error
for fmt.Sprint(err) == "database is locked" {
time.Sleep(time.Millisecond * 100)
- err = db.Exec(sql).Error
+ err = db.Exec(sql, GetID(value)).Error
}
})
if err != nil {
@@ -496,14 +492,12 @@ func customSave(m interface{}) (err error) {
sql := sqlDialect[Database.Type]["insertM2M"]
sql = strings.Replace(sql, "{TABLE1}", table1, -1)
sql = strings.Replace(sql, "{TABLE2}", table2, -1)
- sql = strings.Replace(sql, "{TABLE1_ID}", fmt.Sprint(GetID(value)), -1)
- sql = strings.Replace(sql, "{TABLE2_ID}", fmt.Sprint(GetID(value.Field(i).Index(index))), -1)
TimeMetric("uadmin/db/duration", 1000, func() {
- err = db.Exec(sql).Error
+ err = db.Exec(sql, GetID(value), GetID(value.Field(i).Index(index))).Error
for fmt.Sprint(err) == "database is locked" {
time.Sleep(time.Millisecond * 100)
- err = db.Exec(sql).Error
+ err = db.Exec(sql, GetID(value), GetID(value.Field(i).Index(index))).Error
}
})
if err != nil {
@@ -854,10 +848,9 @@ func customGet(m interface{}, m2m ...string) (err error) {
sqlSelect := sqlDialect[Database.Type]["selectM2M"]
sqlSelect = strings.Replace(sqlSelect, "{TABLE1}", table1, -1)
sqlSelect = strings.Replace(sqlSelect, "{TABLE2}", table2, -1)
- sqlSelect = strings.Replace(sqlSelect, "{TABLE1_ID}", fmt.Sprint(GetID(value)), -1)
var rows *sql.Rows
- rows, err = db.Raw(sqlSelect).Rows()
+ rows, err = db.Raw(sqlSelect, GetID(value)).Rows()
if err != nil {
Trail(ERROR, "Unable to get m2m records. %s", err)
Trail(ERROR, sqlSelect)
diff --git a/rate_limit.go b/rate_limit.go
index 7721fd32..265c3f9d 100644
--- a/rate_limit.go
+++ b/rate_limit.go
@@ -1,7 +1,6 @@
package uadmin
import (
- "net"
"net/http"
"sync"
"time"
@@ -14,7 +13,7 @@ var rateLimitLock = sync.Mutex{}
// the IP in the request has exceeded their quota
func CheckRateLimit(r *http.Request) bool {
rateLimitLock.Lock()
- ip, _, _ := net.SplitHostPort(GetRemoteIP(r))
+ ip := GetRemoteIP(r)
now := time.Now().Unix() * RateLimit
if val, ok := rateLimitMap[ip]; ok {
if (val + RateLimitBurst) < now {
diff --git a/sql_injection.go b/sql_injection.go
index 9bbef373..07f45f2d 100644
--- a/sql_injection.go
+++ b/sql_injection.go
@@ -1,7 +1,6 @@
package uadmin
import (
- "net"
"net/http"
"regexp"
"strings"
@@ -15,16 +14,11 @@ import (
//
// return true for sql injection attempt and false for safe requests
func SQLInjection(r *http.Request, key, value string) bool {
- var err error
-
user := GetUserFromRequest(r)
if user == nil {
user = &User{}
}
ip := GetRemoteIP(r)
- if ip, _, err = net.SplitHostPort(ip); err != nil {
- ip = GetRemoteIP(r)
- }
errMsg := "SQL Injection attempt (%s '%s'). User:" + user.Username + " IP:" + ip
if key != "" {
// Case 1 - Comment injection
From c8eb77db43ee07881a692ac84d810de1587f9c78 Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Wed, 16 Nov 2022 16:30:40 +0400
Subject: [PATCH 002/109] upgrade sockio for client
---
go.mod | 10 ++++-----
go.sum | 22 +++++++++----------
.../assets/socket.io-client/package.json | 2 +-
3 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/go.mod b/go.mod
index 5410c690..409f1962 100644
--- a/go.mod
+++ b/go.mod
@@ -9,10 +9,10 @@ require (
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.1.0
- golang.org/x/mod v0.6.0
- golang.org/x/net v0.1.0
- gorm.io/driver/mysql v1.4.3
+ 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
@@ -36,6 +36,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.1.0 // indirect
+ golang.org/x/sys v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
)
diff --git a/go.sum b/go.sum
index 662a4f72..93ba2e38 100644
--- a/go.sum
+++ b/go.sum
@@ -166,16 +166,16 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
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.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
-golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
+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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
-golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
+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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -184,8 +184,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
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.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
-golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
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=
@@ -205,12 +205,12 @@ golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBc
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.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
-golang.org/x/sys v0.1.0/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/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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
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=
@@ -243,8 +243,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
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.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
-gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
+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=
diff --git a/static/uadmin/assets/socket.io-client/package.json b/static/uadmin/assets/socket.io-client/package.json
index 8ec99e7b..e54be9a7 100644
--- a/static/uadmin/assets/socket.io-client/package.json
+++ b/static/uadmin/assets/socket.io-client/package.json
@@ -64,7 +64,7 @@
"indexof": "0.0.1",
"object-component": "0.0.3",
"parseuri": "0.0.2",
- "socket.io-parser": "3.4.1",
+ "socket.io-parser": "4.2.1",
"to-array": "0.1.3"
},
"description": "[](http://travis-ci.org/Automattic/socket.io-client)  , 5 deletions(-)
create mode 100644 .jwt
diff --git a/.jwt b/.jwt
new file mode 100644
index 00000000..209b5a28
--- /dev/null
+++ b/.jwt
@@ -0,0 +1 @@
+3=J 0\}C#A,k~D
\ No newline at end of file
diff --git a/auth.go b/auth.go
index b3654ea9..4a0d8b36 100644
--- a/auth.go
+++ b/auth.go
@@ -2,11 +2,15 @@ package uadmin
import (
"context"
+ "encoding/base64"
+ "encoding/json"
"math/big"
"net"
"path"
+ "crypto/hmac"
"crypto/rand"
+ "crypto/sha256"
"math"
"net/http"
"strconv"
@@ -21,9 +25,12 @@ import (
// an expiry date.
var CookieTimeout = -1
-// Salt is extra salt added to password hashing
+// Salt is added to password hashing
var Salt = ""
+// JWT secret for signing tokens
+var JWT = ""
+
// bcryptDiff
var bcryptDiff = 12
@@ -497,6 +504,60 @@ func getSession(r *http.Request) string {
return r.FormValue("session")
}
}
+ // 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.DecodeString(jwtParts[0])
+ if err != nil {
+ return ""
+ }
+ jPayload, err := base64.RawURLEncoding.DecodeString(jwtParts[1])
+ if err != nil {
+ return ""
+ }
+
+ header := map[string]interface{}{}
+ err = json.Unmarshal(jHeader, &header)
+ if err != nil {
+ return ""
+ }
+ payload := map[string]interface{}{}
+ err = json.Unmarshal(jPayload, &payload)
+ if err != nil {
+ return ""
+ }
+
+ // Verify the signature
+ if _, ok := payload["alg"]; !ok {
+ return ""
+ }
+ if _, ok := payload["typ"]; ok {
+ if v, ok := payload["typ"].(string); !ok || v != "JWT" {
+ return ""
+ }
+ }
+ switch payload["alg"].(string) {
+ case "none":
+ // Don't allow none type for auth JWT
+ return ""
+ case "HS256":
+ hash := hmac.New(sha256.New, []byte(Salt))
+ hash.Write([]byte(jwtParts[0] + "." + jwtParts[1]))
+ token := hash.Sum(nil)
+ b64Token := base64.RawURLEncoding.EncodeToString(token)
+ if b64Token != jwtParts[2] {
+ return ""
+ }
+ }
+ }
+ }
return ""
}
diff --git a/check_csrf.go b/check_csrf.go
index b24bf3e0..64f9c57c 100644
--- a/check_csrf.go
+++ b/check_csrf.go
@@ -47,7 +47,8 @@ work, `x-csrf-token` paramtere should be added.
Where you replace `MY_SESSION_KEY` with the session key.
*/
func CheckCSRF(r *http.Request) bool {
- if r.FormValue("x-csrf-token") != "" && r.FormValue("x-csrf-token") == getSession(r) {
+ token := getCSRFToken(r)
+ if token != "" && token == getSession(r) {
return false
}
user := GetUserFromRequest(r)
@@ -59,3 +60,13 @@ func CheckCSRF(r *http.Request) bool {
Trail(CRITICAL, "Request failed Anti-CSRF protection from user:%s IP:%s", user.Username, ip)
return true
}
+
+func getCSRFToken(r *http.Request) string {
+ if r.FormValue("x-csrf-token") != "" {
+ return r.FormValue("x-csrf-token")
+ }
+ if r.Header.Get("X-CSRF-TOKEN") != "" {
+ return r.Header.Get("X-CSRF-TOKEN")
+ }
+ return ""
+}
diff --git a/encrypt.go b/encrypt.go
index 5228e96e..ffd856d9 100644
--- a/encrypt.go
+++ b/encrypt.go
@@ -85,7 +85,6 @@ func decryptArray(a interface{}) {
if schema, ok := getSchema(getModelName(a)); ok {
for _, f := range schema.Fields {
if f.Encrypt {
- // TODO: Decrypt
allArray := reflect.ValueOf(a)
for i := 0; i < allArray.Elem().Len(); i++ {
encryptedValue := allArray.Elem().Index(i).FieldByName(f.Name).String()
diff --git a/grouppermission.go b/grouppermission.go
index d16d8fbe..08d3a4fa 100644
--- a/grouppermission.go
+++ b/grouppermission.go
@@ -24,7 +24,7 @@ func (g GroupPermission) String() string {
func (g *GroupPermission) Save() {
Save(g)
- loadSessions()
+ loadPermissions()
}
// HideInDashboard to return false and auto hide this from dashboard
diff --git a/register.go b/register.go
index ee20e4ec..2b5b6ed7 100644
--- a/register.go
+++ b/register.go
@@ -149,6 +149,18 @@ func Register(m ...interface{}) {
}
}
+ // Check if JWT key is there or generate it
+ if _, err := os.Stat(".jwt"); os.IsNotExist(err) && os.Getenv("UADMIN_JWT") == "" {
+ JWT = GenerateBase64(24)
+ ioutil.WriteFile(".jwt", EncryptKey, 0600)
+ } else {
+ JWT = os.Getenv("UADMIN_JWT")
+ if len(JWT) == 0 {
+ buf, _ := ioutil.ReadFile(".jwt")
+ JWT = string(buf)
+ }
+ }
+
// Check if salt is there or generate it
users := []User{}
if _, err := os.Stat(".salt"); os.IsNotExist(err) && os.Getenv("UADMIN_SALT") == "" {
diff --git a/userpermission.go b/userpermission.go
index 5d98f876..bd9e1802 100644
--- a/userpermission.go
+++ b/userpermission.go
@@ -28,7 +28,7 @@ func (u UserPermission) String() string {
func (u *UserPermission) Save() {
Save(u)
- loadSessions()
+ loadPermissions()
}
// HideInDashboard to return false and auto hide this from dashboard
@@ -37,6 +37,9 @@ func (UserPermission) HideInDashboard() bool {
}
func loadPermissions() {
+ if !CachePermissions {
+ return
+ }
cacheUserPerms = []UserPermission{}
cacheGroupPerms = []GroupPermission{}
cachedModels = []DashboardMenu{}
From 402feb9c80be1010ff0a8a1f05c008db9f177e0a Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Thu, 17 Nov 2022 20:46:53 +0400
Subject: [PATCH 004/109] add instructions to remove .jwt after testing
---
.jwt | 1 -
server_test.go | 1 +
2 files changed, 1 insertion(+), 1 deletion(-)
delete mode 100644 .jwt
diff --git a/.jwt b/.jwt
deleted file mode 100644
index 209b5a28..00000000
--- a/.jwt
+++ /dev/null
@@ -1 +0,0 @@
-3=J 0\}C#A,k~D
\ No newline at end of file
diff --git a/server_test.go b/server_test.go
index 198acb35..e1de492d 100644
--- a/server_test.go
+++ b/server_test.go
@@ -319,6 +319,7 @@ func teardownFunction() {
os.Remove(".salt")
os.Remove(".uproj")
os.Remove(".bindip")
+ os.Remove(".jwt")
// Delete temp media file
os.RemoveAll("./media")
From bcab92da407ce74ea4b76e1d978669916ded6c1d Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Thu, 17 Nov 2022 21:30:39 +0400
Subject: [PATCH 005/109] Add JWT access support
---
auth.go | 102 ++++++++++++++++++++++++++++++++++++-----------
login_handler.go | 14 ++-----
2 files changed, 83 insertions(+), 33 deletions(-)
diff --git a/auth.go b/auth.go
index 4a0d8b36..e098da83 100644
--- a/auth.go
+++ b/auth.go
@@ -37,7 +37,7 @@ var bcryptDiff = 12
// cachedSessions is variable for keeping active sessions
var cachedSessions map[string]Session
-// invalidAttemps keeps track of invalid password attempts
+// invalidAttempts keeps track of invalid password attempts
// per IP address
var invalidAttempts = map[string]int{}
@@ -101,7 +101,7 @@ func IsAuthenticated(r *http.Request) *Session {
}
// SetSessionCookie sets the session cookie value, The the value passed in
-// session is nil, then the session assiged will be a no user session
+// session is nil, then the session assigned will be a no user session
func SetSessionCookie(w http.ResponseWriter, r *http.Request, s *Session) {
if s == nil {
http.SetCookie(w, &http.Cookie{
@@ -112,18 +112,55 @@ func SetSessionCookie(w http.ResponseWriter, r *http.Request, s *Session) {
Expires: time.Now().AddDate(0, 0, 1),
})
} else {
- exDate := time.Time{}
- if s.ExpiresOn != nil {
- exDate = *s.ExpiresOn
- }
- http.SetCookie(w, &http.Cookie{
+ sessionCookie := &http.Cookie{
Name: "session",
Value: s.Key,
SameSite: http.SameSiteStrictMode,
Path: "/",
- Expires: exDate,
- })
+ }
+ if s.ExpiresOn != nil {
+ sessionCookie.Expires = *s.ExpiresOn
+ }
+ http.SetCookie(w, sessionCookie)
+
+ jwt := createJWT(r, s)
+ jwtCookie := &http.Cookie{
+ Name: "access-jwt",
+ Value: jwt,
+ SameSite: http.SameSiteStrictMode,
+ Path: "/",
+ }
+ if s.ExpiresOn != nil {
+ jwtCookie.Expires = *s.ExpiresOn
+ }
+ http.SetCookie(w, jwtCookie)
+ }
+}
+
+func createJWT(r *http.Request, s *Session) string {
+ if s == nil {
+ return ""
}
+ if !isValidSession(r, s) {
+ return ""
+ }
+ header := map[string]interface{}{
+ "alg": "HS256",
+ "typ": "JWT",
+ }
+ payload := map[string]interface{}{
+ "sub": s.User.Username,
+ }
+ 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))
+ 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 {
@@ -528,34 +565,53 @@ func getSession(r *http.Request) string {
if err != nil {
return ""
}
- payload := map[string]interface{}{}
- err = json.Unmarshal(jPayload, &payload)
- if err != nil {
- return ""
- }
// Verify the signature
- if _, ok := payload["alg"]; !ok {
- return ""
+ alg := "HS256"
+ if v, ok := header["alg"].(string); ok {
+ alg = v
}
- if _, ok := payload["typ"]; ok {
- if v, ok := payload["typ"].(string); !ok || v != "JWT" {
+ if _, ok := header["typ"]; ok {
+ if v, ok := header["typ"].(string); !ok || v != "JWT" {
return ""
}
}
- switch payload["alg"].(string) {
- case "none":
- // Don't allow none type for auth JWT
- return ""
+ switch alg {
case "HS256":
- hash := hmac.New(sha256.New, []byte(Salt))
+ hash := hmac.New(sha256.New, []byte(JWT))
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 ""
+ }
+
+ // 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 ""
}
+
+ sub := payload["sub"].(string)
+ user := User{}
+ Get(&user, "username = ?", sub)
+
+ if user.ID == 0 {
+ return ""
+ }
+
+ session := user.GetActiveSession()
+ return session.Key
}
}
return ""
diff --git a/login_handler.go b/login_handler.go
index 1d6012ca..11e389e7 100644
--- a/login_handler.go
+++ b/login_handler.go
@@ -61,18 +61,12 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
if session.PendingOTP {
Trail(INFO, "User: %s OTP: %s", session.User.Username, session.User.GetOTP())
}
- cookie, _ := r.Cookie("session")
- if cookie == nil {
- cookie = &http.Cookie{}
- }
- cookie.Name = "session"
- cookie.Value = session.Key
- cookie.Path = "/"
- cookie.SameSite = http.SameSiteStrictMode
- http.SetCookie(w, cookie)
+
+ // Set session cookie
+ SetSessionCookie(w, r, session)
// set language cookie
- cookie, _ = r.Cookie("language")
+ cookie, _ := r.Cookie("language")
if cookie == nil {
cookie = &http.Cookie{}
}
From bb87390d6c0430010984451e5abd67b137817450 Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Thu, 17 Nov 2022 22:59:06 +0400
Subject: [PATCH 006/109] Add extra data to JWT
---
auth.go | 3 +++
register.go | 4 ++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/auth.go b/auth.go
index e098da83..76fef3e8 100644
--- a/auth.go
+++ b/auth.go
@@ -151,6 +151,9 @@ func createJWT(r *http.Request, s *Session) string {
payload := map[string]interface{}{
"sub": s.User.Username,
}
+ if s.ExpiresOn != nil {
+ payload["exp"] = s.ExpiresOn.Unix()
+ }
jHeader, _ := json.Marshal(header)
jPayload, _ := json.Marshal(payload)
b64Header := base64.RawURLEncoding.EncodeToString(jHeader)
diff --git a/register.go b/register.go
index 2b5b6ed7..f92a10b1 100644
--- a/register.go
+++ b/register.go
@@ -151,8 +151,8 @@ func Register(m ...interface{}) {
// Check if JWT key is there or generate it
if _, err := os.Stat(".jwt"); os.IsNotExist(err) && os.Getenv("UADMIN_JWT") == "" {
- JWT = GenerateBase64(24)
- ioutil.WriteFile(".jwt", EncryptKey, 0600)
+ JWT = GenerateBase64(64)
+ ioutil.WriteFile(".jwt", []byte(JWT), 0600)
} else {
JWT = os.Getenv("UADMIN_JWT")
if len(JWT) == 0 {
From 5c515052fb3b6ef2efe56467fbe8e626b2b8736d Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Fri, 18 Nov 2022 10:57:14 +0400
Subject: [PATCH 007/109] BUG FIX: dAPI __is works now with the new version of
gorm
---
d_api_helper.go | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/d_api_helper.go b/d_api_helper.go
index d7881181..1911d070 100644
--- a/d_api_helper.go
+++ b/d_api_helper.go
@@ -258,7 +258,7 @@ func getQueryOperator(r *http.Request, v string, tableName string) string {
return strings.TrimSuffix(v, "__in") + nTerm + columnEnclosure() + " IN (?)"
}
if strings.HasSuffix(v, "__is") {
- return strings.TrimSuffix(v, "__is") + columnEnclosure() + " IS" + nTerm + " ?"
+ return strings.TrimSuffix(v, "__is") + columnEnclosure() + " IS" + nTerm + " NULL"
}
if strings.HasSuffix(v, "__contains") {
return strings.TrimSuffix(v, "__contains") + columnEnclosure() + nTerm + " " + getLike(true) + " ?"
@@ -309,10 +309,10 @@ func getQueryArg(k, v string) []interface{} {
return []interface{}{strings.Split(v, ",")}
}
if strings.HasSuffix(k, "__is") {
- if strings.ToUpper(v) == "NULL" {
- return []interface{}{interface{}(nil)}
- }
- return []interface{}{v}
+ // if strings.ToUpper(v) == "NULL" {
+ // return []interface{}{}
+ // }
+ return []interface{}{}
}
if strings.HasSuffix(k, "__contains") {
return []interface{}{"%" + v + "%"}
From d3d63a1ec0e9ec25f7fce018420a0dc15267814c Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Sat, 19 Nov 2022 20:25:09 +0400
Subject: [PATCH 008/109] BUG FIX: dAPI compatibility with the new version of
gorm
---
d_api_helper.go | 12 ++++++------
d_api_read.go | 1 +
2 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/d_api_helper.go b/d_api_helper.go
index 1911d070..b84afca8 100644
--- a/d_api_helper.go
+++ b/d_api_helper.go
@@ -175,7 +175,7 @@ func getFilters(r *http.Request, params map[string]string, tableName string, sch
orArgs := []interface{}{}
for _, field := range schema.Fields {
if field.Searchable {
- // TODO: Supprt non-string types
+ // TODO: Support non-string types
if field.TypeName == "string" {
orQParts = append(orQParts, getQueryOperator(r, field.ColumnName+"__icontains", tableName))
orArgs = append(orArgs, getQueryArg(field.ColumnName+"__icontains", v)...)
@@ -268,14 +268,14 @@ func getQueryOperator(r *http.Request, v string, tableName string) string {
}
if strings.HasSuffix(v, "__startswith") {
if Database.Type == "mysql" {
- return strings.TrimSuffix(v, "__startswith") + columnEnclosure() + nTerm + " " + getLike(true) + " BINARY ?"
+ return strings.TrimSuffix(v, "__startswith") + columnEnclosure() + nTerm + " " + getLike(true) + " ?"
} else if Database.Type == "sqlite" {
return strings.TrimSuffix(v, "__startswith") + columnEnclosure() + nTerm + " " + getLike(true) + " ?"
}
}
if strings.HasSuffix(v, "__endswith") {
if Database.Type == "mysql" {
- return strings.TrimSuffix(v, "__endswith") + columnEnclosure() + nTerm + " " + getLike(true) + " BINARY ?"
+ return strings.TrimSuffix(v, "__endswith") + columnEnclosure() + nTerm + " " + getLike(true) + " ?"
} else if Database.Type == "sqlite" {
return strings.TrimSuffix(v, "__endswith") + columnEnclosure() + nTerm + " " + getLike(true) + " ?"
}
@@ -284,13 +284,13 @@ func getQueryOperator(r *http.Request, v string, tableName string) string {
return strings.TrimSuffix(v, "__re") + nTerm + " REGEXP ?"
}
if strings.HasSuffix(v, "__icontains") {
- return "UPPER(" + strings.TrimSuffix(v, "__icontains") + columnEnclosure() + ")" + nTerm + " " + getLike(false) + " ?"
+ return strings.TrimSuffix(v, "__icontains") + columnEnclosure() + nTerm + " " + getLike(false) + " ?"
}
if strings.HasSuffix(v, "__istartswith") {
- return "UPPER(" + strings.TrimSuffix(v, "__istartswith") + columnEnclosure() + ")" + nTerm + " " + getLike(false) + " ?"
+ return strings.TrimSuffix(v, "__istartswith") + columnEnclosure() + nTerm + " " + getLike(false) + " ?"
}
if strings.HasSuffix(v, "__iendswith") {
- return "UPPER(" + strings.TrimSuffix(v, "__iendswith") + columnEnclosure() + ")" + nTerm + " " + getLike(false) + " ?"
+ return strings.TrimSuffix(v, "__iendswith") + columnEnclosure() + nTerm + " " + getLike(false) + " ?"
}
if n {
return v + columnEnclosure() + " <> ?"
diff --git a/d_api_read.go b/d_api_read.go
index 77faab5f..8e99c54f 100644
--- a/d_api_read.go
+++ b/d_api_read.go
@@ -80,6 +80,7 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) {
// Get filters from request
q, args := getFilters(r, params, tableName, &schema)
+ Trail(DEBUG, "q:%s, args:%#v", q, args)
// Apply List Modifier from Schema
if schema.ListModifier != nil {
From 3265d2fb84b7764d9b8472778138a78624bda7fe Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Sun, 20 Nov 2022 00:23:30 +0400
Subject: [PATCH 009/109] BUG FIX: dAPI read with custom schema now returns
correct data types
---
d_api_helper.go | 91 ++++++++++++-------------------------------------
d_api_read.go | 53 +++++++---------------------
2 files changed, 33 insertions(+), 111 deletions(-)
diff --git a/d_api_helper.go b/d_api_helper.go
index b84afca8..2b4c226e 100644
--- a/d_api_helper.go
+++ b/d_api_helper.go
@@ -1,8 +1,6 @@
package uadmin
import (
- "database/sql"
- "fmt"
"net/http"
"net/url"
"reflect"
@@ -10,53 +8,6 @@ import (
"time"
)
-func parseCustomDBSchema(rows *sql.Rows) interface{} {
- m := []map[string]interface{}{}
-
- // Parse the data into an array
- columns, _ := rows.Columns()
-
- //var current interface{}
- for rows.Next() {
- var vals = makeResultReceiver(len(columns))
- rows.Scan(vals...)
- row := map[string]interface{}{}
- for i := range columns {
- row[columns[i]] = getDBValue(vals[i])
- }
- m = append(m, row)
- }
- return m
-}
-
-func getDBValue(p interface{}) interface{} {
- i := p.(*dbScanner)
- switch v := (i.Value).(type) {
- case int:
- return v
- case int64:
- return v
- case int32:
- return v
- case int8:
- return v
- case uint64:
- return v
- case uint32:
- return v
- case uint8:
- return v
- case string:
- return v
- case uint:
- return v
- case []uint8:
- return string(v)
- default:
- return fmt.Sprint(v)
- }
-}
-
func getURLArgs(r *http.Request) map[string]string {
params := map[string]string{}
@@ -111,23 +62,23 @@ func getURLArgs(r *http.Request) map[string]string {
return params
}
-type dbScanner struct {
- Value interface{}
-}
+// type dbScanner struct {
+// Value interface{}
+// }
-func (d *dbScanner) Scan(src interface{}) error {
- d.Value = src
- return nil
-}
+// 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)
- }
- return result
-}
+// 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{}
@@ -198,7 +149,7 @@ func getFilters(r *http.Request, params map[string]string, tableName string, sch
if !strings.Contains(query, "deleted_at") {
if val, ok := params["$deleted"]; ok {
- if val == "0" {
+ if val == "0" || val == "false" {
qParts = append(qParts, tableName+".deleted_at IS NULL")
}
} else {
@@ -716,27 +667,27 @@ func returnDAPIJSON(w http.ResponseWriter, r *http.Request, a map[string]interfa
return nil
}
-// APILogReader is an interface for models to control loggin their read function in dAPI
+// APILogReader is an interface for models to control logging their read function in dAPI
type APILogReader interface {
APILogRead(*http.Request) bool
}
-// APILogEditor is an interface for models to control loggin their edit function in dAPI
+// APILogEditor is an interface for models to control logging their edit function in dAPI
type APILogEditor interface {
APILogEdit(*http.Request) bool
}
-// APILogAdder is an interface for models to control loggin their add function in dAPI
+// APILogAdder is an interface for models to control logging their add function in dAPI
type APILogAdder interface {
APILogAdd(*http.Request) bool
}
-// APILogDeleter is an interface for models to control loggin their delete function in dAPI
+// APILogDeleter is an interface for models to control logging their delete function in dAPI
type APILogDeleter interface {
APILogDelete(*http.Request) bool
}
-// APILogSchemer is an interface for models to control loggin their schema function in dAPI
+// APILogSchemer is an interface for models to control logging their schema function in dAPI
type APILogSchemer interface {
APILogSchema(*http.Request) bool
}
diff --git a/d_api_read.go b/d_api_read.go
index 8e99c54f..e7c6386a 100644
--- a/d_api_read.go
+++ b/d_api_read.go
@@ -1,7 +1,6 @@
package uadmin
import (
- "database/sql"
"encoding/json"
"net/http"
"reflect"
@@ -9,7 +8,6 @@ import (
)
func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) {
- var err error
var rowsCount int64
urlParts := strings.Split(r.URL.Path, "/")
@@ -80,7 +78,6 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) {
// Get filters from request
q, args := getFilters(r, params, tableName, &schema)
- Trail(DEBUG, "q:%s, args:%#v", q, args)
// Apply List Modifier from Schema
if schema.ListModifier != nil {
@@ -122,31 +119,21 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) {
Trail(DEBUG, "%#v", args)
}
- var rows *sql.Rows
-
if !customSchema {
mArray, _ := NewModelArray(modelName, true)
m = mArray.Interface()
- } else {
- m = []map[string]interface{}{}
- }
+ } // else {
+ // m = []map[string]interface{}{}
+ // }
if Database.Type == "mysql" {
db := GetDB()
if !customSchema {
db.Raw(SQL, args...).Scan(m)
} else {
- rows, err = db.Raw(SQL, args...).Rows()
- if err != nil {
- w.WriteHeader(500)
- ReturnJSON(w, r, map[string]interface{}{
- "status": "error",
- "err_msg": "Unable to execute SQL. " + err.Error(),
- })
- Trail(ERROR, "SQL: %v\nARGS: %v", SQL, args)
- return
- }
- m = parseCustomDBSchema(rows)
+ var rec []map[string]interface{}
+ db.Raw(SQL, args...).Scan(&rec)
+ m = rec
}
if a, ok := m.([]map[string]interface{}); ok {
rowsCount = int64(len(a))
@@ -159,17 +146,9 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) {
if !customSchema {
db.Raw(SQL, args...).Scan(m)
} else {
- rows, err = db.Raw(SQL, args...).Rows()
- if err != nil {
- w.WriteHeader(500)
- ReturnJSON(w, r, map[string]interface{}{
- "status": "error",
- "err_msg": "Unable to execute SQL. " + err.Error(),
- })
- Trail(ERROR, "SQL: %v\nARGS: %v", SQL, args)
- return
- }
- m = parseCustomDBSchema(rows)
+ var rec []map[string]interface{}
+ db.Raw(SQL, args...).Scan(&rec)
+ m = rec
}
db.Exec("PRAGMA case_sensitive_like=OFF;")
db.Commit()
@@ -183,17 +162,9 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) {
if !customSchema {
db.Raw(SQL, args...).Scan(m)
} else {
- rows, err = db.Raw(SQL, args...).Rows()
- if err != nil {
- w.WriteHeader(500)
- ReturnJSON(w, r, map[string]interface{}{
- "status": "error",
- "err_msg": "Unable to execute SQL. " + err.Error(),
- })
- Trail(ERROR, "SQL: %v\nARGS: %v", SQL, args)
- return
- }
- m = parseCustomDBSchema(rows)
+ var rec []map[string]interface{}
+ db.Raw(SQL, args...).Scan(&rec)
+ m = rec
}
if a, ok := m.([]map[string]interface{}); ok {
rowsCount = int64(len(a))
From c02cc09a175b28d479d84c8279007bb7b02f4931 Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Mon, 21 Nov 2022 08:36:32 +0400
Subject: [PATCH 010/109] Add Custom JWT handler
---
auth.go | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/auth.go b/auth.go
index 76fef3e8..8156b2f1 100644
--- a/auth.go
+++ b/auth.go
@@ -41,6 +41,8 @@ var cachedSessions map[string]Session
// per IP address
var invalidAttempts = map[string]int{}
+var CustomJWT func(r *http.Request, s *Session, payload map[string]interface{}) map[string]interface{}
+
// GenerateBase64 generates a base64 string of length length
func GenerateBase64(length int) string {
base := new(big.Int)
@@ -154,6 +156,12 @@ func createJWT(r *http.Request, s *Session) string {
if s.ExpiresOn != nil {
payload["exp"] = s.ExpiresOn.Unix()
}
+
+ // Check for custom JWT handler
+ if CustomJWT != nil {
+ payload = CustomJWT(r, s, payload)
+ }
+
jHeader, _ := json.Marshal(header)
jPayload, _ := json.Marshal(payload)
b64Header := base64.RawURLEncoding.EncodeToString(jHeader)
From 12f637facd1b603e22d9a1f070e4690383b7b496 Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Mon, 21 Nov 2022 08:38:34 +0400
Subject: [PATCH 011/109] BUG FIX: dAPI bug in __in and __re modifiers
---
d_api_helper.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/d_api_helper.go b/d_api_helper.go
index 2b4c226e..eeac16d3 100644
--- a/d_api_helper.go
+++ b/d_api_helper.go
@@ -206,7 +206,7 @@ func getQueryOperator(r *http.Request, v string, tableName string) string {
return strings.TrimSuffix(v, "__lte") + columnEnclosure() + " <= ?"
}
if strings.HasSuffix(v, "__in") {
- return strings.TrimSuffix(v, "__in") + nTerm + columnEnclosure() + " IN (?)"
+ return strings.TrimSuffix(v, "__in") + columnEnclosure() + nTerm + " IN (?)"
}
if strings.HasSuffix(v, "__is") {
return strings.TrimSuffix(v, "__is") + columnEnclosure() + " IS" + nTerm + " NULL"
@@ -232,7 +232,7 @@ func getQueryOperator(r *http.Request, v string, tableName string) string {
}
}
if strings.HasSuffix(v, "__re") {
- return strings.TrimSuffix(v, "__re") + nTerm + " REGEXP ?"
+ return strings.TrimSuffix(v, "__re") + columnEnclosure() + nTerm + " REGEXP ?"
}
if strings.HasSuffix(v, "__icontains") {
return strings.TrimSuffix(v, "__icontains") + columnEnclosure() + nTerm + " " + getLike(false) + " ?"
From 35572f59439eb76d73c4ad7b61c99411bb6f0777 Mon Sep 17 00:00:00 2001
From: snyk-bot
Date: Tue, 22 Nov 2022 03:01:59 +0000
Subject: [PATCH 012/109] fix: static/uadmin/assets/socket.io/package.json to
reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-ENGINEIO-3136336
---
static/uadmin/assets/socket.io/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/static/uadmin/assets/socket.io/package.json b/static/uadmin/assets/socket.io/package.json
index d67bfc26..2abfa321 100644
--- a/static/uadmin/assets/socket.io/package.json
+++ b/static/uadmin/assets/socket.io/package.json
@@ -56,7 +56,7 @@
],
"dependencies": {
"debug": "2.1.0",
- "engine.io": "3.6.0",
+ "engine.io": "3.6.1",
"has-binary-data": "0.1.3",
"socket.io-adapter": "1.1.2",
"socket.io-client": "3.1.3",
From e64f853e1a340b2e6d836f3639c090b699b7fb5d Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Tue, 22 Nov 2022 22:55:41 +0400
Subject: [PATCH 013/109] dAPI auth and open API
---
approval.go | 13 +
auth.go | 187 ++++++---
d_api.go | 15 +
d_api_auth.go | 32 ++
d_api_change_password.go | 7 +
d_api_helper.go | 2 +-
d_api_login.go | 61 +++
d_api_logout.go | 7 +
d_api_read.go | 4 +-
d_api_reset_password.go | 7 +
d_api_signup.go | 7 +
get_schema.go | 7 +-
global.go | 8 +-
login_handler.go | 4 -
main_handler.go | 1 +
openapi.go | 560 ++++++++++++++++++++++++++
openapi.json | 65 ---
openapi/callback.go | 3 +
openapi/components.go | 14 +
openapi/contact.go | 7 +
openapi/discriminator.go | 6 +
openapi/encoding.go | 9 +
openapi/example.go | 9 +
openapi/external_docs.go | 6 +
openapi/generate_schema.go | 406 +++++++++++++++++++
openapi/header.go | 5 +
openapi/license.go | 7 +
openapi/link.go | 11 +
openapi/media_type.go | 8 +
openapi/oauth_flow.go | 8 +
openapi/oauth_flows.go | 8 +
openapi/operation.go | 16 +
openapi/parameter.go | 18 +
openapi/path.go | 17 +
openapi/request_body.go | 8 +
openapi/response.go | 9 +
openapi/schema.go | 14 +
openapi/schema_info.go | 11 +
openapi/schema_object.go | 24 ++
openapi/security_requirment.go | 3 +
openapi/security_scheme.go | 13 +
openapi/server.go | 7 +
openapi/server_variable.go | 7 +
openapi/tag.go | 7 +
openapi/x_modifier.go | 7 +
openapi/xml.go | 9 +
openapi_template.json | 711 +++++++++++++++++++++++++++++++++
register.go | 42 +-
schema.go | 16 +-
user.go | 1 +
50 files changed, 2280 insertions(+), 154 deletions(-)
create mode 100644 d_api_auth.go
create mode 100644 d_api_change_password.go
create mode 100644 d_api_login.go
create mode 100644 d_api_logout.go
create mode 100644 d_api_reset_password.go
create mode 100644 d_api_signup.go
create mode 100644 openapi.go
delete mode 100644 openapi.json
create mode 100644 openapi/callback.go
create mode 100644 openapi/components.go
create mode 100644 openapi/contact.go
create mode 100644 openapi/discriminator.go
create mode 100644 openapi/encoding.go
create mode 100644 openapi/example.go
create mode 100644 openapi/external_docs.go
create mode 100644 openapi/generate_schema.go
create mode 100644 openapi/header.go
create mode 100644 openapi/license.go
create mode 100644 openapi/link.go
create mode 100644 openapi/media_type.go
create mode 100644 openapi/oauth_flow.go
create mode 100644 openapi/oauth_flows.go
create mode 100644 openapi/operation.go
create mode 100644 openapi/parameter.go
create mode 100644 openapi/path.go
create mode 100644 openapi/request_body.go
create mode 100644 openapi/response.go
create mode 100644 openapi/schema.go
create mode 100644 openapi/schema_info.go
create mode 100644 openapi/schema_object.go
create mode 100644 openapi/security_requirment.go
create mode 100644 openapi/security_scheme.go
create mode 100644 openapi/server.go
create mode 100644 openapi/server_variable.go
create mode 100644 openapi/tag.go
create mode 100644 openapi/x_modifier.go
create mode 100644 openapi/xml.go
create mode 100644 openapi_template.json
diff --git a/approval.go b/approval.go
index 78157ff3..5ae3002c 100644
--- a/approval.go
+++ b/approval.go
@@ -7,6 +7,19 @@ import (
"time"
)
+// ApprovalAction is a selection of approval actions
+type ApprovalAction int
+
+// Approved is an accepted change
+func (ApprovalAction) Approved() ApprovalAction {
+ return 1
+}
+
+// Rejected is a rejected change
+func (ApprovalAction) Rejected() ApprovalAction {
+ return 2
+}
+
// Approval is a model that stores approval data
type Approval struct {
Model
diff --git a/auth.go b/auth.go
index 8156b2f1..8388c199 100644
--- a/auth.go
+++ b/auth.go
@@ -31,6 +31,16 @@ var Salt = ""
// JWT secret for signing tokens
var JWT = ""
+// jwtIssuer is a URL to identify the application issuing JWT tokens.
+// If left empty, a partial hash of JWT will be assigned. This is also
+// used to identify the as JWT audience.
+var JWTIssuer = ""
+
+// AcceptedJWTIssuers is a list of accepted JWT issuers. By default the
+// local JWTIssuer is accepted. To accept other issuers, add them to
+// this list
+var AcceptedJWTIssuers = []string{}
+
// bcryptDiff
var bcryptDiff = 12
@@ -57,7 +67,7 @@ func GenerateBase64(length int) string {
return tempKey
}
-// GenerateBase32 generates a base64 string of length length
+// GenerateBase32 generates a base32 string of length length
func GenerateBase32(length int) string {
base := new(big.Int)
base.SetString("32", 10)
@@ -90,21 +100,16 @@ func IsAuthenticated(r *http.Request) *Session {
return nil
}
- s := Session{}
- if CacheSessions {
- s = cachedSessions[key]
- } else {
- Get(&s, "`key` = ?", key)
- }
- if isValidSession(r, &s) {
- return &s
+ s := getSessionByKey(key)
+ if isValidSession(r, s) {
+ return s
}
return nil
}
// SetSessionCookie sets the session cookie value, The the value passed in
// session is nil, then the session assigned will be a no user session
-func SetSessionCookie(w http.ResponseWriter, r *http.Request, s *Session) {
+func SetSessionCookie(w http.ResponseWriter, r *http.Request, s *Session) string {
if s == nil {
http.SetCookie(w, &http.Cookie{
Name: "session",
@@ -136,7 +141,10 @@ func SetSessionCookie(w http.ResponseWriter, r *http.Request, s *Session) {
jwtCookie.Expires = *s.ExpiresOn
}
http.SetCookie(w, jwtCookie)
+
+ return jwt
}
+ return ""
}
func createJWT(r *http.Request, s *Session) string {
@@ -152,6 +160,9 @@ func createJWT(r *http.Request, s *Session) string {
}
payload := map[string]interface{}{
"sub": s.User.Username,
+ "iat": s.LastLogin.Unix(),
+ "iss": JWTIssuer,
+ "aud": JWTIssuer,
}
if s.ExpiresOn != nil {
payload["exp"] = s.ExpiresOn.Unix()
@@ -167,7 +178,7 @@ func createJWT(r *http.Request, s *Session) string {
b64Header := base64.RawURLEncoding.EncodeToString(jHeader)
b64Payload := base64.RawURLEncoding.EncodeToString(jPayload)
- hash := hmac.New(sha256.New, []byte(JWT))
+ hash := hmac.New(sha256.New, []byte(JWT+s.Key))
hash.Write([]byte(b64Header + "." + b64Payload))
signature := hash.Sum(nil)
b64Signature := base64.RawURLEncoding.EncodeToString(signature)
@@ -175,8 +186,13 @@ func createJWT(r *http.Request, s *Session) string {
}
func isValidSession(r *http.Request, s *Session) bool {
+ valid, otpPending := isValidSessionOTP(r, s)
+ return valid && !otpPending
+}
+
+func isValidSessionOTP(r *http.Request, s *Session) (bool, bool) {
if s != nil && s.ID != 0 {
- if s.Active && !s.PendingOTP && (s.ExpiresOn == nil || s.ExpiresOn.After(time.Now())) {
+ if s.Active && (s.ExpiresOn == nil || s.ExpiresOn.After(time.Now())) {
if s.User.ID != s.UserID {
Get(&s.User, "id = ?", s.UserID)
}
@@ -184,13 +200,13 @@ func isValidSession(r *http.Request, s *Session) bool {
// Check for IP restricted session
if RestrictSessionIP {
ip := GetRemoteIP(r)
- return ip == s.IP
+ return ip == s.IP, s.PendingOTP
}
- return true
+ return true, s.PendingOTP
}
}
}
- return false
+ return false, false
}
// GetUserFromRequest returns a user from a request
@@ -210,16 +226,10 @@ func GetUserFromRequest(r *http.Request) *User {
// getSessionFromRequest returns a session from a request
func getSessionFromRequest(r *http.Request) *Session {
key := getSession(r)
- s := Session{}
-
- if CacheSessions {
- s = cachedSessions[key]
- } else {
- Get(&s, "`key` = ?", key)
- }
+ s := getSessionByKey(key)
if s.ID != 0 {
- return &s
+ return s
}
return nil
}
@@ -241,6 +251,7 @@ func Login(r *http.Request, username string, password string) (*Session, bool) {
log.SignIn(username, log.Action.LoginDenied(), r)
log.Save()
}()
+ incrementInvalidLogins(r)
return nil, false
}
s := user.Login(password, "")
@@ -276,6 +287,29 @@ func Login(r *http.Request, username string, password string) (*Session, bool) {
}()
}
+ incrementInvalidLogins(r)
+
+ // Record metrics
+ IncrementMetric("uadmin/security/invalidlogin")
+ return nil, false
+}
+
+// Login2FA login using username, password and otp for users with OTPRequired = true
+func Login2FA(r *http.Request, username string, password string, otpPass string) *Session {
+ s, otpRequired := Login(r, username, password)
+ if s != nil {
+ if otpRequired && s.User.VerifyOTP(otpPass) {
+ s.PendingOTP = false
+ s.Save()
+ } else if otpRequired && !s.User.VerifyOTP(otpPass) && otpPass != "" {
+ incrementInvalidLogins(r)
+ }
+ return s
+ }
+ return nil
+}
+
+func incrementInvalidLogins(r *http.Request) {
// Increment password attempts and check if it reached
// the maximum invalid password attempts
ip := GetRemoteIP(r)
@@ -286,17 +320,14 @@ func Login(r *http.Request, username string, password string) (*Session, bool) {
rateLimitMap[ip] = time.Now().Add(time.Duration(PasswordTimeout)*time.Minute).Unix() * RateLimit
rateLimitLock.Unlock()
}
-
- // Record metrics
- IncrementMetric("uadmin/security/invalidlogin")
- return nil, false
}
// Login2FA login using username, password and otp for users with OTPRequired = true
-func Login2FA(r *http.Request, username string, password string, otpPass string) *Session {
- s, otpRequired := Login(r, username, password)
- if s != nil {
- if otpRequired && s.User.VerifyOTP(otpPass) {
+func Login2FAKey(r *http.Request, key string, otpPass string) *Session {
+ s := getSessionByKey(key)
+ valid, otpPending := isValidSessionOTP(r, s)
+ if valid {
+ if otpPending && s.User.VerifyOTP(otpPass) {
s.PendingOTP = false
s.Save()
}
@@ -562,11 +593,11 @@ func getSession(r *http.Request) string {
return ""
}
- jHeader, err := base64.RawURLEncoding.DecodeString(jwtParts[0])
+ jHeader, err := base64.RawURLEncoding.WithPadding(base64.NoPadding).DecodeString(jwtParts[0])
if err != nil {
return ""
}
- jPayload, err := base64.RawURLEncoding.DecodeString(jwtParts[1])
+ jPayload, err := base64.RawURLEncoding.WithPadding(base64.NoPadding).DecodeString(jwtParts[1])
if err != nil {
return ""
}
@@ -577,34 +608,48 @@ func getSession(r *http.Request) string {
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 ""
- }
+ // Get data from payload
+ payload := map[string]interface{}{}
+ err = json.Unmarshal(jPayload, &payload)
+ if err != nil {
+ return ""
}
- switch alg {
- case "HS256":
- hash := hmac.New(sha256.New, []byte(JWT))
- hash.Write([]byte(jwtParts[0] + "." + jwtParts[1]))
- token := hash.Sum(nil)
- b64Token := base64.RawURLEncoding.EncodeToString(token)
- if b64Token != jwtParts[2] {
- 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 ""
+ }
}
- default:
- // For now, only support HMAC-SHA256
+ } else {
return ""
}
- // Get data from payload
- payload := map[string]interface{}{}
- err = json.Unmarshal(jPayload, &payload)
- if err != nil {
+ // 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 ""
}
@@ -622,6 +667,36 @@ func getSession(r *http.Request) string {
}
session := user.GetActiveSession()
+ if session == nil {
+ return ""
+ }
+
+ // TODO: verify exp
+
+ // 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
}
}
diff --git a/d_api.go b/d_api.go
index 7193391a..d7b9883d 100644
--- a/d_api.go
+++ b/d_api.go
@@ -117,12 +117,21 @@ 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)
// Check if there is no command and show help
if r.URL.Path == "" || r.URL.Path == "/" || len(urlParts) < 2 {
+ if s == nil {
+ w.WriteHeader(http.StatusForbidden)
+ ReturnJSON(w, r, map[string]interface{}{
+ "status": "error",
+ "err_msg": "access denied",
+ })
+ return
+ }
if urlParts[0] == "$allmodels" {
dAPIAllModelsHandler(w, r, s)
return
@@ -131,6 +140,12 @@ func dAPIHandler(w http.ResponseWriter, r *http.Request, s *Session) {
return
}
+ // auth dAPI
+ if urlParts[0] == "auth" {
+ dAPIAuthHandler(w, r, s)
+ return
+ }
+
// sanity check
// check model name
modelExists := false
diff --git a/d_api_auth.go b/d_api_auth.go
new file mode 100644
index 00000000..cb79ded9
--- /dev/null
+++ b/d_api_auth.go
@@ -0,0 +1,32 @@
+package uadmin
+
+import (
+ "net/http"
+ "strings"
+)
+
+func dAPIAuthHandler(w http.ResponseWriter, r *http.Request, s *Session) {
+ // Trim leading path
+ r.URL.Path = strings.TrimPrefix(r.URL.Path, "auth")
+ r.URL.Path = strings.TrimPrefix(r.URL.Path, "/")
+ r.URL.Path = strings.TrimSuffix(r.URL.Path, "/")
+
+ switch r.URL.Path {
+ case "login":
+ dAPILoginHandler(w, r, s)
+ case "logout":
+ dAPILogoutHandler(w, r, s)
+ case "signup":
+ dAPISignupHandler(w, r, s)
+ case "resetpassword":
+ dAPIResetPasswordHandler(w, r, s)
+ case "changepassword":
+ dAPIChangePasswordHandler(w, r, s)
+ default:
+ w.WriteHeader(http.StatusNotFound)
+ ReturnJSON(w, r, map[string]interface{}{
+ "status": "error",
+ "err_msg": "Unknown auth command: (" + r.URL.Path + ")",
+ })
+ }
+}
diff --git a/d_api_change_password.go b/d_api_change_password.go
new file mode 100644
index 00000000..c4d00b20
--- /dev/null
+++ b/d_api_change_password.go
@@ -0,0 +1,7 @@
+package uadmin
+
+import "net/http"
+
+func dAPIChangePasswordHandler(w http.ResponseWriter, r *http.Request, s *Session) {
+
+}
diff --git a/d_api_helper.go b/d_api_helper.go
index eeac16d3..c54ad972 100644
--- a/d_api_helper.go
+++ b/d_api_helper.go
@@ -604,7 +604,7 @@ func getQueryM2M(params map[string]string, m interface{}, customSchema bool, mod
}
func returnDAPIJSON(w http.ResponseWriter, r *http.Request, a map[string]interface{}, params map[string]string, command string, model interface{}) error {
- if params["$stat"] == "1" {
+ if params["$stat"] == "1" || params["$stat"] == "true" {
start := r.Context().Value(CKey("start"))
if start != nil {
diff --git a/d_api_login.go b/d_api_login.go
new file mode 100644
index 00000000..19e5704d
--- /dev/null
+++ b/d_api_login.go
@@ -0,0 +1,61 @@
+package uadmin
+
+import "net/http"
+
+func dAPILoginHandler(w http.ResponseWriter, r *http.Request, s *Session) {
+ if s != nil {
+ Logout(r)
+ }
+
+ // Get request variables
+ username := r.FormValue("username")
+ password := r.FormValue("password")
+ otp := r.FormValue("otp")
+ session := r.FormValue("session")
+
+ optRequired := false
+ 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)
+ }
+ } else {
+ s, optRequired = Login(r, username, password)
+ }
+
+ if optRequired {
+ w.WriteHeader(http.StatusUnauthorized)
+ ReturnJSON(w, r, map[string]interface{}{
+ "status": "error",
+ "err_msg": "OTP Required",
+ "session": s.Key,
+ })
+ return
+ }
+
+ if s == nil {
+ w.WriteHeader(http.StatusForbidden)
+ ReturnJSON(w, r, map[string]interface{}{
+ "status": "error",
+ "err_msg": "Invalid username or password",
+ })
+ return
+ }
+
+ jwt := SetSessionCookie(w, r, s)
+ ReturnJSON(w, r, map[string]interface{}{
+ "status": "ok",
+ "session": s.Key,
+ "jwt": jwt,
+ "user": map[string]interface{}{
+ "username": s.User.Username,
+ "first_name": s.User.FirstName,
+ "last_name": s.User.LastName,
+ "group_id": s.User.UserGroupID,
+ "admin": s.User.Admin,
+ },
+ })
+}
diff --git a/d_api_logout.go b/d_api_logout.go
new file mode 100644
index 00000000..37b313ec
--- /dev/null
+++ b/d_api_logout.go
@@ -0,0 +1,7 @@
+package uadmin
+
+import "net/http"
+
+func dAPILogoutHandler(w http.ResponseWriter, r *http.Request, s *Session) {
+
+}
diff --git a/d_api_read.go b/d_api_read.go
index e7c6386a..0f231af3 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" {
+ if 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())
@@ -205,7 +205,7 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) {
rowsCount = 1
}
- if params["$preload"] == "1" {
+ if params["$preload"] == "1" || params["$preload"] == "true" {
Preload(m.Interface())
}
diff --git a/d_api_reset_password.go b/d_api_reset_password.go
new file mode 100644
index 00000000..22034da9
--- /dev/null
+++ b/d_api_reset_password.go
@@ -0,0 +1,7 @@
+package uadmin
+
+import "net/http"
+
+func dAPIResetPasswordHandler(w http.ResponseWriter, r *http.Request, s *Session) {
+
+}
diff --git a/d_api_signup.go b/d_api_signup.go
new file mode 100644
index 00000000..c1131139
--- /dev/null
+++ b/d_api_signup.go
@@ -0,0 +1,7 @@
+package uadmin
+
+import "net/http"
+
+func dAPISignupHandler(w http.ResponseWriter, r *http.Request, s *Session) {
+
+}
diff --git a/get_schema.go b/get_schema.go
index b4e9e250..281003f0 100644
--- a/get_schema.go
+++ b/get_schema.go
@@ -36,8 +36,13 @@ func getSchema(a interface{}) (s ModelSchema, ok bool) {
s.ModelName = strings.ToLower(t.Name())
s.DisplayName = getDisplayName(t.Name())
s.TableName = db.Config.NamingStrategy.TableName(s.Name)
+ s.Category = func() string {
+ dashboard := DashboardMenu{}
+ Get(&dashboard, "url = ?", modelName)
+ return dashboard.Cat
+ }()
- // Analize the fields of the model and add them to the fields list
+ // Analyze the fields of the model and add them to the fields list
s.Fields = []F{}
// Add inlines to schema
diff --git a/global.go b/global.go
index 264de224..c6292a20 100644
--- a/global.go
+++ b/global.go
@@ -352,7 +352,7 @@ var PasswordAttempts = 5
var PasswordTimeout = 15
// AllowedHosts is a comma separated list of allowed hosts for the server to work. The
-// default value if only for development and production domain should be added before
+// default value is only for development. Production domain should be added before
// deployment
var AllowedHosts = "0.0.0.0,127.0.0.1,localhost,::1"
@@ -373,6 +373,12 @@ var TimeZone = "local"
// TrailCacheSize is the number of bytes to keep in memory of trail logs
var TrailCacheSize = 65536
+// AllowDAPISignup allows unauthenticated users to sign up
+var AllowDAPISignup = false
+
+// DAPISignupGroupID is the default user group id new users get when they sign up
+var DAPISignupGroupID = false
+
// Private Global Variables
// Regex
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
diff --git a/login_handler.go b/login_handler.go
index 11e389e7..d83c49e6 100644
--- a/login_handler.go
+++ b/login_handler.go
@@ -58,10 +58,6 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
c.ErrExists = true
c.Err = "Invalid username/password or inactive user"
} else {
- if session.PendingOTP {
- Trail(INFO, "User: %s OTP: %s", session.User.Username, session.User.GetOTP())
- }
-
// Set session cookie
SetSessionCookie(w, r, session)
diff --git a/main_handler.go b/main_handler.go
index f1df78c5..e17bef0e 100644
--- a/main_handler.go
+++ b/main_handler.go
@@ -9,6 +9,7 @@ import (
// mainHandler is the main handler for the admin
func mainHandler(w http.ResponseWriter, r *http.Request) {
if !CheckRateLimit(r) {
+ w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte("Slow down. You are going too fast!"))
return
}
diff --git a/openapi.go b/openapi.go
new file mode 100644
index 00000000..a4a1ac62
--- /dev/null
+++ b/openapi.go
@@ -0,0 +1,560 @@
+package uadmin
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "strconv"
+
+ "github.com/uadmin/uadmin/openapi"
+)
+
+var CustomOpenAPI func(*openapi.Schema)
+var CustomOpenAPIJSON func([]byte) []byte
+
+func GenerateOpenAPISchema() {
+ s := openapi.GenerateBaseSchema()
+
+ // Customize schema
+ s.Info.Title = SiteName + s.Info.Title
+
+ // Add Models to /components/schema
+ for _, v := range Schema {
+ // Parse fields
+ fields := map[string]*openapi.SchemaObject{}
+ required := []string{}
+ parameters := []openapi.Parameter{}
+ writeParameters := []openapi.Parameter{}
+ for i := range v.Fields {
+ // Determine data type
+ fields[v.Fields[i].Name] = func() *openapi.SchemaObject {
+ switch v.Fields[i].Type {
+ case cID:
+ return &openapi.SchemaObject{
+ Type: "integer",
+ }
+ case cSTRING:
+ return &openapi.SchemaObject{
+ Type: "string",
+ }
+ case cBOOL:
+ return &openapi.SchemaObject{
+ Type: "boolean",
+ }
+ case cCODE:
+ return &openapi.SchemaObject{
+ Type: "boolean",
+ }
+ case cDATE:
+ return &openapi.SchemaObject{
+ Type: "string",
+ }
+ case cEMAIL:
+ return &openapi.SchemaObject{
+ Type: "string",
+ }
+ case cFILE:
+ return &openapi.SchemaObject{
+ Type: "string",
+ }
+ case cIMAGE:
+ return &openapi.SchemaObject{
+ Type: "string",
+ }
+ case cFK:
+ return &openapi.SchemaObject{
+ Type: "object",
+ Items: &openapi.SchemaObject{Ref: "#/components/schemas/" + v.Fields[i].TypeName},
+ }
+ case cHTML:
+ return &openapi.SchemaObject{
+ Type: "string",
+ }
+ case cLINK:
+ return &openapi.SchemaObject{
+ Type: "string",
+ }
+ case cLIST:
+ return &openapi.SchemaObject{
+ Type: "string",
+ Enum: func() []interface{} {
+ vals := make([]interface{}, len(v.Fields[i].Choices))
+ for j := range v.Fields[i].Choices {
+ vals[j] = v.Fields[i].Choices[j].V
+ }
+ return vals
+ }(),
+ }
+ case cM2M:
+ return &openapi.SchemaObject{
+ Type: "array",
+ Items: &openapi.SchemaObject{Ref: "#/components/schemas/" + v.Fields[i].TypeName},
+ }
+ case cMONEY:
+ return &openapi.SchemaObject{
+ Type: "number",
+ }
+ case cNUMBER:
+ switch v.Fields[i].TypeName {
+ case "float64":
+ return &openapi.SchemaObject{
+ Type: "number",
+ }
+ case "int":
+ return &openapi.SchemaObject{
+ Type: "integer",
+ }
+ default:
+ return &openapi.SchemaObject{
+ Type: "integer",
+ }
+ }
+ case cMULTILINGUAL:
+ return &openapi.SchemaObject{
+ Type: "string",
+ }
+ case cPROGRESSBAR:
+ switch v.Fields[i].TypeName {
+ case "float64":
+ return &openapi.SchemaObject{
+ Type: "number",
+ }
+ case "int":
+ return &openapi.SchemaObject{
+ Type: "integer",
+ }
+ default:
+ return &openapi.SchemaObject{
+ Type: "integer",
+ }
+ }
+ }
+ // If unknown, use string
+ return &openapi.SchemaObject{
+ Type: "string",
+ }
+ }()
+
+ // 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)
+ }
+
+ // Add parameters
+ // skip method fields
+ if v.Fields[i].IsMethod {
+ continue
+ }
+ parameters = append(parameters, func() openapi.Parameter {
+ if v.Fields[i].Type == cID {
+ return openapi.Parameter{
+ Ref: "#/components/parameters/QueryID",
+ }
+ }
+ 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: "Query for " + v.Fields[i].DisplayName,
+ Schema: func() *openapi.SchemaObject {
+ switch v.Fields[i].Type {
+ case cSTRING:
+ case cCODE:
+ case cEMAIL:
+ case cFILE:
+ case cIMAGE:
+ case cHTML:
+ case cLINK:
+ case cMULTILINGUAL:
+ case cPASSWORD:
+ return &openapi.SchemaObject{
+ Ref: "#/components/schemas/String",
+ }
+ case cID:
+ case cFK:
+ case cLIST:
+ case cMONEY:
+ return &openapi.SchemaObject{
+ Ref: "#/components/schemas/Integer",
+ }
+ case cNUMBER:
+ case cPROGRESSBAR:
+ return &openapi.SchemaObject{
+ Ref: "#/components/schemas/Number",
+ }
+ case cBOOL:
+ return &openapi.SchemaObject{
+ Ref: "#/components/schemas/Boolean",
+ }
+ case cDATE:
+ return &openapi.SchemaObject{
+ Ref: "#/components/schemas/DateTime",
+ }
+ }
+ return &openapi.SchemaObject{
+ Ref: "#/components/schemas/String",
+ }
+ }(),
+ }
+ }(),
+ )
+
+ if v.Fields[i].Type == cID {
+ 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:
+ case cCODE:
+ case cEMAIL:
+ case cFILE:
+ case cIMAGE:
+ case cHTML:
+ case cLINK:
+ case cMULTILINGUAL:
+ case cPASSWORD:
+ return &openapi.SchemaObject{
+ Ref: "#/components/schemas/String",
+ }
+ case cID:
+ case cFK:
+ case cLIST:
+ case cMONEY:
+ return &openapi.SchemaObject{
+ Ref: "#/components/schemas/Integer",
+ }
+ case cNUMBER:
+ case cPROGRESSBAR:
+ return &openapi.SchemaObject{
+ Ref: "#/components/schemas/Number",
+ }
+ case cBOOL:
+ return &openapi.SchemaObject{
+ Ref: "#/components/schemas/Boolean",
+ }
+ case cDATE:
+ return &openapi.SchemaObject{
+ Ref: "#/components/schemas/DateTime",
+ }
+ }
+ return &openapi.SchemaObject{
+ Ref: "#/components/schemas/String",
+ }
+ }(),
+ }
+ }(),
+ )
+
+ // Add required fields
+ if v.Fields[i].Required {
+ required = append(required, v.Fields[i].Name)
+ }
+ }
+
+ // 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,
+ Get: &openapi.Operation{
+ Tags: []string{v.Name, func() string {
+ if v.Category != "" {
+ return v.Category
+ } else {
+ return "Other"
+ }
+ }()},
+ Responses: map[string]openapi.Response{
+ "200": {
+ Description: v.DisplayName + " record",
+ Content: map[string]openapi.MediaType{
+ "application/json": {
+ Schema: &openapi.SchemaObject{
+ Ref: "#/components/schemas/" + v.Name,
+ },
+ },
+ },
+ },
+ },
+ },
+ Post: &openapi.Operation{
+ Tags: []string{v.Name, func() string {
+ if v.Category != "" {
+ return v.Category
+ } else {
+ return "Other"
+ }
+ }()},
+ Responses: map[string]openapi.Response{
+ "200": {
+ Description: v.DisplayName + " record",
+ Content: map[string]openapi.MediaType{
+ "application/json": {
+ Schema: &openapi.SchemaObject{
+ Ref: "#/components/schemas/" + v.Name,
+ },
+ },
+ },
+ },
+ },
+ },
+ 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 one " + v.DisplayName,
+ Description: "Read one " + v.DisplayName,
+ Get: &openapi.Operation{
+ Tags: []string{v.Name, func() string {
+ if v.Category != "" {
+ return v.Category
+ } else {
+ return "Other"
+ }
+ }()},
+ Responses: map[string]openapi.Response{
+ "200": {
+ Description: v.DisplayName + " record",
+ Content: map[string]openapi.MediaType{
+ "application/json": {
+ Schema: &openapi.SchemaObject{
+ Ref: "#/components/schemas/" + v.Name,
+ },
+ },
+ },
+ },
+ },
+ },
+ Post: &openapi.Operation{
+ Tags: []string{v.Name, func() string {
+ if v.Category != "" {
+ return v.Category
+ } else {
+ return "Other"
+ }
+ }()},
+ Responses: map[string]openapi.Response{
+ "200": {
+ Description: v.DisplayName + " record",
+ Content: map[string]openapi.MediaType{
+ "application/json": {
+ Schema: &openapi.SchemaObject{
+ Ref: "#/components/schemas/" + v.Name,
+ },
+ },
+ },
+ },
+ },
+ },
+ 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",
+ },
+ }...),
+ }
+ // Add One
+ s.Paths[fmt.Sprintf("/api/d/%s/add", v.ModelName)] = openapi.Path{
+ Summary: "Add one " + v.DisplayName,
+ Description: "Add one " + v.DisplayName,
+ Get: &openapi.Operation{
+ Tags: []string{v.Name, func() string {
+ if v.Category != "" {
+ return v.Category
+ } else {
+ return "Other"
+ }
+ }()},
+ Responses: map[string]openapi.Response{
+ "200": {
+ Description: v.DisplayName + " record",
+ Content: map[string]openapi.MediaType{
+ "application/json": {
+ Schema: &openapi.SchemaObject{
+ Ref: "#/components/schemas/" + v.Name,
+ },
+ },
+ },
+ },
+ },
+ },
+ Post: &openapi.Operation{
+ Tags: []string{v.Name, func() string {
+ if v.Category != "" {
+ return v.Category
+ } else {
+ return "Other"
+ }
+ }()},
+ Responses: map[string]openapi.Response{
+ "200": {
+ Description: v.DisplayName + " record",
+ Content: map[string]openapi.MediaType{
+ "application/json": {
+ Schema: &openapi.SchemaObject{
+ Ref: "#/components/schemas/" + v.Name,
+ },
+ },
+ },
+ },
+ },
+ },
+ Parameters: append(writeParameters, []openapi.Parameter{
+ {
+ 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,
+ Get: &openapi.Operation{
+ Tags: []string{v.Name, func() string {
+ if v.Category != "" {
+ return v.Category
+ } else {
+ return "Other"
+ }
+ }()},
+ Responses: map[string]openapi.Response{
+ "200": {
+ Description: v.DisplayName + " record",
+ Content: map[string]openapi.MediaType{
+ "application/json": {
+ Schema: &openapi.SchemaObject{
+ Ref: "#/components/schemas/" + v.Name,
+ },
+ },
+ },
+ },
+ },
+ },
+ Post: &openapi.Operation{
+ Tags: []string{v.Name, func() string {
+ if v.Category != "" {
+ return v.Category
+ } else {
+ return "Other"
+ }
+ }()},
+ Responses: map[string]openapi.Response{
+ "200": {
+ Description: v.DisplayName + " record",
+ Content: map[string]openapi.MediaType{
+ "application/json": {
+ Schema: &openapi.SchemaObject{
+ Ref: "#/components/schemas/" + v.Name,
+ },
+ },
+ },
+ },
+ },
+ },
+ Parameters: append(writeParameters, []openapi.Parameter{
+ {
+ Ref: "#/components/parameters/CSRF",
+ },
+ {
+ Ref: "#/components/parameters/stat",
+ },
+ }...),
+ }
+
+ s.Components.Schemas[v.Name] = openapi.SchemaObject{
+ Type: "object",
+ Properties: fields,
+ Required: required,
+ }
+ }
+
+ // run custom OpenAPI handler
+ if CustomOpenAPI != nil {
+ CustomOpenAPI(s)
+ }
+
+ buf := getOpenAPIJSON(s)
+ os.WriteFile("./openapi.json", buf, 0644)
+}
+
+func getOpenAPIJSON(s *openapi.Schema) []byte {
+ buf, err := json.Marshal(*s)
+ if err != nil {
+ return nil
+ }
+ if CustomOpenAPIJSON != nil {
+ buf = CustomOpenAPIJSON(buf)
+ }
+ return buf
+}
diff --git a/openapi.json b/openapi.json
deleted file mode 100644
index bf085f70..00000000
--- a/openapi.json
+++ /dev/null
@@ -1,65 +0,0 @@
-{
- "openapi": "3.0.0",
- "info": {
- "title": "dAPI Overview",
- "description": "dAPI Overview",
- "version": "v3.0.0"
- },
- "paths": {
- "/api/d/user/read/{id}": {
- "summary": "dAPI read one user",
- "description": "returns an object for a single user that has the primary key `{id}`",
- "get": {
- "operationId": "readOneUser",
- "responses": {
- "200": {
- "description": "Valid response",
- "examples": {
- "application/json": ""
- }
- }
- }
- },
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "description": "primary key of the record",
- "required": true
- }
- ]
- },
- "/api/d/user/read": {
- "summary": "dAPI read multiple users",
- "description": "returns an array of objects for a multiple users",
- "get": {
- "operationId": "readOneUser",
- "responses": {
- "200": {
- "description": "Valid response",
- "examples": {
- "application/json": ""
- }
- }
- }
- },
- "parameters": [
- {
- "name": "id",
- "in": "query",
- "description": "primary key of the record",
- "required": true
- },
- {
- "name": "username",
- "in": "query",
- "description": "user name, can be appended with `__contains`, `__startswith`, `__endswith`, `__re`, `__icontains`, `__istartswith`, `__iendswith` or prepended with `!` to negate the condition",
- "required": true
- }
- ]
- }
- },
- "consumes": [
- "application/json"
- ]
- }
\ No newline at end of file
diff --git a/openapi/callback.go b/openapi/callback.go
new file mode 100644
index 00000000..9ffb812b
--- /dev/null
+++ b/openapi/callback.go
@@ -0,0 +1,3 @@
+package openapi
+
+type Callback map[string]Path
diff --git a/openapi/components.go b/openapi/components.go
new file mode 100644
index 00000000..c7a3e183
--- /dev/null
+++ b/openapi/components.go
@@ -0,0 +1,14 @@
+package openapi
+
+type Components struct {
+ Schemas map[string]SchemaObject `json:"schemas,omitempty"`
+ Responses map[string]Response `json:"responses,omitempty"`
+ Parameters map[string]Parameter `json:"parameters,omitempty"`
+ Examples map[string]Example `json:"examples,omitempty"`
+ RequestBodies map[string]RequestBody `json:"requestBodies,omitempty"`
+ Headers map[string]Header `json:"headers,omitempty"`
+ SecuritySchemes map[string]SecurityScheme `json:"securitySchemes,omitempty"`
+ Links map[string]Link `json:"links,omitempty"`
+ Callbacks map[string]Callback `json:"callbacks,omitempty"`
+ PathItems map[string]Path `json:"pathItems,omitempty"`
+}
diff --git a/openapi/contact.go b/openapi/contact.go
new file mode 100644
index 00000000..3b643e20
--- /dev/null
+++ b/openapi/contact.go
@@ -0,0 +1,7 @@
+package openapi
+
+type Contact struct {
+ Name string `json:"name,omitempty"`
+ URL string `json:"url,omitempty"`
+ Email string `json:"email,omitempty"`
+}
diff --git a/openapi/discriminator.go b/openapi/discriminator.go
new file mode 100644
index 00000000..867a5b45
--- /dev/null
+++ b/openapi/discriminator.go
@@ -0,0 +1,6 @@
+package openapi
+
+type Discriminator struct {
+ PropertyName string `json:"propertyName,omitempty"`
+ Mapping map[string]string `json:"mapping,omitempty"`
+}
diff --git a/openapi/encoding.go b/openapi/encoding.go
new file mode 100644
index 00000000..1b6d13c3
--- /dev/null
+++ b/openapi/encoding.go
@@ -0,0 +1,9 @@
+package openapi
+
+type Encoding struct {
+ ContentType string `json:"contentType,omitempty"`
+ Headers map[string]Header `json:"headers,omitempty"`
+ Style string `json:"style,omitempty"`
+ Explode bool `json:"explode,omitempty"`
+ AllowReserved bool `json:"allowReserved,omitempty"`
+}
diff --git a/openapi/example.go b/openapi/example.go
new file mode 100644
index 00000000..366f6e30
--- /dev/null
+++ b/openapi/example.go
@@ -0,0 +1,9 @@
+package openapi
+
+type Example struct {
+ Ref string `json:"$ref,omitempty"`
+ Summary string `json:"summary,omitempty"`
+ Description string `json:"description,omitempty"`
+ Value interface{} `json:"value,omitempty"`
+ ExternalValue string `json:"externalValue,omitempty"`
+}
diff --git a/openapi/external_docs.go b/openapi/external_docs.go
new file mode 100644
index 00000000..56956dce
--- /dev/null
+++ b/openapi/external_docs.go
@@ -0,0 +1,6 @@
+package openapi
+
+type ExternalDocs struct {
+ Description string `json:"description,omitempty"`
+ URL string `json:"url,omitempty"`
+}
diff --git a/openapi/generate_schema.go b/openapi/generate_schema.go
new file mode 100644
index 00000000..abe8be48
--- /dev/null
+++ b/openapi/generate_schema.go
@@ -0,0 +1,406 @@
+package openapi
+
+func GenerateBaseSchema() *Schema {
+ s := &Schema{
+ OpenAPI: "3.0.0",
+ Info: &SchemaInfo{
+ Title: " API documentation",
+ Description: "API documentation",
+ Version: "1.0.0",
+ },
+ Components: &Components{
+ Schemas: map[string]SchemaObject{
+ "Integer": {
+ Type: "integer",
+ XFilters: []XModifier{
+ {Modifier: "__gt", In: "suffix", Summary: "Greater than"},
+ {Modifier: "__gte", In: "suffix", Summary: "Greater than or equal to"},
+ {Modifier: "__lt", In: "suffix", Summary: "Less than"},
+ {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"},
+ },
+ XAggregator: []XModifier{
+ {Modifier: "__sum", In: "suffix", Summary: "Returns the total sum of a numeric field"},
+ {Modifier: "__avg", In: "suffix", Summary: "Returns the average value of a numeric field"},
+ {Modifier: "__min", In: "suffix", Summary: "Returns the smallest value of a numeric field"},
+ {Modifier: "__max", In: "suffix", Summary: "Returns the largest value of a numeric field"},
+ {Modifier: "__count", In: "suffix", Summary: "Returns the number of rows"},
+ },
+ },
+ "Number": {
+ Type: "number",
+ XFilters: []XModifier{
+ {Modifier: "__gt", In: "suffix", Summary: "Greater than"},
+ {Modifier: "__gte", In: "suffix", Summary: "Greater than or equal to"},
+ {Modifier: "__lt", In: "suffix", Summary: "Less than"},
+ {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"},
+ },
+ XAggregator: []XModifier{
+ {Modifier: "__sum", In: "suffix", Summary: "Returns the total sum of a numeric field"},
+ {Modifier: "__avg", In: "suffix", Summary: "Returns the average value of a numeric field"},
+ {Modifier: "__min", In: "suffix", Summary: "Returns the smallest value of a numeric field"},
+ {Modifier: "__max", In: "suffix", Summary: "Returns the largest value of a numeric field"},
+ {Modifier: "__count", In: "suffix", Summary: "Returns the number of rows"},
+ },
+ },
+ "String": {
+ Type: "string",
+ XFilters: []XModifier{
+ {Modifier: "__contains", In: "suffix", Summary: "Search for string values that contains"},
+ {Modifier: "__startswith", In: "suffix", Summary: "Search for string values that starts with a given substring"},
+ {Modifier: "__endswith", In: "suffix", Summary: "Search for string values that ends with a given substring"},
+ {Modifier: "__re", In: "suffix", Summary: "Search for string values that matches regular expression"},
+ {Modifier: "__icontains", In: "suffix", Summary: "Search for string values that contains"},
+ {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"},
+ },
+ XAggregator: []XModifier{
+ {Modifier: "__count", In: "suffix", Summary: "Returns the number of rows"},
+ },
+ },
+ "DateTime": {
+ Type: "string",
+ XFilters: []XModifier{
+ {Modifier: "__contains", In: "suffix", Summary: "Search for string values that contains"},
+ {Modifier: "__startswith", In: "suffix", Summary: "Search for string values that starts with a given substring"},
+ {Modifier: "__endswith", In: "suffix", Summary: "Search for string values that ends with a given substring"},
+ {Modifier: "__re", In: "suffix", Summary: "Search for string values that matches regular expression"},
+ {Modifier: "__icontains", In: "suffix", Summary: "Search for string values that contains"},
+ {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"},
+ },
+ XAggregator: []XModifier{
+ {Modifier: "__count", In: "suffix", Summary: "Returns the number of rows"},
+ {Modifier: "__date", In: "suffix", Summary: "Returns DATE() of the field"},
+ {Modifier: "__year", In: "suffix", Summary: "Returns YEAR() of the field"},
+ {Modifier: "__month", In: "suffix", Summary: "Returns MONTH() of the field"},
+ {Modifier: "__day", In: "suffix", Summary: "Returns DAY() of the field"},
+ },
+ },
+ "Boolean": {
+ Type: "boolean",
+ XAggregator: []XModifier{
+ {Modifier: "__count", In: "suffix", Summary: "Returns the number of rows"},
+ },
+ },
+ "GeneralError": {
+ Type: "object",
+ Properties: map[string]*SchemaObject{
+ "status": {
+ Type: "string",
+ },
+ "err_msg": {
+ Type: "string",
+ },
+ },
+ },
+ },
+ Parameters: map[string]Parameter{
+ "PathID": {
+ Name: "id",
+ In: "path",
+ Description: "Primary key of the record",
+ Required: true,
+ Schema: &SchemaObject{
+ Type: "integer",
+ },
+ },
+ "QueryID": {
+ Name: "id",
+ In: "query",
+ Description: "Primary key of the record",
+ Required: false,
+ Schema: &SchemaObject{
+ Ref: "#/components/schemas/Integer",
+ },
+ },
+ "CSRF": {
+ Name: "x-csrf-token",
+ In: "query",
+ Description: "Token for CSRF protection which should be set to the session token or JWT token",
+ Required: true,
+ Schema: &SchemaObject{
+ Type: "string",
+ },
+ },
+ "limit": {
+ Name: "$limit",
+ In: "query",
+ Description: "Maximum number of records to return",
+ Required: false,
+ AllowReserved: true,
+ Schema: &SchemaObject{
+ Type: "integer",
+ },
+ },
+ "offset": {
+ Name: "$offset",
+ In: "query",
+ Description: "Starting point to read in the list of records",
+ Required: false,
+ AllowReserved: true,
+ Schema: &SchemaObject{
+ Type: "integer",
+ },
+ },
+ "order": {
+ Name: "$order",
+ In: "query",
+ Description: "Sort the results. Use '-' for descending order and comma for more field",
+ Required: false,
+ AllowReserved: true,
+ Schema: &SchemaObject{
+ Type: "array",
+ Items: &SchemaObject{
+ Type: "string",
+ },
+ },
+ Examples: map[string]Example{
+ "multiColumn": {
+ Summary: "An example multi-column sorting with ascending and descending",
+ Value: "$order=id,-name",
+ },
+ },
+ },
+ "fields": {
+ Name: "$f",
+ In: "query",
+ Description: "Selecting fields to return in results",
+ Required: false,
+ AllowReserved: true,
+ Schema: &SchemaObject{
+ Type: "array",
+ Items: &SchemaObject{
+ Type: "string",
+ },
+ },
+ Examples: map[string]Example{
+ "multiColumn": {
+ Summary: "An example multi-column selection",
+ Value: "$f=id,name",
+ },
+ "aggColumn": {
+ Summary: "An example multi-column selection with aggregation function",
+ Value: "$f=id__count,score__sum",
+ },
+ "joinTable": {
+ Summary: "An example of multi-column selection from a different table using a join (see $join)",
+ Value: "$f=username,user_groups.group_name",
+ },
+ },
+ },
+ "groupBy": {
+ Name: "$groupby",
+ In: "query",
+ Description: "Groups rows that have the same values into summary rows",
+ Required: false,
+ AllowReserved: true,
+ Schema: &SchemaObject{
+ Type: "array",
+ Items: &SchemaObject{
+ Type: "string",
+ },
+ },
+ Examples: map[string]Example{
+ "simple": {
+ Summary: "An example of grouping results based on category",
+ Value: "$groupby=category_id",
+ },
+ "agg": {
+ Summary: "An example of grouping results based on year and month",
+ Value: "$groupby=date__year,date__month",
+ },
+ },
+ },
+ "deleted": {
+ Name: "$deleted",
+ In: "query",
+ Description: "Returns results including deleted records",
+ Required: false,
+ AllowReserved: true,
+ AllowEmptyValue: true,
+ Schema: &SchemaObject{
+ Type: "string",
+ },
+ Examples: map[string]Example{
+ "getDeleted": {
+ Summary: "An example of a query that returns deleted records",
+ Value: "$deleted=true",
+ },
+ },
+ },
+ "join": {
+ Name: "$join",
+ In: "query",
+ Description: "Joins results from another model based on a foreign key",
+ Required: false,
+ AllowReserved: true,
+ Schema: &SchemaObject{
+ Type: "array",
+ Items: &SchemaObject{
+ Type: "string",
+ },
+ },
+ Examples: map[string]Example{
+ "getGroupName": {
+ Summary: "An example of a query with a left join from users to user_groups",
+ Value: "$join=user_groups__left__user_group_id",
+ },
+ "getGroupNameInner": {
+ Summary: "An example of a query with a inner join from users to user_groups",
+ Value: "$join=user_groups__user_group_id",
+ },
+ },
+ },
+ "m2m": {
+ Name: "$m2m",
+ In: "query",
+ Description: "Returns results from M2M fields",
+ Required: false,
+ AllowReserved: true,
+ Schema: &SchemaObject{
+ Type: "string",
+ Description: "0=don't get, 1/fill=get full records, id=get ids only",
+ },
+ Examples: map[string]Example{
+ "fillAll": {
+ Summary: "An example of a query that fills all m2m records",
+ Value: "$m2m=fill",
+ },
+ "fillOne": {
+ Summary: "An example of a query that fills IDs from s specific m2m field called cards",
+ Value: "$m2m=cards__id",
+ },
+ },
+ },
+ "q": {
+ Name: "$q",
+ In: "query",
+ Description: "Searches all string fields marked as Searchable",
+ Required: false,
+ AllowReserved: true,
+ Schema: &SchemaObject{
+ Type: "string",
+ },
+ },
+ "preload": {
+ Name: "$preload",
+ In: "query",
+ Description: "Fills the data from foreign keys",
+ Required: false,
+ AllowReserved: true,
+ Schema: &SchemaObject{
+ Type: "string",
+ },
+ Examples: map[string]Example{
+ "getDeleted": {
+ Summary: "An example of a query that fills foreign key object",
+ Value: "$preload=true",
+ },
+ },
+ },
+ "next": {
+ Name: "$next",
+ In: "query",
+ 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,
+ Schema: &SchemaObject{
+ Type: "string",
+ },
+ },
+ "stat": {
+ Name: "$stat",
+ In: "query",
+ Description: "Returns the API call execution time in milliseconds",
+ Required: false,
+ AllowReserved: true,
+ Schema: &SchemaObject{
+ Type: "string",
+ },
+ Examples: map[string]Example{
+ "getDeleted": {
+ Summary: "An example of a query that measures the execution time",
+ Value: "$stat=1",
+ },
+ },
+ },
+ "or": {
+ Name: "$or",
+ In: "query",
+ 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,
+ Schema: &SchemaObject{
+ Type: "array",
+ Items: &SchemaObject{
+ Type: "string",
+ },
+ },
+ Examples: map[string]Example{
+ "simple": {
+ Summary: "An example of a query that returns records with active=1 or admin=1",
+ Value: "$or=active=1|admin=1",
+ },
+ "multiValueOr": {
+ Summary: "An example of a query that returns records where the name starts with the letter a or ends with the letter a",
+ Value: "$or=name__startswith=a|name__endswith=a",
+ },
+ "nestedAnd": {
+ Summary: "An example of a query that returns records with admin=1 or (active=1 and username=john)",
+ Value: "$or=admin=1|active=1+username=john",
+ },
+ },
+ },
+ },
+ Responses: map[string]Response{
+ "401": {
+ Description: "Access denied due to missing authentication or insufficient permissions",
+ Content: map[string]MediaType{
+ "application/json": {
+ Schema: &SchemaObject{
+ Ref: "#/components/schemas/GeneralError",
+ },
+ },
+ },
+ },
+ },
+ SecuritySchemes: map[string]SecurityScheme{
+ "apiKeyCookie": {
+ Type: "apiKey",
+ Name: "session",
+ In: "cookie",
+ },
+ "apiKeyQuery": {
+ Type: "apiKey",
+ Name: "session",
+ In: "query",
+ },
+ "JWT": {
+ Type: "http",
+ Scheme: "bearer",
+ BearerFormat: "JWT",
+ },
+ },
+ },
+ Paths: map[string]Path{},
+ Security: []SecurityRequirement{
+ {
+ "apiKeyCookie": []string{},
+ "apiKeyQuery": []string{},
+ "JWT": []string{},
+ },
+ },
+ }
+
+ return s
+}
diff --git a/openapi/header.go b/openapi/header.go
new file mode 100644
index 00000000..98d4caab
--- /dev/null
+++ b/openapi/header.go
@@ -0,0 +1,5 @@
+package openapi
+
+type Header struct {
+ Parameter
+}
diff --git a/openapi/license.go b/openapi/license.go
new file mode 100644
index 00000000..a0179f4c
--- /dev/null
+++ b/openapi/license.go
@@ -0,0 +1,7 @@
+package openapi
+
+type License struct {
+ Name string `json:"name,omitempty"`
+ Identifier string `json:"identifier,omitempty"`
+ URL string `json:"url,omitempty"`
+}
diff --git a/openapi/link.go b/openapi/link.go
new file mode 100644
index 00000000..0af33ddb
--- /dev/null
+++ b/openapi/link.go
@@ -0,0 +1,11 @@
+package openapi
+
+type Link struct {
+ Ref string `json:"$ref,omitempty"`
+ OperationRef string `json:"operationRef,omitempty"`
+ OperationId string `json:"operationId,omitempty"`
+ Parameters map[string]interface{} `json:"parameters,omitempty"`
+ RequestBody map[string]interface{} `json:"requestBody,omitempty"`
+ Description string `json:"description,omitempty"`
+ Server *Server `json:"server,omitempty"`
+}
diff --git a/openapi/media_type.go b/openapi/media_type.go
new file mode 100644
index 00000000..32194e9c
--- /dev/null
+++ b/openapi/media_type.go
@@ -0,0 +1,8 @@
+package openapi
+
+type MediaType struct {
+ Schema *SchemaObject `json:"schema,omitempty"`
+ Example *Example `json:"example,omitempty"`
+ Examples map[string]Example `json:"examples,omitempty"`
+ Encoding map[string]Encoding `json:"encoding,omitempty"`
+}
diff --git a/openapi/oauth_flow.go b/openapi/oauth_flow.go
new file mode 100644
index 00000000..15570885
--- /dev/null
+++ b/openapi/oauth_flow.go
@@ -0,0 +1,8 @@
+package openapi
+
+type OAuthFlow struct {
+ AuthorizationUrl string `json:"authorizationUrl,omitempty"`
+ TokenUrl string `json:"tokenUrl,omitempty"`
+ RefreshUrl string `json:"refreshUrl,omitempty"`
+ Scopes map[string]string `json:"scopes,omitempty"`
+}
diff --git a/openapi/oauth_flows.go b/openapi/oauth_flows.go
new file mode 100644
index 00000000..b35e6e6b
--- /dev/null
+++ b/openapi/oauth_flows.go
@@ -0,0 +1,8 @@
+package openapi
+
+type OAuthFlows struct {
+ Implicit *OAuthFlow `json:"implicit,omitempty"`
+ Password *OAuthFlow `json:"password,omitempty"`
+ ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty"`
+ AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty"`
+}
diff --git a/openapi/operation.go b/openapi/operation.go
new file mode 100644
index 00000000..3322ff70
--- /dev/null
+++ b/openapi/operation.go
@@ -0,0 +1,16 @@
+package openapi
+
+type Operation struct {
+ Tags []string `json:"tags,omitempty"`
+ Summary string `json:"summary,omitempty"`
+ Description string `json:"description,omitempty"`
+ ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"`
+ OperationId string `json:"operationId,omitempty"`
+ Parameters []Parameter `json:"parameters,omitempty"`
+ RequestBody *RequestBody `json:"requestBody,omitempty"`
+ Responses map[string]Response `json:"responses,omitempty"`
+ Callbacks map[string]Callback `json:"callbacks,omitempty"`
+ Deprecated bool `json:"deprecated,omitempty"`
+ Security []SecurityRequirement `json:"security,omitempty"`
+ Servers []Server `json:"servers,omitempty"`
+}
diff --git a/openapi/parameter.go b/openapi/parameter.go
new file mode 100644
index 00000000..7b8376ef
--- /dev/null
+++ b/openapi/parameter.go
@@ -0,0 +1,18 @@
+package openapi
+
+type Parameter struct {
+ Ref string `json:"$ref,omitempty"`
+ Name string `json:"name,omitempty"`
+ In string `json:"in,omitempty"`
+ Description string `json:"description,omitempty"`
+ Required bool `json:"required,omitempty"`
+ Deprecated bool `json:"deprecated,omitempty"`
+ AllowEmptyValue bool `json:"allowEmptyValue,omitempty"`
+ Style string `json:"style,omitempty"`
+ Explode bool `json:"explode,omitempty"`
+ AllowReserved bool `json:"allowReserved,omitempty"`
+ Schema *SchemaObject `json:"schema,omitempty"`
+ Example *Example `json:"example,omitempty"`
+ Examples map[string]Example `json:"examples,omitempty"`
+ Content map[string]MediaType `json:"content,omitempty"`
+}
diff --git a/openapi/path.go b/openapi/path.go
new file mode 100644
index 00000000..ad910d1a
--- /dev/null
+++ b/openapi/path.go
@@ -0,0 +1,17 @@
+package openapi
+
+type Path struct {
+ Ref string `json:"$ref,omitempty"`
+ Summary string `json:"summary,omitempty"`
+ Description string `json:"description,omitempty"`
+ Get *Operation `json:"get,omitempty"`
+ Put *Operation `json:"put,omitempty"`
+ Post *Operation `json:"post,omitempty"`
+ Delete *Operation `json:"delete,omitempty"`
+ Options *Operation `json:"options,omitempty"`
+ Head *Operation `json:"head,omitempty"`
+ Patch *Operation `json:"patch,omitempty"`
+ Trace *Operation `json:"trace,omitempty"`
+ Servers []Server `json:"servers,omitempty"`
+ Parameters []Parameter `json:"parameters,omitempty"`
+}
diff --git a/openapi/request_body.go b/openapi/request_body.go
new file mode 100644
index 00000000..1bef0eed
--- /dev/null
+++ b/openapi/request_body.go
@@ -0,0 +1,8 @@
+package openapi
+
+type RequestBody struct {
+ Ref string `json:"$ref,omitempty"`
+ Description string `json:"description,omitempty"`
+ Content map[string]MediaType `json:"content,omitempty"`
+ Required bool `json:"required,omitempty"`
+}
diff --git a/openapi/response.go b/openapi/response.go
new file mode 100644
index 00000000..ed28cbce
--- /dev/null
+++ b/openapi/response.go
@@ -0,0 +1,9 @@
+package openapi
+
+type Response struct {
+ Ref string `json:"$ref,omitempty"`
+ Description string `json:"description,omitempty"`
+ Headers map[string]Header `json:"headers,omitempty"`
+ Content map[string]MediaType `json:"content,omitempty"`
+ Links map[string]Link `json:"links,omitempty"`
+}
diff --git a/openapi/schema.go b/openapi/schema.go
new file mode 100644
index 00000000..3e98ce92
--- /dev/null
+++ b/openapi/schema.go
@@ -0,0 +1,14 @@
+package openapi
+
+type Schema struct {
+ OpenAPI string `json:"openapi,omitempty"`
+ Info *SchemaInfo `json:"info,omitempty"`
+ JSONSchemaDialect string `json:"jsonSchemaDialect,omitempty"`
+ Servers []Server `json:"servers,omitempty"`
+ Paths map[string]Path `json:"paths,omitempty"`
+ Webhooks map[string]Path `json:"webhooks,omitempty"`
+ Components *Components `json:"components,omitempty"`
+ Security []SecurityRequirement `json:"security,omitempty"`
+ Tags []Tag `json:"tags,omitempty"`
+ ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"`
+}
diff --git a/openapi/schema_info.go b/openapi/schema_info.go
new file mode 100644
index 00000000..f4dbfe35
--- /dev/null
+++ b/openapi/schema_info.go
@@ -0,0 +1,11 @@
+package openapi
+
+type SchemaInfo struct {
+ Title string `json:"title,omitempty"`
+ Summary string `json:"summary,omitempty"`
+ Description string `json:"description,omitempty"`
+ TermsOfService string `json:"termsOfService,omitempty"`
+ Contact *Contact `json:"contact,omitempty"`
+ License *License `json:"license,omitempty"`
+ Version string `json:"version,omitempty"`
+}
diff --git a/openapi/schema_object.go b/openapi/schema_object.go
new file mode 100644
index 00000000..aac02a07
--- /dev/null
+++ b/openapi/schema_object.go
@@ -0,0 +1,24 @@
+package openapi
+
+type SchemaObject struct {
+ Ref string `json:"$ref,omitempty"`
+ Type string `json:"type,omitempty"`
+ Pattern string `json:"pattern,omitempty"`
+ Maximum int `json:"maximum,omitempty"`
+ Minimum int `json:"minimum,omitempty"`
+ Required []string `json:"required,omitempty"`
+ Title string `json:"title,omitempty"`
+ Description string `json:"description,omitempty"`
+ Default string `json:"default,omitempty"`
+ ReadOnly bool `json:"ReadOnly,omitempty"`
+ Examples []Example `json:"examples,omitempty"`
+ Items *SchemaObject `json:"items,omitempty"`
+ Properties map[string]*SchemaObject `json:"properties,omitempty"`
+ Discriminator *Discriminator `json:"discriminator,omitempty"`
+ XML *XML `json:"xml,omitempty"`
+ ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"`
+ Example *Example `json:"example,omitempty"`
+ Enum []interface{} `json:"enum,omitempty"`
+ XFilters []XModifier `json:"x-filter,omitempty"`
+ XAggregator []XModifier `json:"x-aggregator,omitempty"`
+}
diff --git a/openapi/security_requirment.go b/openapi/security_requirment.go
new file mode 100644
index 00000000..d13fa29b
--- /dev/null
+++ b/openapi/security_requirment.go
@@ -0,0 +1,3 @@
+package openapi
+
+type SecurityRequirement map[string][]string
diff --git a/openapi/security_scheme.go b/openapi/security_scheme.go
new file mode 100644
index 00000000..ec7073a7
--- /dev/null
+++ b/openapi/security_scheme.go
@@ -0,0 +1,13 @@
+package openapi
+
+type SecurityScheme struct {
+ Ref string `json:"$ref,omitempty"`
+ Type string `json:"type,omitempty"`
+ Description string `json:"description,omitempty"`
+ Name string `json:"name,omitempty"`
+ In string `json:"in,omitempty"`
+ Scheme string `json:"scheme,omitempty"`
+ BearerFormat string `json:"bearerFormat,omitempty"`
+ Flows *OAuthFlows `json:"flows,omitempty"`
+ OpenIdConnectUrl string `json:"openIdConnectUrl,omitempty"`
+}
diff --git a/openapi/server.go b/openapi/server.go
new file mode 100644
index 00000000..1f1f9ee2
--- /dev/null
+++ b/openapi/server.go
@@ -0,0 +1,7 @@
+package openapi
+
+type Server struct {
+ URL string `json:"url,omitempty"`
+ Description string `json:"description,omitempty"`
+ Variables map[string]OpenAPIServerVariable `json:"variables,omitempty"`
+}
diff --git a/openapi/server_variable.go b/openapi/server_variable.go
new file mode 100644
index 00000000..c9010333
--- /dev/null
+++ b/openapi/server_variable.go
@@ -0,0 +1,7 @@
+package openapi
+
+type OpenAPIServerVariable struct {
+ Enum []string `json:"enum,omitempty"`
+ Default string `json:"default,omitempty"`
+ Description string `json:"description,omitempty"`
+}
diff --git a/openapi/tag.go b/openapi/tag.go
new file mode 100644
index 00000000..832ee67d
--- /dev/null
+++ b/openapi/tag.go
@@ -0,0 +1,7 @@
+package openapi
+
+type Tag struct {
+ Name string `json:"name,omitempty"`
+ Description string `json:"description,omitempty"`
+ ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"`
+}
diff --git a/openapi/x_modifier.go b/openapi/x_modifier.go
new file mode 100644
index 00000000..c29fc779
--- /dev/null
+++ b/openapi/x_modifier.go
@@ -0,0 +1,7 @@
+package openapi
+
+type XModifier struct {
+ Modifier string `json:"modifier,omitempty"`
+ In string `json:"in,omitempty"`
+ Summary string `json:"summary,omitempty"`
+}
diff --git a/openapi/xml.go b/openapi/xml.go
new file mode 100644
index 00000000..c96bca8e
--- /dev/null
+++ b/openapi/xml.go
@@ -0,0 +1,9 @@
+package openapi
+
+type XML struct {
+ Name string `json:"name,omitempty"`
+ Namespace string `json:"namespace,omitempty"`
+ Prefix string `json:"prefix,omitempty"`
+ Attribute bool `json:"attribute,omitempty"`
+ Wrapped bool `json:"wrapped,omitempty"`
+}
diff --git a/openapi_template.json b/openapi_template.json
new file mode 100644
index 00000000..c2653f66
--- /dev/null
+++ b/openapi_template.json
@@ -0,0 +1,711 @@
+{
+ "openapi": "3.0.1",
+ "info": {
+ "title": "dAPI documentation",
+ "description": "dAPI documentation",
+ "version": "v1.0.0"
+ },
+ "components": {
+ "schemas": {
+ "Integer": {
+ "type": "integer",
+ "x-parameter-modifiers": [
+ {
+ "modifier": "__gt",
+ "in": "suffix",
+ "description": "Greater than",
+ "type": "filter"
+ },
+ {
+ "modifier": "__gte",
+ "in": "suffix",
+ "description": "Greater than or equal to",
+ "type": "filter"
+ },
+ {
+ "modifier": "__lt",
+ "in": "suffix",
+ "description": "Less than",
+ "type": "filter"
+ },
+ {
+ "modifier": "__lte",
+ "in": "suffix",
+ "description": "Less than or equal to",
+ "type": "filter"
+ },
+ {
+ "modifier": "__in",
+ "in": "suffix",
+ "description": "Find a value matching any of these values",
+ "type": "filter"
+ },
+ {
+ "modifier": "__between",
+ "in": "suffix",
+ "description": "Selects values within a given range",
+ "type": "filter"
+ },
+ {
+ "modifier": "!",
+ "in": "prefix",
+ "description": "Negates the operator",
+ "type": "filter"
+ },
+ {
+ "modifier": "__sum",
+ "in": "suffix",
+ "description": "Returns the total sum of a numeric field",
+ "type": "aggregator"
+ },
+ {
+ "modifier": "__avg",
+ "in": "suffix",
+ "description": "Returns the average value of a numeric field",
+ "type": "aggregator"
+ },
+ {
+ "modifier": "__min",
+ "in": "suffix",
+ "description": "Returns the smallest value of a numeric field",
+ "type": "aggregator"
+ },
+ {
+ "modifier": "__max",
+ "in": "suffix",
+ "description": "Returns the largest value of a numeric field",
+ "type": "aggregator"
+ },
+ {
+ "modifier": "__count",
+ "in": "suffix",
+ "description": "Returns the number of rows",
+ "type": "aggregator"
+ }
+ ]
+ },
+ "Float": {
+ "type": "number",
+ "x-parameter-modifiers": [
+ {
+ "modifier": "__gt",
+ "in": "suffix",
+ "description": "Greater than",
+ "type": "filter"
+ },
+ {
+ "modifier": "__gte",
+ "in": "suffix",
+ "description": "Greater than or equal to",
+ "type": "filter"
+ },
+ {
+ "modifier": "__lt",
+ "in": "suffix",
+ "description": "Less than",
+ "type": "filter"
+ },
+ {
+ "modifier": "__lte",
+ "in": "suffix",
+ "description": "Less than or equal to",
+ "type": "filter"
+ },
+ {
+ "modifier": "__in",
+ "in": "suffix",
+ "description": "Find a value matching any of these values",
+ "type": "filter"
+ },
+ {
+ "modifier": "__between",
+ "in": "suffix",
+ "description": "Selects values within a given range",
+ "type": "filter"
+ },
+ {
+ "modifier": "!",
+ "in": "prefix",
+ "description": "Negates the operator",
+ "type": "filter"
+ },
+ {
+ "modifier": "__sum",
+ "in": "suffix",
+ "description": "Returns the total sum of a numeric field",
+ "type": "aggregator"
+ },
+ {
+ "modifier": "__avg",
+ "in": "suffix",
+ "description": "Returns the average value of a numeric field",
+ "type": "aggregator"
+ },
+ {
+ "modifier": "__min",
+ "in": "suffix",
+ "description": "Returns the smallest value of a numeric field",
+ "type": "aggregator"
+ },
+ {
+ "modifier": "__max",
+ "in": "suffix",
+ "description": "Returns the largest value of a numeric field",
+ "type": "aggregator"
+ },
+ {
+ "modifier": "__count",
+ "in": "suffix",
+ "description": "Returns the number of rows",
+ "type": "aggregator"
+ }
+ ]
+ },
+ "String": {
+ "type": "string",
+ "x-parameter-modifiers": [
+ {
+ "modifier": "__contains",
+ "in": "suffix",
+ "description": "Search for string values that contains",
+ "type": "filter"
+ },
+ {
+ "modifier": "__startswith",
+ "in": "suffix",
+ "description": "Search for string values that starts with a given substring",
+ "type": "filter"
+ },
+ {
+ "modifier": "__endswith",
+ "in": "suffix",
+ "description": "Search for string values that ends with a given substring",
+ "type": "filter"
+ },
+ {
+ "modifier": "__re",
+ "in": "suffix",
+ "description": "Search for string values that matches regular expression",
+ "type": "filter"
+ },
+ {
+ "modifier": "__icontains",
+ "in": "suffix",
+ "description": "Search for string values that contains",
+ "type": "filter"
+ },
+ {
+ "modifier": "__istartswith",
+ "in": "suffix",
+ "description": "Search for string values that starts with a given substring",
+ "type": "filter"
+ },
+ {
+ "modifier": "__iendswith",
+ "in": "suffix",
+ "description": "Search for string values that ends with a given substring",
+ "type": "filter"
+ },
+ {
+ "modifier": "__in",
+ "in": "suffix",
+ "description": "Find a value matching any of these values",
+ "type": "filter"
+ },
+ {
+ "modifier": "!",
+ "in": "prefix",
+ "description": "Negates operator",
+ "type": "filter"
+ },
+ {
+ "modifier": "__count",
+ "in": "suffix",
+ "description": "Returns the number of rows",
+ "type": "aggregator"
+ }
+ ]
+ },
+ "DateTime": {
+ "type": "string",
+ "x-parameter-modifiers": [
+ {
+ "modifier": "__contains",
+ "in": "suffix",
+ "description": "Search for string values that contains",
+ "type": "filter"
+ },
+ {
+ "modifier": "__startswith",
+ "in": "suffix",
+ "description": "Search for string values that starts with a given substring",
+ "type": "filter"
+ },
+ {
+ "modifier": "__endswith",
+ "in": "suffix",
+ "description": "Search for string values that ends with a given substring",
+ "type": "filter"
+ },
+ {
+ "modifier": "__re",
+ "in": "suffix",
+ "description": "Search for string values that matches regular expression",
+ "type": "filter"
+ },
+ {
+ "modifier": "__icontains",
+ "in": "suffix",
+ "description": "Search for string values that contains",
+ "type": "filter"
+ },
+ {
+ "modifier": "__istartswith",
+ "in": "suffix",
+ "description": "Search for string values that starts with a given substring",
+ "type": "filter"
+ },
+ {
+ "modifier": "__iendswith",
+ "in": "suffix",
+ "description": "Search for string values that ends with a given substring",
+ "type": "filter"
+ },
+ {
+ "modifier": "__in",
+ "in": "suffix",
+ "description": "Find a value matching any of these values",
+ "type": "filter"
+ },
+ {
+ "modifier": "!",
+ "in": "prefix",
+ "description": "Negates the operator",
+ "type": "filter"
+ },
+ {
+ "modifier": "__count",
+ "in": "suffix",
+ "description": "Returns the number of rows",
+ "type": "aggregator"
+ },
+ {
+ "modifier": "__date",
+ "in": "suffix",
+ "description": "Returns DATE() of the field",
+ "type": "aggregator"
+ },
+ {
+ "modifier": "__year",
+ "in": "suffix",
+ "description": "Returns YEAR() of the field",
+ "type": "aggregator"
+ },
+ {
+ "modifier": "__month",
+ "in": "suffix",
+ "description": "Returns MONTH() of the field",
+ "type": "aggregator"
+ },
+ {
+ "modifier": "__day",
+ "in": "suffix",
+ "description": "Returns DAY() of the field",
+ "type": "aggregator"
+ }
+ ]
+ }
+ },
+ "parameters": {
+ "pathID": {
+ "name": "id",
+ "in": "path",
+ "description": "primary key of the record",
+ "required": true,
+ "schema": {
+ "type": "integer"
+ }
+ },
+ "queryID": {
+ "name": "id",
+ "in": "query",
+ "description": "primary key of the record",
+ "required": false,
+ "schema": {
+ "$rf": "#components/schemas/Integer"
+ }
+ },
+ "model": {
+ "name": "model",
+ "in": "path",
+ "description": "name of a model registered in the system",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ "limit": {
+ "name": "$limit",
+ "in": "query",
+ "description": "Maximum number of records to return",
+ "required": false,
+ "allowReserved": true,
+ "schema": {
+ "type": "integer"
+ }
+ },
+ "offset": {
+ "name": "$offset",
+ "in": "query",
+ "description": "Starting point to read in the list of records",
+ "required": false,
+ "allowReserved": true,
+ "schema": {
+ "type": "integer"
+ }
+ },
+ "order": {
+ "name": "$order",
+ "in": "query",
+ "description": "Sort the results. Use '-' for descending order and comma for more field",
+ "required": false,
+ "allowReserved": true,
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "examples": {
+ "multiColumn": {
+ "summary": "An example multi-column sorting with ascending and descending",
+ "value": "$order=id,-name"
+ }
+ }
+ },
+ "fields": {
+ "name": "$f",
+ "in": "query",
+ "description": "Selecting fields to return in results",
+ "required": false,
+ "allowReserved": true,
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "examples": {
+ "multiColumn": {
+ "summary": "An example multi-column selection",
+ "value": "$f=id,name"
+ },
+ "aggColumn": {
+ "summary": "An example multi-column selection with aggregation function",
+ "value": "$f=id__count,score__sum"
+ },
+ "joinTable": {
+ "summary": "An example of multi-column selection from a different table using a join (see $join)",
+ "value": "$f=username,user_groups.group_name"
+ }
+ }
+ },
+ "groupBy": {
+ "name": "$groupby",
+ "in": "query",
+ "description": "Groups rows that have the same values into summary rows",
+ "required": false,
+ "allowReserved": true,
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "examples": {
+ "simple": {
+ "summary": "An example of grouping results based on category",
+ "value": "$groupby=category_id"
+ },
+ "agg": {
+ "summary": "An example of grouping results based on year and month",
+ "value": "$groupby=date__year,date__month"
+ }
+ }
+ },
+ "deleted": {
+ "name": "$deleted",
+ "in": "query",
+ "description": "Returns results including deleted records",
+ "required": false,
+ "allowReserved": true,
+ "schema": {
+ "type": "boolean"
+ },
+ "examples": {
+ "getDeleted": {
+ "summary": "An example of a query that returns deleted records",
+ "value": "$deleted=1"
+ }
+ }
+ },
+ "join": {
+ "name": "$join",
+ "in": "query",
+ "description": "Joins results from another model based on a foreign key",
+ "required": false,
+ "allowReserved": true,
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "examples": {
+ "getGroupName": {
+ "summary": "An example of a query with a left join from users to user_groups",
+ "value": "$join=user_groups__left__user_group_id"
+ },
+ "getGroupNameInner": {
+ "summary": "An example of a query with a inner join from users to user_groups",
+ "value": "$join=user_groups__user_group_id"
+ }
+ }
+ },
+ "m2m": {
+ "name": "$m2m",
+ "in": "query",
+ "description": "Returns results from M2M fields",
+ "required": false,
+ "allowReserved": true,
+ "schema": {
+ "type": "string",
+ "description": "0=don't get, 1/fill=get full records, id=get ids only"
+ },
+ "examples": {
+ "fillAll": {
+ "summary": "An example of a query that fills all m2m records",
+ "value": "$m2m=fill"
+ },
+ "fillOne": {
+ "summary": "An example of a query that fills IDs from s specific m2m field called cards",
+ "value": "$m2m=cards__id"
+ }
+ }
+ },
+ "q": {
+ "name": "$q",
+ "in": "query",
+ "description": "Searches all string fields marked as Searchable",
+ "required": false,
+ "allowReserved": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ "preload": {
+ "name": "$preload",
+ "in": "query",
+ "description": "Fills the data from foreign keys",
+ "required": false,
+ "allowReserved": true,
+ "schema": {
+ "type": "boolean"
+ },
+ "examples": {
+ "getDeleted": {
+ "summary": "An example of a query that fills foreign key object",
+ "value": "$preload=1"
+ }
+ }
+ },
+ "next": {
+ "name": "$next",
+ "in": "query",
+ "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,
+ "schema": {
+ "type": "string"
+ }
+ },
+ "stat": {
+ "name": "$stat",
+ "in": "query",
+ "description": "Returns the API call execution time in milliseconds",
+ "required": false,
+ "allowReserved": true,
+ "schema": {
+ "type": "boolean"
+ },
+ "examples": {
+ "getDeleted": {
+ "summary": "An example of a query that measures the execution time",
+ "value": "$stat=1"
+ }
+ }
+ },
+ "or": {
+ "name": "$or",
+ "in": "query",
+ "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,
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "examples": {
+ "simple": {
+ "summary": "An example of a query that returns records with active=1 or admin=1",
+ "value": "$or=active=1|admin=1"
+ },
+ "multiValueOr": {
+ "summary": "An example of a query that returns records where the name starts with the letter a or ends with the letter a",
+ "value": "$or=name__startswith=a|name__endswith=a"
+ },
+ "nestedAnd": {
+ "summary": "An example of a query that returns records with admin=1 or (active=1 and username=john)",
+ "value": "$or=admin=1|active=1+username=john"
+ }
+ }
+ }
+ }
+ },
+ "paths": {
+ "/api/d/{model}/read/{id}": {
+ "summary": "read one record",
+ "description": "returns an object for a single user that has the primary key `{id}`",
+ "get": {
+ "operationId": "readOneRecordGet",
+ "responses": {
+ "200": {
+ "description": "Valid response",
+ "content": {
+ "examples": {
+ "standard": {
+ "value":""
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "operationId": "readOneRecordPost",
+ "responses": {
+ "200": {
+ "description": "Valid response",
+ "content": {
+ "examples": {
+ "standard": {
+ "value":""
+ }
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/model"
+ },
+ {
+ "$ref": "#/components/parameters/pathID"
+ },
+ {
+ "$ref": "#/components/parameters/deleted"
+ },
+ {
+ "$ref": "#/components/parameters/m2m"
+ },
+ {
+ "$ref": "#/components/parameters/preload"
+ },
+ {
+ "$ref": "#/components/parameters/stat"
+ }
+ ]
+ },
+ "/api/d/{model}/read": {
+ "summary": "dAPI read multiple records",
+ "description": "returns an array of objects for a multiple records",
+ "get": {
+ "operationId": "readMultiRecordGet",
+ "responses": {
+ "200": {
+ "description": "Valid response",
+ "content": {
+ "examples": {
+ "standard": {
+ "value":""
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "operationId": "readMultiRecordPost",
+ "responses": {
+ "200": {
+ "description": "Valid response",
+ "content": {
+ "examples": {
+ "standard": {
+ "value":""
+ }
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/model"
+ },
+ {
+ "$ref": "#/components/parameters/queryID"
+ },
+ {
+ "$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/next"
+ },
+ {
+ "$ref": "#/components/parameters/stat"
+ },
+ {
+ "$ref": "#/components/parameters/or"
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/register.go b/register.go
index f92a10b1..b5cd1fd2 100644
--- a/register.go
+++ b/register.go
@@ -1,6 +1,8 @@
package uadmin
import (
+ "crypto/sha512"
+ "encoding/base64"
"io/ioutil"
"net/http"
"os"
@@ -17,6 +19,12 @@ type HideInDashboarder interface {
HideInDashboard() bool
}
+// SchemaCategory used to check if a model should be hidden in
+// dashboard
+type SchemaCategory interface {
+ SchemaCategory() string
+}
+
// CustomTranslation is where you can register custom translation files.
// To register a custom translation file, always assign it with it's key
// in the this format "category/name". For example:
@@ -24,7 +32,7 @@ type HideInDashboarder interface {
// uadmin.CustomTranslation = append(uadmin.CustomTranslation, "ui/billing")
//
// This will register the file and you will be able to use it if `uadmin.Tf`.
-// By default there is only one registed custom translation wich is "uadmin/system".
+// By default there is only one registered custom translation which is "uadmin/system".
var CustomTranslation = []string{
"uadmin/system",
}
@@ -77,7 +85,6 @@ func Register(m ...interface{}) {
dashboardMenus := []DashboardMenu{}
All(&dashboardMenus)
var modelExists bool
- cat := ""
Schema = map[string]ModelSchema{}
for i := range modelList {
modelExists = false
@@ -91,6 +98,17 @@ func Register(m ...interface{}) {
hideItem = hider.HideInDashboard()
}
+ // Get Category Name
+ cat := "System"
+ // Check if the model is a system model
+ if i >= SMCount {
+ if category, ok := modelList[i].(SchemaCategory); ok {
+ cat = category.SchemaCategory()
+ } else {
+ cat = ""
+ }
+ }
+
// Register Dashboard menu
// First check if the model is already in dashboard
dashboardIndex := 0
@@ -104,13 +122,6 @@ func Register(m ...interface{}) {
// If not in dashboard, then add it
if !modelExists {
- // Check if the model is a system model
- if i < SMCount {
- cat = "System"
- } else {
- // TODO: Add SetCategory(string) method
- cat = ""
- }
dashboard := DashboardMenu{
MenuName: inflection.Plural(strings.Join(helper.SplitCamelCase(t.Name()), " ")),
URL: name,
@@ -119,11 +130,15 @@ func Register(m ...interface{}) {
}
Save(&dashboard)
} else {
- // If model exists, synchnorize it if changed
+ // If model exists, synchronize it if changed
if hideItem != dashboardMenus[dashboardIndex].Hidden {
dashboardMenus[dashboardIndex].Hidden = hideItem
Save(&dashboardMenus[dashboardIndex])
}
+ if cat != dashboardMenus[dashboardIndex].Cat {
+ dashboardMenus[dashboardIndex].Cat = cat
+ Save(&dashboardMenus[dashboardIndex])
+ }
}
}
@@ -160,6 +175,13 @@ func Register(m ...interface{}) {
JWT = string(buf)
}
}
+ JWTIssuer = func() string {
+ hash := sha512.New()
+ hash.Write([]byte(JWT))
+ buf := hash.Sum(nil)
+ b64 := base64.RawURLEncoding.EncodeToString(buf)
+ return b64[:8]
+ }()
// Check if salt is there or generate it
users := []User{}
diff --git a/schema.go b/schema.go
index 4198d2e5..07641f09 100644
--- a/schema.go
+++ b/schema.go
@@ -15,6 +15,7 @@ type ModelSchema struct {
DisplayName string // Display Name of the model e.g. Order Items
ModelName string // URL e.g. orderitem
TableName string // DB table name e.g. order_items
+ Category string
ModelID uint
Inlines []*ModelSchema
InlinesData []listData
@@ -28,7 +29,7 @@ type ModelSchema struct {
}
// FieldByName returns a field from a ModelSchema by name or nil if
-// it doen't exist
+// it doesn't exist
func (s ModelSchema) FieldByName(a string) *F {
for i := range s.Fields {
if strings.EqualFold(s.Fields[i].Name, a) {
@@ -103,19 +104,6 @@ func (s ModelSchema) MarshalJSON() ([]byte, error) {
})
}
-// ApprovalAction is a selection of approval actions
-type ApprovalAction int
-
-// Approved is an accepted change
-func (ApprovalAction) Approved() ApprovalAction {
- return 1
-}
-
-// Rejected is a rejected change
-func (ApprovalAction) Rejected() ApprovalAction {
- return 2
-}
-
// F is a field
type F struct {
Name string
diff --git a/user.go b/user.go
index 913b7c03..46e08ec9 100644
--- a/user.go
+++ b/user.go
@@ -89,6 +89,7 @@ func (u *User) Login(pass string, otp string) *Session {
s.LastLogin = time.Now()
if u.OTPRequired {
if otp == "" {
+ Trail(INFO, "OTP login for: %s is %s", u.Username, u.GetOTP())
s.PendingOTP = true
} else {
s.PendingOTP = !u.VerifyOTP(otp)
From fe3adceafef2e60b4e7c7b40309b5346ce606a55 Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Tue, 22 Nov 2022 22:58:26 +0400
Subject: [PATCH 014/109] BUG FIX: trail none type was not implemeted for
windows
---
colors/colors_windows.go | 3 +++
1 file changed, 3 insertions(+)
diff --git a/colors/colors_windows.go b/colors/colors_windows.go
index b8f387d4..c8c51088 100644
--- a/colors/colors_windows.go
+++ b/colors/colors_windows.go
@@ -68,6 +68,9 @@ const Warning = "[ " + FGYellowB + "WARNING" + FGNormal + "] "
// Error CLI Display
const Error = "[ " + FGRedB + "ERROR" + FGNormal + " ] "
+// None CLI Display
+const None = ""
+
// Debug CLI Display
const Debug = "[ " + FGCyanB + "DEBUG" + FGNormal + " ] "
From 5e8916e9ac42a5ea83ded476ee6accbc0ee5ed73 Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Wed, 23 Nov 2022 08:23:30 +0400
Subject: [PATCH 015/109] BUG FIX:dAPI nested AND doesn't work in GET method
---
d_api_helper.go | 6 +++++-
d_api_logout.go | 13 ++++++++++++-
d_api_read.go | 2 ++
3 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/d_api_helper.go b/d_api_helper.go
index c54ad972..62ce8239 100644
--- a/d_api_helper.go
+++ b/d_api_helper.go
@@ -29,7 +29,11 @@ func getURLArgs(r *http.Request) map[string]string {
continue
}
- val, _ := url.QueryUnescape(paramParts[1])
+ // unescape URL except for $or because it uses + for nested AND
+ val := paramParts[1]
+ if paramParts[0] != "$or" {
+ val, _ = url.QueryUnescape(paramParts[1])
+ }
if _, ok := params[paramParts[0]]; ok {
params[paramParts[0]] += "," + val
} else {
diff --git a/d_api_logout.go b/d_api_logout.go
index 37b313ec..671206fe 100644
--- a/d_api_logout.go
+++ b/d_api_logout.go
@@ -3,5 +3,16 @@ package uadmin
import "net/http"
func dAPILogoutHandler(w http.ResponseWriter, r *http.Request, s *Session) {
-
+ if s == nil {
+ w.WriteHeader(http.StatusUnauthorized)
+ ReturnJSON(w, r, map[string]interface{}{
+ "status": "error",
+ "err_msg": "Already logged out",
+ })
+ return
+ }
+ Logout(r)
+ ReturnJSON(w, r, map[string]interface{}{
+ "status": "ok",
+ })
}
diff --git a/d_api_read.go b/d_api_read.go
index 0f231af3..f7dc1de6 100644
--- a/d_api_read.go
+++ b/d_api_read.go
@@ -114,6 +114,8 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) {
SQL += " OFFSET " + offset
}
+ Trail(DEBUG, SQL)
+ Trail(DEBUG, "%#v", args)
if DebugDB {
Trail(DEBUG, SQL)
Trail(DEBUG, "%#v", args)
From c67ec7b1b73acda82bd450f160d3728b044fd0c2 Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Wed, 23 Nov 2022 08:49:51 +0400
Subject: [PATCH 016/109] BUG FIX: dAPI edit row count in edit one was always
returning 0
---
d_api_edit.go | 3 ++-
d_api_read.go | 2 --
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/d_api_edit.go b/d_api_edit.go
index 7c1206d9..ada2444a 100644
--- a/d_api_edit.go
+++ b/d_api_edit.go
@@ -167,6 +167,7 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) {
})
return
}
+ rowsAffected := db.RowsAffected
// Process M2M
db = GetDB().Begin()
@@ -200,7 +201,7 @@ func dAPIEditHandler(w http.ResponseWriter, r *http.Request, s *Session) {
returnDAPIJSON(w, r, map[string]interface{}{
"status": "ok",
- "rows_count": db.RowsAffected,
+ "rows_count": rowsAffected,
}, params, "edit", model.Interface())
} else {
// Error: Unknown format
diff --git a/d_api_read.go b/d_api_read.go
index f7dc1de6..0f231af3 100644
--- a/d_api_read.go
+++ b/d_api_read.go
@@ -114,8 +114,6 @@ func dAPIReadHandler(w http.ResponseWriter, r *http.Request, s *Session) {
SQL += " OFFSET " + offset
}
- Trail(DEBUG, SQL)
- Trail(DEBUG, "%#v", args)
if DebugDB {
Trail(DEBUG, SQL)
Trail(DEBUG, "%#v", args)
From a2e8833064e0bc9f70ee0da0f2bc1563e5bf32d3 Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Wed, 23 Nov 2022 14:21:16 +0400
Subject: [PATCH 017/109] release v0.9.1
---
admin.go | 5 +-
d_api_auth.go | 8 ++
global.go | 5 +-
openapi.go | 211 +++++++++++++++++--------------------
openapi/generate_schema.go | 2 +-
openapi/schema_object.go | 2 +
6 files changed, 118 insertions(+), 115 deletions(-)
diff --git a/admin.go b/admin.go
index f03ee98e..8456fbf9 100644
--- a/admin.go
+++ b/admin.go
@@ -166,6 +166,10 @@ func JSONMarshal(v interface{}, safeEncoding bool) ([]byte, error) {
// 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, "", " ")
if err != nil {
response := map[string]interface{}{
@@ -173,7 +177,6 @@ func ReturnJSON(w http.ResponseWriter, r *http.Request, v interface{}) {
"error_msg": fmt.Sprintf("unable to encode JSON. %s", err),
}
b, _ = json.MarshalIndent(response, "", " ")
- w.Header().Set("Content-Type", "application/json")
w.Write(b)
return
}
diff --git a/d_api_auth.go b/d_api_auth.go
index cb79ded9..16ebbdd0 100644
--- a/d_api_auth.go
+++ b/d_api_auth.go
@@ -6,6 +6,14 @@ import (
)
func dAPIAuthHandler(w http.ResponseWriter, r *http.Request, s *Session) {
+ if DisableDAPIAuth {
+ w.WriteHeader(http.StatusForbidden)
+ ReturnJSON(w, r, map[string]interface{}{
+ "status": "error",
+ "err_msg": "dAPI auth is disabled",
+ })
+
+ }
// Trim leading path
r.URL.Path = strings.TrimPrefix(r.URL.Path, "auth")
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/")
diff --git a/global.go b/global.go
index c6292a20..c73bd3e3 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.0"
+const Version = "0.9.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.
@@ -373,6 +373,9 @@ var TimeZone = "local"
// TrailCacheSize is the number of bytes to keep in memory of trail logs
var TrailCacheSize = 65536
+// DisableDAPIAuth enables or disables access to dAPI auth API
+var DisableDAPIAuth = true
+
// AllowDAPISignup allows unauthenticated users to sign up
var AllowDAPISignup = false
diff --git a/openapi.go b/openapi.go
index a4a1ac62..e9ae80f6 100644
--- a/openapi.go
+++ b/openapi.go
@@ -9,10 +9,18 @@ import (
"github.com/uadmin/uadmin/openapi"
)
+// CustomOpenAPI is a handler to be called to customize OpenAPI schema
+// Use of OpenAPI schema generation is under development and should not be used in production
var CustomOpenAPI func(*openapi.Schema)
+
+// CustomOpenAPIJSON is a handler to be called to customize OpenAPI schema JSON output
+// Use of OpenAPI schema generation is under development and should not be used in production
var CustomOpenAPIJSON func([]byte) []byte
+// GenerateOpenAPISchema generates API schema for dAPI that is compatible with OpenAPI 3.1.0
+// Use of OpenAPI schema generation is under development and should not be used in production
func GenerateOpenAPISchema() {
+ Trail(WARNING, "Use of OpenAPI schema generation is under development and should not be used in production")
s := openapi.GenerateBaseSchema()
// Customize schema
@@ -63,8 +71,7 @@ func GenerateOpenAPISchema() {
}
case cFK:
return &openapi.SchemaObject{
- Type: "object",
- Items: &openapi.SchemaObject{Ref: "#/components/schemas/" + v.Fields[i].TypeName},
+ Ref: "#/components/schemas/" + v.Fields[i].TypeName,
}
case cHTML:
return &openapi.SchemaObject{
@@ -76,11 +83,15 @@ func GenerateOpenAPISchema() {
}
case cLIST:
return &openapi.SchemaObject{
- Type: "string",
- Enum: func() []interface{} {
- vals := make([]interface{}, len(v.Fields[i].Choices))
+ Type: "integer",
+ OneOf: func() []*openapi.SchemaObject {
+ vals := make([]*openapi.SchemaObject, len(v.Fields[i].Choices))
for j := range v.Fields[i].Choices {
- vals[j] = v.Fields[i].Choices[j].V
+ vals[j] = &openapi.SchemaObject{
+ Type: "integer",
+ Title: v.Fields[i].Choices[j].V,
+ Const: v.Fields[i].Choices[j].K,
+ }
}
return vals
}(),
@@ -128,13 +139,21 @@ func GenerateOpenAPISchema() {
Type: "integer",
}
}
+ default:
+ return &openapi.SchemaObject{
+ Type: "string",
+ }
}
- // If unknown, use string
- return &openapi.SchemaObject{
- Type: "string",
- }
+
}()
+ // If the field is a foreign key, then add the ID field for it
+ if v.Fields[i].Type == cFK {
+ fields[v.Fields[i].Name+"ID"] = &openapi.SchemaObject{
+ Type: "integer",
+ }
+ }
+
// Set other schema properties
fields[v.Fields[i].Name].Description = v.Fields[i].Help
fields[v.Fields[i].Name].Default = v.Fields[i].DefaultValue
@@ -170,25 +189,48 @@ func GenerateOpenAPISchema() {
Schema: func() *openapi.SchemaObject {
switch v.Fields[i].Type {
case cSTRING:
+ fallthrough
case cCODE:
+ fallthrough
case cEMAIL:
+ fallthrough
case cFILE:
+ fallthrough
case cIMAGE:
+ fallthrough
case cHTML:
+ fallthrough
case cLINK:
+ fallthrough
case cMULTILINGUAL:
+ fallthrough
case cPASSWORD:
return &openapi.SchemaObject{
Ref: "#/components/schemas/String",
}
- case cID:
case cFK:
- case cLIST:
- case cMONEY:
return &openapi.SchemaObject{
Ref: "#/components/schemas/Integer",
}
+ case cLIST:
+ return &openapi.SchemaObject{
+ Type: "integer",
+ OneOf: func() []*openapi.SchemaObject {
+ vals := make([]*openapi.SchemaObject, len(v.Fields[i].Choices))
+ for j := range v.Fields[i].Choices {
+ vals[j] = &openapi.SchemaObject{
+ Type: "integer",
+ Title: v.Fields[i].Choices[j].V,
+ Const: v.Fields[i].Choices[j].K,
+ }
+ }
+ return vals
+ }(),
+ }
+ case cMONEY:
+ fallthrough
case cNUMBER:
+ fallthrough
case cPROGRESSBAR:
return &openapi.SchemaObject{
Ref: "#/components/schemas/Number",
@@ -201,9 +243,8 @@ func GenerateOpenAPISchema() {
return &openapi.SchemaObject{
Ref: "#/components/schemas/DateTime",
}
- }
- return &openapi.SchemaObject{
- Ref: "#/components/schemas/String",
+ default:
+ return &openapi.SchemaObject{Ref: "#/components/schemas/String"}
}
}(),
}
@@ -228,40 +269,51 @@ func GenerateOpenAPISchema() {
Schema: func() *openapi.SchemaObject {
switch v.Fields[i].Type {
case cSTRING:
+ fallthrough
case cCODE:
+ fallthrough
case cEMAIL:
+ fallthrough
case cFILE:
+ fallthrough
case cIMAGE:
+ fallthrough
case cHTML:
+ fallthrough
case cLINK:
+ fallthrough
case cMULTILINGUAL:
+ fallthrough
case cPASSWORD:
return &openapi.SchemaObject{
- Ref: "#/components/schemas/String",
+ Type: "string",
}
- case cID:
case cFK:
+ fallthrough
case cLIST:
+ fallthrough
case cMONEY:
return &openapi.SchemaObject{
- Ref: "#/components/schemas/Integer",
+ Type: "integer",
}
case cNUMBER:
+ fallthrough
case cPROGRESSBAR:
return &openapi.SchemaObject{
- Ref: "#/components/schemas/Number",
+ Type: "number",
}
case cBOOL:
return &openapi.SchemaObject{
- Ref: "#/components/schemas/Boolean",
+ Type: "boolean",
}
case cDATE:
return &openapi.SchemaObject{
- Ref: "#/components/schemas/DateTime",
+ Type: "string",
+ }
+ default:
+ return &openapi.SchemaObject{
+ Type: "string",
}
- }
- return &openapi.SchemaObject{
- Ref: "#/components/schemas/String",
}
}(),
}
@@ -293,28 +345,11 @@ func GenerateOpenAPISchema() {
Content: map[string]openapi.MediaType{
"application/json": {
Schema: &openapi.SchemaObject{
- Ref: "#/components/schemas/" + v.Name,
- },
- },
- },
- },
- },
- },
- Post: &openapi.Operation{
- Tags: []string{v.Name, func() string {
- if v.Category != "" {
- return v.Category
- } else {
- return "Other"
- }
- }()},
- Responses: map[string]openapi.Response{
- "200": {
- Description: v.DisplayName + " record",
- Content: map[string]openapi.MediaType{
- "application/json": {
- Schema: &openapi.SchemaObject{
- Ref: "#/components/schemas/" + v.Name,
+ Type: "object",
+ Properties: map[string]*openapi.SchemaObject{
+ "result": {Ref: "#/components/schemas/" + v.Name},
+ "status": {Type: "string"},
+ },
},
},
},
@@ -357,28 +392,14 @@ func GenerateOpenAPISchema() {
Content: map[string]openapi.MediaType{
"application/json": {
Schema: &openapi.SchemaObject{
- Ref: "#/components/schemas/" + v.Name,
- },
- },
- },
- },
- },
- },
- Post: &openapi.Operation{
- Tags: []string{v.Name, func() string {
- if v.Category != "" {
- return v.Category
- } else {
- return "Other"
- }
- }()},
- Responses: map[string]openapi.Response{
- "200": {
- Description: v.DisplayName + " record",
- Content: map[string]openapi.MediaType{
- "application/json": {
- Schema: &openapi.SchemaObject{
- Ref: "#/components/schemas/" + v.Name,
+ Type: "object",
+ Properties: map[string]*openapi.SchemaObject{
+ "result": {
+ Type: "array",
+ Items: &openapi.SchemaObject{Ref: "#/components/schemas/" + v.Name},
+ },
+ "status": {Type: "string"},
+ },
},
},
},
@@ -425,27 +446,6 @@ func GenerateOpenAPISchema() {
s.Paths[fmt.Sprintf("/api/d/%s/add", v.ModelName)] = openapi.Path{
Summary: "Add one " + v.DisplayName,
Description: "Add one " + v.DisplayName,
- Get: &openapi.Operation{
- Tags: []string{v.Name, func() string {
- if v.Category != "" {
- return v.Category
- } else {
- return "Other"
- }
- }()},
- Responses: map[string]openapi.Response{
- "200": {
- Description: v.DisplayName + " record",
- Content: map[string]openapi.MediaType{
- "application/json": {
- Schema: &openapi.SchemaObject{
- Ref: "#/components/schemas/" + v.Name,
- },
- },
- },
- },
- },
- },
Post: &openapi.Operation{
Tags: []string{v.Name, func() string {
if v.Category != "" {
@@ -460,7 +460,15 @@ func GenerateOpenAPISchema() {
Content: map[string]openapi.MediaType{
"application/json": {
Schema: &openapi.SchemaObject{
- Ref: "#/components/schemas/" + v.Name,
+ Type: "object",
+ Properties: map[string]*openapi.SchemaObject{
+ "id": {
+ Type: "array",
+ Items: &openapi.SchemaObject{Type: "integer"},
+ },
+ "rows_count": {Type: "integer"},
+ "status": {Type: "string"},
+ },
},
},
},
@@ -480,27 +488,6 @@ func GenerateOpenAPISchema() {
s.Paths[fmt.Sprintf("/api/d/%s/add", v.ModelName)] = openapi.Path{
Summary: "Add one " + v.DisplayName,
Description: "Add one " + v.DisplayName,
- Get: &openapi.Operation{
- Tags: []string{v.Name, func() string {
- if v.Category != "" {
- return v.Category
- } else {
- return "Other"
- }
- }()},
- Responses: map[string]openapi.Response{
- "200": {
- Description: v.DisplayName + " record",
- Content: map[string]openapi.MediaType{
- "application/json": {
- Schema: &openapi.SchemaObject{
- Ref: "#/components/schemas/" + v.Name,
- },
- },
- },
- },
- },
- },
Post: &openapi.Operation{
Tags: []string{v.Name, func() string {
if v.Category != "" {
diff --git a/openapi/generate_schema.go b/openapi/generate_schema.go
index abe8be48..65e3e80c 100644
--- a/openapi/generate_schema.go
+++ b/openapi/generate_schema.go
@@ -2,7 +2,7 @@ package openapi
func GenerateBaseSchema() *Schema {
s := &Schema{
- OpenAPI: "3.0.0",
+ OpenAPI: "3.1.0",
Info: &SchemaInfo{
Title: " API documentation",
Description: "API documentation",
diff --git a/openapi/schema_object.go b/openapi/schema_object.go
index aac02a07..acc61dfd 100644
--- a/openapi/schema_object.go
+++ b/openapi/schema_object.go
@@ -19,6 +19,8 @@ type SchemaObject struct {
ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"`
Example *Example `json:"example,omitempty"`
Enum []interface{} `json:"enum,omitempty"`
+ OneOf []*SchemaObject `json:"oneOf,omitempty"`
+ Const interface{} `json:"const,omitempty"`
XFilters []XModifier `json:"x-filter,omitempty"`
XAggregator []XModifier `json:"x-aggregator,omitempty"`
}
From 73f7fe63533972add0f52e250664cd6478e87ee2 Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Fri, 25 Nov 2022 05:25:39 +0400
Subject: [PATCH 018/109] 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 019/109] 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 020/109] 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 021/109] 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 022/109] 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 023/109] 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 024/109] 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 025/109] 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 026/109] 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 027/109] 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 028/109] 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 029/109] 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 030/109] 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 031/109] 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 032/109] 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 033/109] 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 034/109] 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 035/109] 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 036/109] 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 037/109] 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 038/109] 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 039/109] 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 040/109] 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 041/109] 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 042/109] 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 043/109] 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 044/109] 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 045/109] 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 046/109] 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 047/109] 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 048/109] 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 049/109] 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 050/109] 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 051/109] 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 052/109] 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 053/109] 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 054/109] 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 055/109] 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 056/109] 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 057/109] 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 058/109] 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 059/109] 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 060/109] 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 061/109] 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 062/109] 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 063/109] 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 064/109] 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 065/109] 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 066/109] 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 067/109] 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 068/109] 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 069/109] 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 070/109] 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 071/109] 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 072/109] 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 073/109] 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 074/109] 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 075/109] 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 076/109] 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 077/109] 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 078/109] 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 079/109] 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 080/109] 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 081/109] 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 082/109] 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 083/109] 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 084/109] 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 085/109] 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 086/109] 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 087/109] 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 088/109] 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 089/109] 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 090/109] 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 091/109] 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 092/109] 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 093/109] 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 094/109] 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 095/109] 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 096/109] 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 097/109] 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 098/109] 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 099/109] 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 100/109] 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 101/109] 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 102/109] 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 103/109] 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 104/109] 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 105/109] 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 @@