Commit 63b2ae620176e76f604e31887ed0819b0b29e4da
1 parent
087f8fb21c
Exists in
master
and in
1 other branch
renamed files
Showing
10 changed files
with
866 additions
and
866 deletions
Show diff stats
auth.go
File was created | 1 | package webutility | |
2 | |||
3 | import ( | ||
4 | "crypto/rand" | ||
5 | "crypto/sha256" | ||
6 | "encoding/hex" | ||
7 | "errors" | ||
8 | "net/http" | ||
9 | "strings" | ||
10 | "time" | ||
11 | |||
12 | "github.com/dgrijalva/jwt-go" | ||
13 | ) | ||
14 | |||
15 | const OneDay = time.Hour * 24 | ||
16 | const OneWeek = OneDay * 7 | ||
17 | const saltSize = 32 | ||
18 | |||
19 | var appName = "webutility" | ||
20 | var secret = "webutility" | ||
21 | |||
22 | type Role struct { | ||
23 | Name string `json:"name"` | ||
24 | ID int64 `json:"id"` | ||
25 | } | ||
26 | |||
27 | // TokenClaims are JWT token claims. | ||
28 | type TokenClaims struct { | ||
29 | // extending a struct | ||
30 | jwt.StandardClaims | ||
31 | |||
32 | // custom claims | ||
33 | Token string `json:"access_token"` | ||
34 | TokenType string `json:"token_type"` | ||
35 | Username string `json:"username"` | ||
36 | Role string `json:"role"` | ||
37 | RoleID int64 `json:"role_id"` | ||
38 | ExpiresIn int64 `json:"expires_in"` | ||
39 | } | ||
40 | |||
41 | func InitJWT(appName, secret string) { | ||
42 | appName = appName | ||
43 | secret = secret | ||
44 | } | ||
45 | |||
46 | // ValidateCredentials hashes pass and salt and returns comparison result with resultHash | ||
47 | func ValidateCredentials(pass, salt, resultHash string) (bool, error) { | ||
48 | hash, _, err := CreateHash(pass, salt) | ||
49 | if err != nil { | ||
50 | return false, err | ||
51 | } | ||
52 | res := hash == resultHash | ||
53 | return res, nil | ||
54 | } | ||
55 | |||
56 | // CreateHash hashes str using SHA256. | ||
57 | // If the presalt parameter is not provided CreateHash will generate new salt string. | ||
58 | // Returns hash and salt strings or an error if it fails. | ||
59 | func CreateHash(str, presalt string) (hash, salt string, err error) { | ||
60 | // chech if message is presalted | ||
61 | if presalt == "" { | ||
62 | salt, err = randomSalt() | ||
63 | if err != nil { | ||
64 | return "", "", err | ||
65 | } | ||
66 | } else { | ||
67 | salt = presalt | ||
68 | } | ||
69 | |||
70 | // convert strings to raw byte slices | ||
71 | rawstr := []byte(str) | ||
72 | rawsalt, err := hex.DecodeString(salt) | ||
73 | if err != nil { | ||
74 | return "", "", err | ||
75 | } | ||
76 | |||
77 | rawdata := make([]byte, len(rawstr)+len(rawsalt)) | ||
78 | rawdata = append(rawdata, rawstr...) | ||
79 | rawdata = append(rawdata, rawsalt...) | ||
80 | |||
81 | // hash message + salt | ||
82 | hasher := sha256.New() | ||
83 | hasher.Write(rawdata) | ||
84 | rawhash := hasher.Sum(nil) | ||
85 | |||
86 | hash = hex.EncodeToString(rawhash) | ||
87 | return hash, salt, nil | ||
88 | } | ||
89 | |||
90 | // CreateAuthToken returns JWT token with encoded username, role, expiration date and issuer claims. | ||
91 | // It returns an error if it fails. | ||
92 | func CreateAuthToken(username string, role Role) (TokenClaims, error) { | ||
93 | t0 := (time.Now()).Unix() | ||
94 | t1 := (time.Now().Add(OneWeek)).Unix() | ||
95 | claims := TokenClaims{ | ||
96 | TokenType: "Bearer", | ||
97 | Username: username, | ||
98 | Role: role.Name, | ||
99 | RoleID: role.ID, | ||
100 | ExpiresIn: t1 - t0, | ||
101 | } | ||
102 | // initialize jwt.StandardClaims fields (anonymous struct) | ||
103 | claims.IssuedAt = t0 | ||
104 | claims.ExpiresAt = t1 | ||
105 | claims.Issuer = appName | ||
106 | |||
107 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | ||
108 | token, err := jwtToken.SignedString([]byte(secret)) | ||
109 | if err != nil { | ||
110 | return TokenClaims{}, err | ||
111 | } | ||
112 | claims.Token = token | ||
113 | return claims, nil | ||
114 | } | ||
115 | |||
116 | // RefreshAuthToken returns new JWT token with sprolongs JWT token's expiration date for one week. | ||
117 | // It returns new JWT token or an error if it fails. | ||
118 | func RefreshAuthToken(tok string) (TokenClaims, error) { | ||
119 | token, err := jwt.ParseWithClaims(tok, &TokenClaims{}, secretFunc) | ||
120 | if err != nil { | ||
121 | if validation, ok := err.(*jwt.ValidationError); ok { | ||
122 | // don't return error if token is expired | ||
123 | // just extend it | ||
124 | if !(validation.Errors&jwt.ValidationErrorExpired != 0) { | ||
125 | return TokenClaims{}, err | ||
126 | } | ||
127 | } else { | ||
128 | return TokenClaims{}, err | ||
129 | } | ||
130 | } | ||
131 | |||
132 | // type assertion | ||
133 | claims, ok := token.Claims.(*TokenClaims) | ||
134 | if !ok { | ||
135 | return TokenClaims{}, errors.New("token is not valid") | ||
136 | } | ||
137 | |||
138 | // extend token expiration date | ||
139 | return CreateAuthToken(claims.Username, Role{claims.Role, claims.RoleID}) | ||
140 | } | ||
141 | |||
142 | // RbacCheck returns true if user that made HTTP request is authorized to | ||
143 | // access the resource it is targeting. | ||
144 | // It exctracts user's role from the JWT token located in Authorization header of | ||
145 | // http.Request and then compares it with the list of supplied roles and returns | ||
146 | // true if there's a match, if "*" is provided or if the authRoles is nil. | ||
147 | // Otherwise it returns false. | ||
148 | func RbacCheck(req *http.Request, authRoles []string) bool { | ||
149 | if authRoles == nil { | ||
150 | return true | ||
151 | } | ||
152 | |||
153 | // validate token and check expiration date | ||
154 | claims, err := GetTokenClaims(req) | ||
155 | if err != nil { | ||
156 | return false | ||
157 | } | ||
158 | // check if token has expired | ||
159 | if claims.ExpiresAt < (time.Now()).Unix() { | ||
160 | return false | ||
161 | } | ||
162 | |||
163 | // check if role extracted from token matches | ||
164 | // any of the provided (allowed) ones | ||
165 | for _, r := range authRoles { | ||
166 | if claims.Role == r || r == "*" { | ||
167 | return true | ||
168 | } | ||
169 | } | ||
170 | |||
171 | return false | ||
172 | } | ||
173 | |||
174 | // AuthCheck returns token claims and boolean value based on user's rights to access resource specified in req. | ||
175 | // It exctracts user's role from the JWT token located in Authorization header of | ||
176 | // HTTP request and then compares it with the list of supplied (authorized); | ||
177 | // it returns true if there's a match, if "*" is provided or if the authRoles is nil. | ||
178 | func AuthCheck(req *http.Request, authRoles []string) (*TokenClaims, bool) { | ||
179 | if authRoles == nil { | ||
180 | return nil, true | ||
181 | } | ||
182 | |||
183 | // validate token and check expiration date | ||
184 | claims, err := GetTokenClaims(req) | ||
185 | if err != nil { | ||
186 | return claims, false | ||
187 | } | ||
188 | // check if token has expired | ||
189 | if claims.ExpiresAt < (time.Now()).Unix() { | ||
190 | return claims, false | ||
191 | } | ||
192 | |||
193 | // check if role extracted from token matches | ||
194 | // any of the provided (allowed) ones | ||
195 | for _, r := range authRoles { | ||
196 | if claims.Role == r || r == "*" { | ||
197 | return claims, true | ||
198 | } | ||
199 | } | ||
200 | |||
201 | return claims, false | ||
202 | } | ||
203 | |||
204 | // GetTokenClaims extracts JWT claims from Authorization header of the request. | ||
205 | // Returns token claims or an error. | ||
206 | func GetTokenClaims(req *http.Request) (*TokenClaims, error) { | ||
207 | // check for and strip 'Bearer' prefix | ||
208 | var tokstr string | ||
209 | authHead := req.Header.Get("Authorization") | ||
210 | if ok := strings.HasPrefix(authHead, "Bearer "); ok { | ||
211 | tokstr = strings.TrimPrefix(authHead, "Bearer ") | ||
212 | } else { | ||
213 | return &TokenClaims{}, errors.New("authorization header in incomplete") | ||
214 | } | ||
215 | |||
216 | token, err := jwt.ParseWithClaims(tokstr, &TokenClaims{}, secretFunc) | ||
217 | if err != nil { | ||
218 | return &TokenClaims{}, err | ||
219 | } | ||
220 | |||
221 | // type assertion | ||
222 | claims, ok := token.Claims.(*TokenClaims) | ||
223 | if !ok || !token.Valid { | ||
224 | return &TokenClaims{}, errors.New("token is not valid") | ||
225 | } | ||
226 | |||
227 | return claims, nil | ||
228 | } | ||
229 | |||
230 | // randomSalt returns a string of random characters of 'saltSize' length. | ||
231 | func randomSalt() (s string, err error) { | ||
232 | rawsalt := make([]byte, saltSize) | ||
233 | |||
234 | _, err = rand.Read(rawsalt) | ||
235 | if err != nil { | ||
236 | return "", err | ||
237 | } | ||
238 | |||
239 | s = hex.EncodeToString(rawsalt) | ||
240 | return s, nil | ||
241 | } | ||
242 | |||
243 | // secretFunc returns byte slice of API secret keyword. | ||
244 | func secretFunc(token *jwt.Token) (interface{}, error) { | ||
245 | return []byte(secret), nil | ||
246 | } | ||
247 |
auth_utility.go
1 | package webutility | File was deleted | |
2 | |||
3 | import ( | ||
4 | "crypto/rand" | ||
5 | "crypto/sha256" | ||
6 | "encoding/hex" | ||
7 | "errors" | ||
8 | "net/http" | ||
9 | "strings" | ||
10 | "time" | ||
11 | |||
12 | "github.com/dgrijalva/jwt-go" | ||
13 | ) | ||
14 | |||
15 | const OneDay = time.Hour * 24 | ||
16 | const OneWeek = OneDay * 7 | ||
17 | const saltSize = 32 | ||
18 | |||
19 | var appName = "webutility" | ||
20 | var secret = "webutility" | ||
21 | |||
22 | type Role struct { | ||
23 | Name string `json:"name"` | ||
24 | ID int64 `json:"id"` | ||
25 | } | ||
26 | |||
27 | // TokenClaims are JWT token claims. | ||
28 | type TokenClaims struct { | ||
29 | // extending a struct | ||
30 | jwt.StandardClaims | ||
31 | |||
32 | // custom claims | ||
33 | Token string `json:"access_token"` | ||
34 | TokenType string `json:"token_type"` | ||
35 | Username string `json:"username"` | ||
36 | Role string `json:"role"` | ||
37 | RoleID int64 `json:"role_id"` | ||
38 | ExpiresIn int64 `json:"expires_in"` | ||
39 | } | ||
40 | |||
41 | func InitJWT(appName, secret string) { | ||
42 | appName = appName | ||
43 | secret = secret | ||
44 | } | ||
45 | |||
46 | // ValidateCredentials hashes pass and salt and returns comparison result with resultHash | ||
47 | func ValidateCredentials(pass, salt, resultHash string) (bool, error) { | ||
48 | hash, _, err := CreateHash(pass, salt) | ||
49 | if err != nil { | ||
50 | return false, err | ||
51 | } | ||
52 | res := hash == resultHash | ||
53 | return res, nil | ||
54 | } | ||
55 | |||
56 | // CreateHash hashes str using SHA256. | ||
57 | // If the presalt parameter is not provided CreateHash will generate new salt string. | ||
58 | // Returns hash and salt strings or an error if it fails. | ||
59 | func CreateHash(str, presalt string) (hash, salt string, err error) { | ||
60 | // chech if message is presalted | ||
61 | if presalt == "" { | ||
62 | salt, err = randomSalt() | ||
63 | if err != nil { | ||
64 | return "", "", err | ||
65 | } | ||
66 | } else { | ||
67 | salt = presalt | ||
68 | } | ||
69 | |||
70 | // convert strings to raw byte slices | ||
71 | rawstr := []byte(str) | ||
72 | rawsalt, err := hex.DecodeString(salt) | ||
73 | if err != nil { | ||
74 | return "", "", err | ||
75 | } | ||
76 | |||
77 | rawdata := make([]byte, len(rawstr)+len(rawsalt)) | ||
78 | rawdata = append(rawdata, rawstr...) | ||
79 | rawdata = append(rawdata, rawsalt...) | ||
80 | |||
81 | // hash message + salt | ||
82 | hasher := sha256.New() | ||
83 | hasher.Write(rawdata) | ||
84 | rawhash := hasher.Sum(nil) | ||
85 | |||
86 | hash = hex.EncodeToString(rawhash) | ||
87 | return hash, salt, nil | ||
88 | } | ||
89 | |||
90 | // CreateAuthToken returns JWT token with encoded username, role, expiration date and issuer claims. | ||
91 | // It returns an error if it fails. | ||
92 | func CreateAuthToken(username string, role Role) (TokenClaims, error) { | ||
93 | t0 := (time.Now()).Unix() | ||
94 | t1 := (time.Now().Add(OneWeek)).Unix() | ||
95 | claims := TokenClaims{ | ||
96 | TokenType: "Bearer", | ||
97 | Username: username, | ||
98 | Role: role.Name, | ||
99 | RoleID: role.ID, | ||
100 | ExpiresIn: t1 - t0, | ||
101 | } | ||
102 | // initialize jwt.StandardClaims fields (anonymous struct) | ||
103 | claims.IssuedAt = t0 | ||
104 | claims.ExpiresAt = t1 | ||
105 | claims.Issuer = appName | ||
106 | |||
107 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | ||
108 | token, err := jwtToken.SignedString([]byte(secret)) | ||
109 | if err != nil { | ||
110 | return TokenClaims{}, err | ||
111 | } | ||
112 | claims.Token = token | ||
113 | return claims, nil | ||
114 | } | ||
115 | |||
116 | // RefreshAuthToken returns new JWT token with sprolongs JWT token's expiration date for one week. | ||
117 | // It returns new JWT token or an error if it fails. | ||
118 | func RefreshAuthToken(tok string) (TokenClaims, error) { | ||
119 | token, err := jwt.ParseWithClaims(tok, &TokenClaims{}, secretFunc) | ||
120 | if err != nil { | ||
121 | if validation, ok := err.(*jwt.ValidationError); ok { | ||
122 | // don't return error if token is expired | ||
123 | // just extend it | ||
124 | if !(validation.Errors&jwt.ValidationErrorExpired != 0) { | ||
125 | return TokenClaims{}, err | ||
126 | } | ||
127 | } else { | ||
128 | return TokenClaims{}, err | ||
129 | } | ||
130 | } | ||
131 | |||
132 | // type assertion | ||
133 | claims, ok := token.Claims.(*TokenClaims) | ||
134 | if !ok { | ||
135 | return TokenClaims{}, errors.New("token is not valid") | ||
136 | } | ||
137 | |||
138 | // extend token expiration date | ||
139 | return CreateAuthToken(claims.Username, Role{claims.Role, claims.RoleID}) | ||
140 | } | ||
141 | |||
142 | // RbacCheck returns true if user that made HTTP request is authorized to | ||
143 | // access the resource it is targeting. | ||
144 | // It exctracts user's role from the JWT token located in Authorization header of | ||
145 | // http.Request and then compares it with the list of supplied roles and returns | ||
146 | // true if there's a match, if "*" is provided or if the authRoles is nil. | ||
147 | // Otherwise it returns false. | ||
148 | func RbacCheck(req *http.Request, authRoles []string) bool { | ||
149 | if authRoles == nil { | ||
150 | return true | ||
151 | } | ||
152 | |||
153 | // validate token and check expiration date | ||
154 | claims, err := GetTokenClaims(req) | ||
155 | if err != nil { | ||
156 | return false | ||
157 | } | ||
158 | // check if token has expired | ||
159 | if claims.ExpiresAt < (time.Now()).Unix() { | ||
160 | return false | ||
161 | } | ||
162 | |||
163 | // check if role extracted from token matches | ||
164 | // any of the provided (allowed) ones | ||
165 | for _, r := range authRoles { | ||
166 | if claims.Role == r || r == "*" { | ||
167 | return true | ||
168 | } | ||
169 | } | ||
170 | |||
171 | return false | ||
172 | } | ||
173 | |||
174 | // ProcessRBAC returns token claims and boolean value based on user's rights to access resource specified in req. | ||
175 | // It exctracts user's role from the JWT token located in Authorization header of | ||
176 | // HTTP request and then compares it with the list of supplied (authorized); | ||
177 | // it returns true if there's a match, if "*" is provided or if the authRoles is nil. | ||
178 | func ProcessRBAC(req *http.Request, authRoles []string) (*TokenClaims, bool) { | ||
179 | if authRoles == nil { | ||
180 | return nil, true | ||
181 | } | ||
182 | |||
183 | // validate token and check expiration date | ||
184 | claims, err := GetTokenClaims(req) | ||
185 | if err != nil { | ||
186 | return claims, false | ||
187 | } | ||
188 | // check if token has expired | ||
189 | if claims.ExpiresAt < (time.Now()).Unix() { | ||
190 | return claims, false | ||
191 | } | ||
192 | |||
193 | // check if role extracted from token matches | ||
194 | // any of the provided (allowed) ones | ||
195 | for _, r := range authRoles { | ||
196 | if claims.Role == r || r == "*" { | ||
197 | return claims, true | ||
198 | } | ||
199 | } | ||
200 | |||
201 | return claims, false | ||
202 | } | ||
203 | |||
204 | // GetTokenClaims extracts JWT claims from Authorization header of the request. | ||
205 | // Returns token claims or an error. | ||
206 | func GetTokenClaims(req *http.Request) (*TokenClaims, error) { | ||
207 | // check for and strip 'Bearer' prefix | ||
208 | var tokstr string | ||
209 | authHead := req.Header.Get("Authorization") | ||
210 | if ok := strings.HasPrefix(authHead, "Bearer "); ok { | ||
211 | tokstr = strings.TrimPrefix(authHead, "Bearer ") | ||
212 | } else { | ||
213 | return &TokenClaims{}, errors.New("authorization header in incomplete") | ||
214 | } | ||
215 | |||
216 | token, err := jwt.ParseWithClaims(tokstr, &TokenClaims{}, secretFunc) | ||
217 | if err != nil { | ||
218 | return &TokenClaims{}, err | ||
219 | } | ||
220 | |||
221 | // type assertion | ||
222 | claims, ok := token.Claims.(*TokenClaims) | ||
223 | if !ok || !token.Valid { | ||
224 | return &TokenClaims{}, errors.New("token is not valid") | ||
225 | } | ||
226 | |||
227 | return claims, nil | ||
228 | } | ||
229 | |||
230 | // randomSalt returns a string of random characters of 'saltSize' length. | ||
231 | func randomSalt() (s string, err error) { | ||
232 | rawsalt := make([]byte, saltSize) | ||
233 | |||
234 | _, err = rand.Read(rawsalt) | ||
235 | if err != nil { | ||
236 | return "", err | ||
237 | } | ||
238 | |||
239 | s = hex.EncodeToString(rawsalt) | ||
240 | return s, nil | ||
241 | } | ||
242 | |||
243 | // secretFunc returns byte slice of API secret keyword. | ||
244 | func secretFunc(token *jwt.Token) (interface{}, error) { | ||
245 | return []byte(secret), nil | ||
246 | } | ||
247 | 1 | package webutility |
format.go
File was created | 1 | package webutility | |
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | "time" | ||
6 | ) | ||
7 | |||
8 | // UnixToDate converts given Unix time to local time in format and returns result: | ||
9 | // YYYY-MM-DD hh:mm:ss +zzzz UTC | ||
10 | func UnixToDate(unix int64) time.Time { | ||
11 | return time.Unix(unix, 0) | ||
12 | } | ||
13 | |||
14 | // DateToUnix converts given date in Unix timestamp. | ||
15 | func DateToUnix(date interface{}) int64 { | ||
16 | if date != nil { | ||
17 | t, ok := date.(time.Time) | ||
18 | if !ok { | ||
19 | return 0 | ||
20 | } | ||
21 | return t.Unix() | ||
22 | |||
23 | } | ||
24 | return 0 | ||
25 | } | ||
26 | |||
27 | // EqualQuotes encapsulates given string in SQL 'equal' statement and returns result. | ||
28 | // Example: "hello" -> " = 'hello'" | ||
29 | func EqualQuotes(stmt string) string { | ||
30 | if stmt != "" { | ||
31 | stmt = fmt.Sprintf(" = '%s'", stmt) | ||
32 | } | ||
33 | return stmt | ||
34 | } | ||
35 | |||
36 | func EqualString(stmt string) string { | ||
37 | if stmt != "" { | ||
38 | stmt = fmt.Sprintf(" = %s", stmt) | ||
39 | } | ||
40 | return stmt | ||
41 | } | ||
42 | |||
43 | // LikeQuotes encapsulates given string in SQL 'like' statement and returns result. | ||
44 | // Example: "hello" -> " LIKE UPPER('%hello%')" | ||
45 | func LikeQuotes(stmt string) string { | ||
46 | if stmt != "" { | ||
47 | stmt = fmt.Sprintf(" LIKE UPPER('%s%s%s')", "%", stmt, "%") | ||
48 | } | ||
49 | return stmt | ||
50 | } | ||
51 |
format_utility.go
1 | package webutility | File was deleted | |
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | "time" | ||
6 | ) | ||
7 | |||
8 | // UnixToDate converts given Unix time to local time in format and returns result: | ||
9 | // YYYY-MM-DD hh:mm:ss +zzzz UTC | ||
10 | func UnixToDate(unix int64) time.Time { | ||
11 | return time.Unix(unix, 0) | ||
12 | } | ||
13 | |||
14 | // DateToUnix converts given date in Unix timestamp. | ||
15 | func DateToUnix(date interface{}) int64 { | ||
16 | if date != nil { | ||
17 | t, ok := date.(time.Time) | ||
18 | if !ok { | ||
19 | return 0 | ||
20 | } | ||
21 | return t.Unix() | ||
22 | |||
23 | } | ||
24 | return 0 | ||
25 | } | ||
26 | |||
27 | // EqualQuotes encapsulates given string in SQL 'equal' statement and returns result. | ||
28 | // Example: "hello" -> " = 'hello'" | ||
29 | func EqualQuotes(stmt string) string { | ||
30 | if stmt != "" { | ||
31 | stmt = fmt.Sprintf(" = '%s'", stmt) | ||
32 | } | ||
33 | return stmt | ||
34 | } | ||
35 | |||
36 | func EqualString(stmt string) string { | ||
37 | if stmt != "" { | ||
38 | stmt = fmt.Sprintf(" = %s", stmt) | ||
39 | } | ||
40 | return stmt | ||
41 | } | ||
42 | |||
43 | // LikeQuotes encapsulates given string in SQL 'like' statement and returns result. | ||
44 | // Example: "hello" -> " LIKE UPPER('%hello%')" | ||
45 | func LikeQuotes(stmt string) string { | ||
46 | if stmt != "" { | ||
47 | stmt = fmt.Sprintf(" LIKE UPPER('%s%s%s')", "%", stmt, "%") | ||
48 | } | ||
49 | return stmt | ||
50 | } | ||
51 | 1 | package webutility |
http.go
File was created | 1 | package webutility | |
2 | |||
3 | import ( | ||
4 | "encoding/json" | ||
5 | "net/http" | ||
6 | ) | ||
7 | |||
8 | type webError struct { | ||
9 | Request string `json:"request"` | ||
10 | Error string `json:"error"` | ||
11 | } | ||
12 | |||
13 | // NotFoundHandler writes HTTP error 404 to w. | ||
14 | func NotFoundHandler(w http.ResponseWriter, req *http.Request) { | ||
15 | SetDefaultHeaders(w) | ||
16 | if req.Method == "OPTIONS" { | ||
17 | return | ||
18 | } | ||
19 | NotFound(w, req, "Not found") | ||
20 | } | ||
21 | |||
22 | // SetDefaultHeaders set's default headers for an HTTP response. | ||
23 | func SetDefaultHeaders(w http.ResponseWriter) { | ||
24 | w.Header().Set("Access-Control-Allow-Origin", "*") | ||
25 | w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") | ||
26 | w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type, | ||
27 | Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`) | ||
28 | w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||
29 | } | ||
30 | |||
31 | func ReqLocale(req *http.Request, dflt string) string { | ||
32 | loc := req.FormValue("locale") | ||
33 | if loc == "" { | ||
34 | return dflt | ||
35 | } | ||
36 | return loc | ||
37 | } | ||
38 | |||
39 | // 2xx | ||
40 | func Success(w http.ResponseWriter, payload *Payload, code int) { | ||
41 | w.WriteHeader(code) | ||
42 | if payload != nil { | ||
43 | json.NewEncoder(w).Encode(*payload) | ||
44 | } | ||
45 | } | ||
46 | |||
47 | // 200 | ||
48 | func OK(w http.ResponseWriter, payload *Payload) { | ||
49 | Success(w, payload, http.StatusOK) | ||
50 | } | ||
51 | |||
52 | // 201 | ||
53 | func Created(w http.ResponseWriter, payload *Payload) { | ||
54 | Success(w, payload, http.StatusCreated) | ||
55 | } | ||
56 | |||
57 | // 4xx; 5xx | ||
58 | func Error(w http.ResponseWriter, r *http.Request, code int, err string) { | ||
59 | werr := webError{Error: err, Request: r.Method + " " + r.RequestURI} | ||
60 | w.WriteHeader(code) | ||
61 | json.NewEncoder(w).Encode(werr) | ||
62 | } | ||
63 | |||
64 | // 400 | ||
65 | func BadRequest(w http.ResponseWriter, r *http.Request, err string) { | ||
66 | Error(w, r, http.StatusBadRequest, err) | ||
67 | } | ||
68 | |||
69 | // 404 | ||
70 | func NotFound(w http.ResponseWriter, r *http.Request, err string) { | ||
71 | Error(w, r, http.StatusNotFound, err) | ||
72 | } | ||
73 | |||
74 | // 401 | ||
75 | func Unauthorized(w http.ResponseWriter, r *http.Request, err string) { | ||
76 | Error(w, r, http.StatusUnauthorized, err) | ||
77 | } | ||
78 | |||
79 | // 403 | ||
80 | func Forbidden(w http.ResponseWriter, r *http.Request, err string) { | ||
81 | Error(w, r, http.StatusForbidden, err) | ||
82 | } | ||
83 | |||
84 | // 403 | ||
85 | func Conflict(w http.ResponseWriter, r *http.Request, err string) { | ||
86 | Error(w, r, http.StatusConflict, err) | ||
87 | } | ||
88 | |||
89 | // 500 | ||
90 | func InternalServerError(w http.ResponseWriter, r *http.Request, err string) { | ||
91 | Error(w, r, http.StatusInternalServerError, err) | ||
92 | } | ||
93 | |||
94 | /// | ||
95 | /// Old API | ||
96 | /// | ||
97 | |||
98 | const ( | ||
99 | templateHttpErr500_EN = "An internal server error has occurred." | ||
100 | templateHttpErr500_RS = "Došlo je do greške na serveru." | ||
101 | templateHttpErr400_EN = "Bad request." | ||
102 | templateHttpErr400_RS = "Neispravan zahtev." | ||
103 | templateHttpErr404_EN = "Resource not found." | ||
104 | templateHttpErr404_RS = "Resurs nije pronadjen." | ||
105 | templateHttpErr401_EN = "Unauthorized request." | ||
106 | templateHttpErr401_RS = "Neautorizovan zahtev." | ||
107 | ) | ||
108 | |||
109 | type httpError struct { | ||
110 | Error []HttpErrorDesc `json:"error"` | ||
111 | Request string `json:"request"` | ||
112 | } | ||
113 | |||
114 | type HttpErrorDesc struct { | ||
115 | Lang string `json:"lang"` | ||
116 | Desc string `json:"description"` | ||
117 | } | ||
118 | |||
119 | // DeliverPayload encodes payload as JSON to w. | ||
120 | func DeliverPayload(w http.ResponseWriter, payload Payload) { | ||
121 | // Don't write status OK in the headers here. Leave it up for the caller. | ||
122 | // E.g. Status 201. | ||
123 | json.NewEncoder(w).Encode(payload) | ||
124 | payload.Data = nil | ||
125 | } | ||
126 | |||
127 | // ErrorResponse writes HTTP error to w. | ||
128 | func ErrorResponse(w http.ResponseWriter, r *http.Request, code int, desc []HttpErrorDesc) { | ||
129 | err := httpError{desc, r.Method + " " + r.RequestURI} | ||
130 | w.WriteHeader(code) | ||
131 | json.NewEncoder(w).Encode(err) | ||
132 | } | ||
133 | |||
134 | // NotFoundResponse writes HTTP error 404 to w. | ||
135 | func NotFoundResponse(w http.ResponseWriter, req *http.Request) { | ||
136 | ErrorResponse(w, req, http.StatusNotFound, []HttpErrorDesc{ | ||
137 | {"en", templateHttpErr404_EN}, | ||
138 | {"rs", templateHttpErr404_RS}, | ||
139 | }) | ||
140 | } | ||
141 | |||
142 | // BadRequestResponse writes HTTP error 400 to w. | ||
143 | func BadRequestResponse(w http.ResponseWriter, req *http.Request) { | ||
144 | ErrorResponse(w, req, http.StatusBadRequest, []HttpErrorDesc{ | ||
145 | {"en", templateHttpErr400_EN}, | ||
146 | {"rs", templateHttpErr400_RS}, | ||
147 | }) | ||
148 | } | ||
149 | |||
150 | // InternalSeverErrorResponse writes HTTP error 500 to w. | ||
151 | func InternalServerErrorResponse(w http.ResponseWriter, req *http.Request) { | ||
152 | ErrorResponse(w, req, http.StatusInternalServerError, []HttpErrorDesc{ | ||
153 | {"en", templateHttpErr500_EN}, | ||
154 | {"rs", templateHttpErr500_RS}, | ||
155 | }) | ||
156 | } | ||
157 | |||
158 | // UnauthorizedError writes HTTP error 401 to w. | ||
159 | func UnauthorizedResponse(w http.ResponseWriter, req *http.Request) { | ||
160 | w.Header().Set("WWW-Authenticate", "Bearer") | ||
161 | ErrorResponse(w, req, http.StatusUnauthorized, []HttpErrorDesc{ | ||
162 | {"en", templateHttpErr401_EN}, | ||
163 | {"rs", templateHttpErr401_RS}, | ||
164 | }) | ||
165 | } | ||
166 |
http_utility.go
1 | package webutility | File was deleted | |
2 | |||
3 | import ( | ||
4 | "encoding/json" | ||
5 | "net/http" | ||
6 | ) | ||
7 | |||
8 | type webError struct { | ||
9 | Request string `json:"request"` | ||
10 | Error string `json:"error"` | ||
11 | } | ||
12 | |||
13 | // NotFoundHandler writes HTTP error 404 to w. | ||
14 | func NotFoundHandler(w http.ResponseWriter, req *http.Request) { | ||
15 | SetDefaultHeaders(w) | ||
16 | if req.Method == "OPTIONS" { | ||
17 | return | ||
18 | } | ||
19 | NotFound(w, req, "Not found") | ||
20 | } | ||
21 | |||
22 | // SetDefaultHeaders set's default headers for an HTTP response. | ||
23 | func SetDefaultHeaders(w http.ResponseWriter) { | ||
24 | w.Header().Set("Access-Control-Allow-Origin", "*") | ||
25 | w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") | ||
26 | w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type, | ||
27 | Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`) | ||
28 | w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||
29 | } | ||
30 | |||
31 | func ReqLocale(req *http.Request, dflt string) string { | ||
32 | loc := req.FormValue("locale") | ||
33 | if loc == "" { | ||
34 | return dflt | ||
35 | } | ||
36 | return loc | ||
37 | } | ||
38 | |||
39 | // 2xx | ||
40 | func Success(w http.ResponseWriter, payload *Payload, code int) { | ||
41 | w.WriteHeader(code) | ||
42 | if payload != nil { | ||
43 | json.NewEncoder(w).Encode(*payload) | ||
44 | } | ||
45 | } | ||
46 | |||
47 | // 200 | ||
48 | func OK(w http.ResponseWriter, payload *Payload) { | ||
49 | Success(w, payload, http.StatusOK) | ||
50 | } | ||
51 | |||
52 | // 201 | ||
53 | func Created(w http.ResponseWriter, payload *Payload) { | ||
54 | Success(w, payload, http.StatusCreated) | ||
55 | } | ||
56 | |||
57 | // 4xx; 5xx | ||
58 | func Error(w http.ResponseWriter, r *http.Request, code int, err string) { | ||
59 | werr := webError{Error: err, Request: r.Method + " " + r.RequestURI} | ||
60 | w.WriteHeader(code) | ||
61 | json.NewEncoder(w).Encode(werr) | ||
62 | } | ||
63 | |||
64 | // 400 | ||
65 | func BadRequest(w http.ResponseWriter, r *http.Request, err string) { | ||
66 | Error(w, r, http.StatusBadRequest, err) | ||
67 | } | ||
68 | |||
69 | // 404 | ||
70 | func NotFound(w http.ResponseWriter, r *http.Request, err string) { | ||
71 | Error(w, r, http.StatusNotFound, err) | ||
72 | } | ||
73 | |||
74 | // 401 | ||
75 | func Unauthorized(w http.ResponseWriter, r *http.Request, err string) { | ||
76 | Error(w, r, http.StatusUnauthorized, err) | ||
77 | } | ||
78 | |||
79 | // 403 | ||
80 | func Forbidden(w http.ResponseWriter, r *http.Request, err string) { | ||
81 | Error(w, r, http.StatusForbidden, err) | ||
82 | } | ||
83 | |||
84 | // 403 | ||
85 | func Conflict(w http.ResponseWriter, r *http.Request, err string) { | ||
86 | Error(w, r, http.StatusConflict, err) | ||
87 | } | ||
88 | |||
89 | // 500 | ||
90 | func InternalServerError(w http.ResponseWriter, r *http.Request, err string) { | ||
91 | Error(w, r, http.StatusInternalServerError, err) | ||
92 | } | ||
93 | |||
94 | /// | ||
95 | /// Old API | ||
96 | /// | ||
97 | |||
98 | const ( | ||
99 | templateHttpErr500_EN = "An internal server error has occurred." | ||
100 | templateHttpErr500_RS = "Došlo je do greške na serveru." | ||
101 | templateHttpErr400_EN = "Bad request." | ||
102 | templateHttpErr400_RS = "Neispravan zahtev." | ||
103 | templateHttpErr404_EN = "Resource not found." | ||
104 | templateHttpErr404_RS = "Resurs nije pronadjen." | ||
105 | templateHttpErr401_EN = "Unauthorized request." | ||
106 | templateHttpErr401_RS = "Neautorizovan zahtev." | ||
107 | ) | ||
108 | |||
109 | type httpError struct { | ||
110 | Error []HttpErrorDesc `json:"error"` | ||
111 | Request string `json:"request"` | ||
112 | } | ||
113 | |||
114 | type HttpErrorDesc struct { | ||
115 | Lang string `json:"lang"` | ||
116 | Desc string `json:"description"` | ||
117 | } | ||
118 | |||
119 | // DeliverPayload encodes payload as JSON to w. | ||
120 | func DeliverPayload(w http.ResponseWriter, payload Payload) { | ||
121 | // Don't write status OK in the headers here. Leave it up for the caller. | ||
122 | // E.g. Status 201. | ||
123 | json.NewEncoder(w).Encode(payload) | ||
124 | payload.Data = nil | ||
125 | } | ||
126 | |||
127 | // ErrorResponse writes HTTP error to w. | ||
128 | func ErrorResponse(w http.ResponseWriter, r *http.Request, code int, desc []HttpErrorDesc) { | ||
129 | err := httpError{desc, r.Method + " " + r.RequestURI} | ||
130 | w.WriteHeader(code) | ||
131 | json.NewEncoder(w).Encode(err) | ||
132 | } | ||
133 | |||
134 | // NotFoundResponse writes HTTP error 404 to w. | ||
135 | func NotFoundResponse(w http.ResponseWriter, req *http.Request) { | ||
136 | ErrorResponse(w, req, http.StatusNotFound, []HttpErrorDesc{ | ||
137 | {"en", templateHttpErr404_EN}, | ||
138 | {"rs", templateHttpErr404_RS}, | ||
139 | }) | ||
140 | } | ||
141 | |||
142 | // BadRequestResponse writes HTTP error 400 to w. | ||
143 | func BadRequestResponse(w http.ResponseWriter, req *http.Request) { | ||
144 | ErrorResponse(w, req, http.StatusBadRequest, []HttpErrorDesc{ | ||
145 | {"en", templateHttpErr400_EN}, | ||
146 | {"rs", templateHttpErr400_RS}, | ||
147 | }) | ||
148 | } | ||
149 | |||
150 | // InternalSeverErrorResponse writes HTTP error 500 to w. | ||
151 | func InternalServerErrorResponse(w http.ResponseWriter, req *http.Request) { | ||
152 | ErrorResponse(w, req, http.StatusInternalServerError, []HttpErrorDesc{ | ||
153 | {"en", templateHttpErr500_EN}, | ||
154 | {"rs", templateHttpErr500_RS}, | ||
155 | }) | ||
156 | } | ||
157 | |||
158 | // UnauthorizedError writes HTTP error 401 to w. | ||
159 | func UnauthorizedResponse(w http.ResponseWriter, req *http.Request) { | ||
160 | w.Header().Set("WWW-Authenticate", "Bearer") | ||
161 | ErrorResponse(w, req, http.StatusUnauthorized, []HttpErrorDesc{ | ||
162 | {"en", templateHttpErr401_EN}, | ||
163 | {"rs", templateHttpErr401_RS}, | ||
164 | }) | ||
165 | } | ||
166 | 1 | package webutility |
json.go
File was created | 1 | package webutility | |
2 | |||
3 | import ( | ||
4 | "database/sql" | ||
5 | "encoding/json" | ||
6 | "errors" | ||
7 | "fmt" | ||
8 | "io" | ||
9 | "net/http" | ||
10 | "sync" | ||
11 | "time" | ||
12 | |||
13 | "git.to-net.rs/marko.tikvic/gologger" | ||
14 | ) | ||
15 | |||
16 | var ( | ||
17 | mu = &sync.Mutex{} | ||
18 | metadata = make(map[string]Payload) | ||
19 | updateQue = make(map[string][]byte) | ||
20 | |||
21 | metadataDB *sql.DB | ||
22 | activeProject string | ||
23 | |||
24 | inited bool | ||
25 | driver string | ||
26 | ) | ||
27 | |||
28 | var logger *gologger.Logger | ||
29 | |||
30 | func init() { | ||
31 | var err error | ||
32 | logger, err = gologger.New("webutility", gologger.MaxLogSize100KB) | ||
33 | if err != nil { | ||
34 | fmt.Printf("webutility: %s\n", err.Error()) | ||
35 | } | ||
36 | } | ||
37 | |||
38 | type LangMap map[string]map[string]string | ||
39 | |||
40 | type Field struct { | ||
41 | Parameter string `json:"param"` | ||
42 | Type string `json:"type"` | ||
43 | Visible bool `json:"visible"` | ||
44 | Editable bool `json:"editable"` | ||
45 | } | ||
46 | |||
47 | type CorrelationField struct { | ||
48 | Result string `json:"result"` | ||
49 | Elements []string `json:"elements"` | ||
50 | Type string `json:"type"` | ||
51 | } | ||
52 | |||
53 | type Translation struct { | ||
54 | Language string `json:"language"` | ||
55 | FieldsLabels map[string]string `json:"fieldsLabels"` | ||
56 | } | ||
57 | |||
58 | type Payload struct { | ||
59 | Method string `json:"method"` | ||
60 | Params map[string]string `json:"params"` | ||
61 | Lang []Translation `json:"lang"` | ||
62 | Fields []Field `json:"fields"` | ||
63 | Correlations []CorrelationField `json:"correlationFields"` | ||
64 | IdField string `json:"idField"` | ||
65 | |||
66 | // Data holds JSON payload. | ||
67 | // It can't be used for itteration. | ||
68 | Data interface{} `json:"data"` | ||
69 | } | ||
70 | |||
71 | // NewPayload returs a payload sceleton for entity described with etype. | ||
72 | func NewPayload(r *http.Request, etype string) Payload { | ||
73 | pload := metadata[etype] | ||
74 | pload.Method = r.Method + " " + r.RequestURI | ||
75 | return pload | ||
76 | } | ||
77 | |||
78 | // DecodeJSON decodes JSON data from r to v. | ||
79 | // Returns an error if it fails. | ||
80 | func DecodeJSON(r io.Reader, v interface{}) error { | ||
81 | return json.NewDecoder(r).Decode(v) | ||
82 | } | ||
83 | |||
84 | // InitPayloadsMetadata loads all payloads' information into 'metadata' variable. | ||
85 | func InitPayloadsMetadata(drv string, db *sql.DB, project string) error { | ||
86 | if drv != "ora" && drv != "mysql" { | ||
87 | return errors.New("driver not supported") | ||
88 | } | ||
89 | driver = drv | ||
90 | metadataDB = db | ||
91 | activeProject = project | ||
92 | |||
93 | mu.Lock() | ||
94 | defer mu.Unlock() | ||
95 | err := initMetadata(project) | ||
96 | if err != nil { | ||
97 | return err | ||
98 | } | ||
99 | inited = true | ||
100 | |||
101 | return nil | ||
102 | } | ||
103 | |||
104 | func EnableHotloading(interval int) { | ||
105 | if interval > 0 { | ||
106 | go hotload(interval) | ||
107 | } | ||
108 | } | ||
109 | |||
110 | func GetMetadataForAllEntities() map[string]Payload { | ||
111 | return metadata | ||
112 | } | ||
113 | |||
114 | func GetMetadataForEntity(t string) (Payload, bool) { | ||
115 | p, ok := metadata[t] | ||
116 | return p, ok | ||
117 | } | ||
118 | |||
119 | func QueEntityModelUpdate(entityType string, v interface{}) { | ||
120 | updateQue[entityType], _ = json.Marshal(v) | ||
121 | } | ||
122 | |||
123 | func UpdateEntityModels(command string) (total, upd, add int, err error) { | ||
124 | if command != "force" && command != "missing" { | ||
125 | return total, 0, 0, errors.New("webutility: unknown command: " + command) | ||
126 | } | ||
127 | |||
128 | if !inited { | ||
129 | return 0, 0, 0, errors.New("webutility: metadata not initialized but update was tried.") | ||
130 | } | ||
131 | |||
132 | total = len(updateQue) | ||
133 | |||
134 | toUpdate := make([]string, 0) | ||
135 | toAdd := make([]string, 0) | ||
136 | |||
137 | for k, _ := range updateQue { | ||
138 | if _, exists := metadata[k]; exists { | ||
139 | if command == "force" { | ||
140 | toUpdate = append(toUpdate, k) | ||
141 | } | ||
142 | } else { | ||
143 | toAdd = append(toAdd, k) | ||
144 | } | ||
145 | } | ||
146 | |||
147 | var uStmt *sql.Stmt | ||
148 | if driver == "ora" { | ||
149 | uStmt, err = metadataDB.Prepare("update entities set entity_model = :1 where entity_type = :2") | ||
150 | if err != nil { | ||
151 | return | ||
152 | } | ||
153 | } else if driver == "mysql" { | ||
154 | uStmt, err = metadataDB.Prepare("update entities set entity_model = ? where entity_type = ?") | ||
155 | if err != nil { | ||
156 | return | ||
157 | } | ||
158 | } | ||
159 | for _, k := range toUpdate { | ||
160 | //fmt.Printf("Updating: %s\n", k) | ||
161 | //fmt.Printf("New model: %s\n", updateQue[k]) | ||
162 | _, err = uStmt.Exec(string(updateQue[k]), k) | ||
163 | if err != nil { | ||
164 | logger.Log("webutility: %v\n", err) | ||
165 | return | ||
166 | } | ||
167 | upd++ | ||
168 | } | ||
169 | |||
170 | blankPayload, _ := json.Marshal(Payload{}) | ||
171 | var iStmt *sql.Stmt | ||
172 | if driver == "ora" { | ||
173 | iStmt, err = metadataDB.Prepare("insert into entities(projekat, metadata, entity_type, entity_model) values(:1, :2, :3, :4)") | ||
174 | if err != nil { | ||
175 | return | ||
176 | } | ||
177 | } else if driver == "mysql" { | ||
178 | iStmt, err = metadataDB.Prepare("insert into entities(projekat, metadata, entity_type, entity_model) values(?, ?, ?, ?)") | ||
179 | if err != nil { | ||
180 | return | ||
181 | } | ||
182 | } | ||
183 | for _, k := range toAdd { | ||
184 | _, err = iStmt.Exec(activeProject, string(blankPayload), k, string(updateQue[k])) | ||
185 | if err != nil { | ||
186 | logger.Log("webutility: %v\n", err) | ||
187 | return | ||
188 | } | ||
189 | metadata[k] = Payload{} | ||
190 | add++ | ||
191 | } | ||
192 | |||
193 | return total, upd, add, nil | ||
194 | } | ||
195 | |||
196 | func initMetadata(project string) error { | ||
197 | rows, err := metadataDB.Query(`select | ||
198 | entity_type, | ||
199 | metadata | ||
200 | from entities | ||
201 | where projekat = ` + fmt.Sprintf("'%s'", project)) | ||
202 | if err != nil { | ||
203 | return err | ||
204 | } | ||
205 | defer rows.Close() | ||
206 | |||
207 | count := 0 | ||
208 | success := 0 | ||
209 | if len(metadata) > 0 { | ||
210 | metadata = nil | ||
211 | } | ||
212 | metadata = make(map[string]Payload) | ||
213 | for rows.Next() { | ||
214 | var name, load string | ||
215 | rows.Scan(&name, &load) | ||
216 | |||
217 | p := Payload{} | ||
218 | err := json.Unmarshal([]byte(load), &p) | ||
219 | if err != nil { | ||
220 | logger.Log("webutility: couldn't init: '%s' metadata: %s:\n%s\n", name, err.Error(), load) | ||
221 | } else { | ||
222 | success++ | ||
223 | metadata[name] = p | ||
224 | } | ||
225 | count++ | ||
226 | } | ||
227 | perc := float32(success) / float32(count) * 100.0 | ||
228 | logger.Log("webutility: loaded %d/%d (%.1f%%) entities\n", success, count, perc) | ||
229 | |||
230 | return nil | ||
231 | } | ||
232 | |||
233 | func hotload(n int) { | ||
234 | entityScan := make(map[string]int64) | ||
235 | firstCheck := true | ||
236 | for { | ||
237 | time.Sleep(time.Duration(n) * time.Second) | ||
238 | rows, err := metadataDB.Query(`select | ||
239 | ora_rowscn, | ||
240 | entity_type | ||
241 | from entities where projekat = ` + fmt.Sprintf("'%s'", activeProject)) | ||
242 | if err != nil { | ||
243 | logger.Log("webutility: hotload failed: %v\n", err) | ||
244 | time.Sleep(time.Duration(n) * time.Second) | ||
245 | continue | ||
246 | } | ||
247 | |||
248 | var toRefresh []string | ||
249 | for rows.Next() { | ||
250 | var scanID int64 | ||
251 | var entity string | ||
252 | rows.Scan(&scanID, &entity) | ||
253 | oldID, ok := entityScan[entity] | ||
254 | if !ok || oldID != scanID { | ||
255 | entityScan[entity] = scanID | ||
256 | toRefresh = append(toRefresh, entity) | ||
257 | } | ||
258 | } | ||
259 | rows.Close() | ||
260 | |||
261 | if rows.Err() != nil { | ||
262 | logger.Log("webutility: hotload rset error: %v\n", rows.Err()) | ||
263 | time.Sleep(time.Duration(n) * time.Second) | ||
264 | continue | ||
265 | } | ||
266 | |||
267 | if len(toRefresh) > 0 && !firstCheck { | ||
268 | mu.Lock() | ||
269 | refreshMetadata(toRefresh) | ||
270 | mu.Unlock() | ||
271 | } | ||
272 | if firstCheck { | ||
273 | firstCheck = false | ||
274 | } | ||
275 | } | ||
276 | } | ||
277 | |||
278 | func refreshMetadata(entities []string) { | ||
279 | for _, e := range entities { | ||
280 | fmt.Printf("refreshing %s\n", e) | ||
281 | rows, err := metadataDB.Query(`select | ||
282 | metadata | ||
283 | from entities | ||
284 | where projekat = ` + fmt.Sprintf("'%s'", activeProject) + | ||
285 | ` and entity_type = ` + fmt.Sprintf("'%s'", e)) | ||
286 | |||
287 | if err != nil { | ||
288 | logger.Log("webutility: refresh: prep: %v\n", err) | ||
289 | rows.Close() | ||
290 | continue | ||
291 | } | ||
292 | |||
293 | for rows.Next() { | ||
294 | var load string | ||
295 | rows.Scan(&load) | ||
296 | p := Payload{} | ||
297 | err := json.Unmarshal([]byte(load), &p) | ||
298 | if err != nil { | ||
299 | logger.Log("webutility: couldn't refresh: '%s' metadata: %s\n%s\n", e, err.Error(), load) | ||
300 | } else { | ||
301 | metadata[e] = p | ||
302 | } | ||
303 | } | ||
304 | rows.Close() | ||
305 | } | ||
306 | } | ||
307 | |||
308 | /* | ||
309 | func ModifyMetadataForEntity(entityType string, p *Payload) error { | ||
310 | md, err := json.Marshal(*p) | ||
311 | if err != nil { | ||
312 | return err | ||
313 | } | ||
314 | |||
315 | mu.Lock() | ||
316 | defer mu.Unlock() | ||
317 | _, err = metadataDB.PrepAndExe(`update entities set | ||
318 | metadata = :1 | ||
319 | where projekat = :2 | ||
320 | and entity_type = :3`, | ||
321 | string(md), | ||
322 | activeProject, | ||
323 | entityType) | ||
324 | if err != nil { | ||
325 | return err | ||
326 | } | ||
327 | return nil | ||
328 | } | ||
329 | |||
330 | func DeleteEntityModel(entityType string) error { | ||
331 | _, err := metadataDB.PrepAndExe("delete from entities where entity_type = :1", entityType) | ||
332 | if err == nil { | ||
333 | mu.Lock() | ||
334 | delete(metadata, entityType) | ||
335 | mu.Unlock() | ||
336 | } | ||
337 | return err | ||
338 | } | ||
339 | */ | ||
340 |
json_utility.go
1 | package webutility | File was deleted | |
2 | |||
3 | import ( | ||
4 | "database/sql" | ||
5 | "encoding/json" | ||
6 | "errors" | ||
7 | "fmt" | ||
8 | "io" | ||
9 | "net/http" | ||
10 | "sync" | ||
11 | "time" | ||
12 | |||
13 | "git.to-net.rs/marko.tikvic/gologger" | ||
14 | ) | ||
15 | |||
16 | var ( | ||
17 | mu = &sync.Mutex{} | ||
18 | metadata = make(map[string]Payload) | ||
19 | updateQue = make(map[string][]byte) | ||
20 | |||
21 | metadataDB *sql.DB | ||
22 | activeProject string | ||
23 | |||
24 | inited bool | ||
25 | driver string | ||
26 | ) | ||
27 | |||
28 | var logger *gologger.Logger | ||
29 | |||
30 | func init() { | ||
31 | var err error | ||
32 | logger, err = gologger.New("webutility", gologger.MaxLogSize100KB) | ||
33 | if err != nil { | ||
34 | fmt.Printf("webutility: %s\n", err.Error()) | ||
35 | } | ||
36 | } | ||
37 | |||
38 | type LangMap map[string]map[string]string | ||
39 | |||
40 | type Field struct { | ||
41 | Parameter string `json:"param"` | ||
42 | Type string `json:"type"` | ||
43 | Visible bool `json:"visible"` | ||
44 | Editable bool `json:"editable"` | ||
45 | } | ||
46 | |||
47 | type CorrelationField struct { | ||
48 | Result string `json:"result"` | ||
49 | Elements []string `json:"elements"` | ||
50 | Type string `json:"type"` | ||
51 | } | ||
52 | |||
53 | type Translation struct { | ||
54 | Language string `json:"language"` | ||
55 | FieldsLabels map[string]string `json:"fieldsLabels"` | ||
56 | } | ||
57 | |||
58 | type Payload struct { | ||
59 | Method string `json:"method"` | ||
60 | Params map[string]string `json:"params"` | ||
61 | Lang []Translation `json:"lang"` | ||
62 | Fields []Field `json:"fields"` | ||
63 | Correlations []CorrelationField `json:"correlationFields"` | ||
64 | IdField string `json:"idField"` | ||
65 | |||
66 | // Data holds JSON payload. | ||
67 | // It can't be used for itteration. | ||
68 | Data interface{} `json:"data"` | ||
69 | } | ||
70 | |||
71 | // NewPayload returs a payload sceleton for entity described with etype. | ||
72 | func NewPayload(r *http.Request, etype string) Payload { | ||
73 | pload := metadata[etype] | ||
74 | pload.Method = r.Method + " " + r.RequestURI | ||
75 | return pload | ||
76 | } | ||
77 | |||
78 | // DecodeJSON decodes JSON data from r to v. | ||
79 | // Returns an error if it fails. | ||
80 | func DecodeJSON(r io.Reader, v interface{}) error { | ||
81 | return json.NewDecoder(r).Decode(v) | ||
82 | } | ||
83 | |||
84 | // InitPayloadsMetadata loads all payloads' information into 'metadata' variable. | ||
85 | func InitPayloadsMetadata(drv string, db *sql.DB, project string) error { | ||
86 | if drv != "ora" && drv != "mysql" { | ||
87 | return errors.New("driver not supported") | ||
88 | } | ||
89 | driver = drv | ||
90 | metadataDB = db | ||
91 | activeProject = project | ||
92 | |||
93 | mu.Lock() | ||
94 | defer mu.Unlock() | ||
95 | err := initMetadata(project) | ||
96 | if err != nil { | ||
97 | return err | ||
98 | } | ||
99 | inited = true | ||
100 | |||
101 | return nil | ||
102 | } | ||
103 | |||
104 | func EnableHotloading(interval int) { | ||
105 | if interval > 0 { | ||
106 | go hotload(interval) | ||
107 | } | ||
108 | } | ||
109 | |||
110 | func GetMetadataForAllEntities() map[string]Payload { | ||
111 | return metadata | ||
112 | } | ||
113 | |||
114 | func GetMetadataForEntity(t string) (Payload, bool) { | ||
115 | p, ok := metadata[t] | ||
116 | return p, ok | ||
117 | } | ||
118 | |||
119 | func QueEntityModelUpdate(entityType string, v interface{}) { | ||
120 | updateQue[entityType], _ = json.Marshal(v) | ||
121 | } | ||
122 | |||
123 | func initMetadata(project string) error { | ||
124 | rows, err := metadataDB.Query(`select | ||
125 | entity_type, | ||
126 | metadata | ||
127 | from entities | ||
128 | where projekat = ` + fmt.Sprintf("'%s'", project)) | ||
129 | if err != nil { | ||
130 | return err | ||
131 | } | ||
132 | defer rows.Close() | ||
133 | |||
134 | count := 0 | ||
135 | success := 0 | ||
136 | if len(metadata) > 0 { | ||
137 | metadata = nil | ||
138 | } | ||
139 | metadata = make(map[string]Payload) | ||
140 | for rows.Next() { | ||
141 | var name, load string | ||
142 | rows.Scan(&name, &load) | ||
143 | |||
144 | p := Payload{} | ||
145 | err := json.Unmarshal([]byte(load), &p) | ||
146 | if err != nil { | ||
147 | logger.Log("webutility: couldn't init: '%s' metadata: %s\n%s\n", name, err.Error(), load) | ||
148 | } else { | ||
149 | success++ | ||
150 | metadata[name] = p | ||
151 | } | ||
152 | count++ | ||
153 | } | ||
154 | perc := float32(success/count) * 100.0 | ||
155 | logger.Log("webutility: loaded %d/%d (%.1f%%) entities\n", success, count, perc) | ||
156 | |||
157 | return nil | ||
158 | } | ||
159 | |||
160 | func hotload(n int) { | ||
161 | entityScan := make(map[string]int64) | ||
162 | firstCheck := true | ||
163 | for { | ||
164 | time.Sleep(time.Duration(n) * time.Second) | ||
165 | rows, err := metadataDB.Query(`select | ||
166 | ora_rowscn, | ||
167 | entity_type | ||
168 | from entities where projekat = ` + fmt.Sprintf("'%s'", activeProject)) | ||
169 | if err != nil { | ||
170 | logger.Log("webutility: hotload failed: %v\n", err) | ||
171 | time.Sleep(time.Duration(n) * time.Second) | ||
172 | continue | ||
173 | } | ||
174 | |||
175 | var toRefresh []string | ||
176 | for rows.Next() { | ||
177 | var scanID int64 | ||
178 | var entity string | ||
179 | rows.Scan(&scanID, &entity) | ||
180 | oldID, ok := entityScan[entity] | ||
181 | if !ok || oldID != scanID { | ||
182 | entityScan[entity] = scanID | ||
183 | toRefresh = append(toRefresh, entity) | ||
184 | } | ||
185 | } | ||
186 | rows.Close() | ||
187 | |||
188 | if rows.Err() != nil { | ||
189 | logger.Log("webutility: hotload rset error: %v\n", rows.Err()) | ||
190 | time.Sleep(time.Duration(n) * time.Second) | ||
191 | continue | ||
192 | } | ||
193 | |||
194 | if len(toRefresh) > 0 && !firstCheck { | ||
195 | mu.Lock() | ||
196 | refreshMetadata(toRefresh) | ||
197 | mu.Unlock() | ||
198 | } | ||
199 | if firstCheck { | ||
200 | firstCheck = false | ||
201 | } | ||
202 | } | ||
203 | } | ||
204 | |||
205 | func refreshMetadata(entities []string) { | ||
206 | for _, e := range entities { | ||
207 | fmt.Printf("refreshing %s\n", e) | ||
208 | rows, err := metadataDB.Query(`select | ||
209 | metadata | ||
210 | from entities | ||
211 | where projekat = ` + fmt.Sprintf("'%s'", activeProject) + | ||
212 | ` and entity_type = ` + fmt.Sprintf("'%s'", e)) | ||
213 | |||
214 | if err != nil { | ||
215 | logger.Log("webutility: refresh: prep: %v\n", err) | ||
216 | rows.Close() | ||
217 | continue | ||
218 | } | ||
219 | |||
220 | for rows.Next() { | ||
221 | var load string | ||
222 | rows.Scan(&load) | ||
223 | p := Payload{} | ||
224 | err := json.Unmarshal([]byte(load), &p) | ||
225 | if err != nil { | ||
226 | logger.Log("webutility: couldn't refresh: '%s' metadata: %s\n%s\n", e, err.Error(), load) | ||
227 | } else { | ||
228 | metadata[e] = p | ||
229 | } | ||
230 | } | ||
231 | rows.Close() | ||
232 | } | ||
233 | } | ||
234 | |||
235 | func UpdateEntityModels(command string) (total, upd, add int, err error) { | ||
236 | if command != "force" && command != "missing" { | ||
237 | return total, 0, 0, errors.New("webutility: unknown command: " + command) | ||
238 | } | ||
239 | |||
240 | if !inited { | ||
241 | return 0, 0, 0, errors.New("webutility: metadata not initialized but update was tried.") | ||
242 | } | ||
243 | |||
244 | total = len(updateQue) | ||
245 | |||
246 | toUpdate := make([]string, 0) | ||
247 | toAdd := make([]string, 0) | ||
248 | |||
249 | for k, _ := range updateQue { | ||
250 | if _, exists := metadata[k]; exists { | ||
251 | if command == "force" { | ||
252 | toUpdate = append(toUpdate, k) | ||
253 | } | ||
254 | } else { | ||
255 | toAdd = append(toAdd, k) | ||
256 | } | ||
257 | } | ||
258 | |||
259 | var uStmt *sql.Stmt | ||
260 | if driver == "ora" { | ||
261 | uStmt, err = metadataDB.Prepare("update entities set entity_model = :1 where entity_type = :2") | ||
262 | if err != nil { | ||
263 | return | ||
264 | } | ||
265 | } else if driver == "mysql" { | ||
266 | uStmt, err = metadataDB.Prepare("update entities set entity_model = ? where entity_type = ?") | ||
267 | if err != nil { | ||
268 | return | ||
269 | } | ||
270 | } | ||
271 | for _, k := range toUpdate { | ||
272 | //fmt.Printf("Updating: %s\n", k) | ||
273 | //fmt.Printf("New model: %s\n", updateQue[k]) | ||
274 | _, err = uStmt.Exec(string(updateQue[k]), k) | ||
275 | if err != nil { | ||
276 | logger.Log("webutility: %v\n", err) | ||
277 | return | ||
278 | } | ||
279 | upd++ | ||
280 | } | ||
281 | |||
282 | blankPayload, _ := json.Marshal(Payload{}) | ||
283 | var iStmt *sql.Stmt | ||
284 | if driver == "ora" { | ||
285 | iStmt, err = metadataDB.Prepare("INSERT INTO ENTITIES(PROJEKAT, METADATA, ENTITY_TYPE, ENTITY_MODEL) VALUES(:1, :2, :3, :4)") | ||
286 | if err != nil { | ||
287 | return | ||
288 | } | ||
289 | } else if driver == "mysql" { | ||
290 | iStmt, err = metadataDB.Prepare("INSERT INTO ENTITIES(PROJEKAT, METADATA, ENTITY_TYPE, ENTITY_MODEL) VALUES(?, ?, ?, ?)") | ||
291 | if err != nil { | ||
292 | return | ||
293 | } | ||
294 | } | ||
295 | for _, k := range toAdd { | ||
296 | _, err = iStmt.Exec(activeProject, string(blankPayload), k, string(updateQue[k])) | ||
297 | if err != nil { | ||
298 | logger.Log("webutility: %v\n", err) | ||
299 | return | ||
300 | } | ||
301 | metadata[k] = Payload{} | ||
302 | add++ | ||
303 | } | ||
304 | |||
305 | return total, upd, add, nil | ||
306 | } | ||
307 | |||
308 | /* | ||
309 | func ModifyMetadataForEntity(entityType string, p *Payload) error { | ||
310 | md, err := json.Marshal(*p) | ||
311 | if err != nil { | ||
312 | return err | ||
313 | } | ||
314 | |||
315 | mu.Lock() | ||
316 | defer mu.Unlock() | ||
317 | _, err = metadataDB.PrepAndExe(`update entities set | ||
318 | metadata = :1 | ||
319 | where projekat = :2 | ||
320 | and entity_type = :3`, | ||
321 | string(md), | ||
322 | activeProject, | ||
323 | entityType) | ||
324 | if err != nil { | ||
325 | return err | ||
326 | } | ||
327 | return nil | ||
328 | } | ||
329 | |||
330 | func DeleteEntityModel(entityType string) error { | ||
331 | _, err := metadataDB.PrepAndExe("delete from entities where entity_type = :1", entityType) | ||
332 | if err == nil { | ||
333 | mu.Lock() | ||
334 | delete(metadata, entityType) | ||
335 | mu.Unlock() | ||
336 | } | ||
337 | return err | ||
338 | } | ||
339 | */ | ||
340 | 1 | package webutility |
locale_utility.go
1 | package webutility | File was deleted | |
2 | |||
3 | import ( | ||
4 | "encoding/json" | ||
5 | "errors" | ||
6 | "io/ioutil" | ||
7 | ) | ||
8 | |||
9 | type Dictionary struct { | ||
10 | locales map[string]map[string]string | ||
11 | supported []string | ||
12 | defaultLocale string | ||
13 | } | ||
14 | |||
15 | func NewDictionary() Dictionary { | ||
16 | return Dictionary{ | ||
17 | locales: map[string]map[string]string{}, | ||
18 | } | ||
19 | } | ||
20 | |||
21 | func (d *Dictionary) AddLocale(loc, filePath string) error { | ||
22 | file, err := ioutil.ReadFile(filePath) | ||
23 | if err != nil { | ||
24 | return err | ||
25 | } | ||
26 | |||
27 | var data interface{} | ||
28 | err = json.Unmarshal(file, &data) | ||
29 | if err != nil { | ||
30 | return err | ||
31 | } | ||
32 | |||
33 | l := map[string]string{} | ||
34 | for k, v := range data.(map[string]interface{}) { | ||
35 | l[k] = v.(string) | ||
36 | } | ||
37 | d.locales[loc] = l | ||
38 | d.supported = append(d.supported, loc) | ||
39 | |||
40 | return nil | ||
41 | } | ||
42 | |||
43 | func (d *Dictionary) Translate(loc, key string) string { | ||
44 | return d.locales[loc][key] | ||
45 | } | ||
46 | |||
47 | func (d *Dictionary) HasLocale(loc string) bool { | ||
48 | for _, v := range d.supported { | ||
49 | if v == loc { | ||
50 | return true | ||
51 | } | ||
52 | } | ||
53 | return false | ||
54 | } | ||
55 | |||
56 | func (d *Dictionary) SetDefaultLocale(loc string) error { | ||
57 | if !d.HasLocale(loc) { | ||
58 | return errors.New("dictionary does not contain translations for " + loc) | ||
59 | } | ||
60 | d.defaultLocale = loc | ||
61 | return nil | ||
62 | } | ||
63 | |||
64 | func (d *Dictionary) GetDefaultLocale() string { | ||
65 | return d.defaultLocale | ||
66 | } | ||
67 | 1 | package webutility |
localization.go
File was created | 1 | package webutility | |
2 | |||
3 | import ( | ||
4 | "encoding/json" | ||
5 | "errors" | ||
6 | "io/ioutil" | ||
7 | ) | ||
8 | |||
9 | type Dictionary struct { | ||
10 | locales map[string]map[string]string | ||
11 | supported []string | ||
12 | defaultLocale string | ||
13 | } | ||
14 | |||
15 | func NewDictionary() Dictionary { | ||
16 | return Dictionary{ | ||
17 | locales: map[string]map[string]string{}, | ||
18 | } | ||
19 | } | ||
20 | |||
21 | func (d *Dictionary) AddLocale(loc, filePath string) error { | ||
22 | file, err := ioutil.ReadFile(filePath) | ||
23 | if err != nil { | ||
24 | return err | ||
25 | } | ||
26 | |||
27 | var data interface{} | ||
28 | err = json.Unmarshal(file, &data) | ||
29 | if err != nil { | ||
30 | return err | ||
31 | } | ||
32 | |||
33 | l := map[string]string{} | ||
34 | for k, v := range data.(map[string]interface{}) { | ||
35 | l[k] = v.(string) | ||
36 | } | ||
37 | d.locales[loc] = l | ||
38 | d.supported = append(d.supported, loc) | ||
39 | |||
40 | return nil | ||
41 | } | ||
42 | |||
43 | func (d *Dictionary) Translate(loc, key string) string { | ||
44 | return d.locales[loc][key] | ||
45 | } | ||
46 | |||
47 | func (d *Dictionary) HasLocale(loc string) bool { | ||
48 | for _, v := range d.supported { | ||
49 | if v == loc { | ||
50 | return true | ||
51 | } | ||
52 | } | ||
53 | return false | ||
54 | } | ||
55 | |||
56 | func (d *Dictionary) SetDefaultLocale(loc string) error { | ||
57 | if !d.HasLocale(loc) { | ||
58 | return errors.New("dictionary does not contain translations for " + loc) | ||
59 | } | ||
60 | d.defaultLocale = loc | ||
61 | return nil | ||
62 | } | ||
63 | |||
64 | func (d *Dictionary) GetDefaultLocale() string { | ||
65 | return d.defaultLocale | ||
66 | } | ||
67 |