feat(admin auth): create auth API for use with frontend created auth hardcoded values in config.yaml and API endpoints to verify credentials and return JWT token and also a middleware to verify token. (not tested with frontend might need more changes)
Brijesh ops@brijesh.dev
Tue, 09 Jul 2024 18:06:15 +0530
9 files changed,
202 insertions(+),
28 deletions(-)
M
go.sum
→
go.sum
@@ -1,19 +1,14 @@
-github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/joho/godotenv v1.5.0 h1:C/Vohk/9L1RCoS/UW2gfyi2N0EElSW3yb9zwi3PjosE= github.com/joho/godotenv v1.5.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM= -github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/tursodatabase/go-libsql v0.0.0-20240429120401-651096bbee0b h1:R7hev4b96zgXjKbS2ZNbHBnDvyFZhH+LlMqtKH6hIkU= -github.com/tursodatabase/go-libsql v0.0.0-20240429120401-651096bbee0b/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8= -golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= -golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
A
internal/auth.go
@@ -0,0 +1,67 @@
+package internal + +import ( + "encoding/json" + "net/http" + "time" + "watchman/schema" + "watchman/utils" + + "github.com/golang-jwt/jwt/v5" +) + +func AdminLogin(w http.ResponseWriter, r *http.Request) { + utils.HandleMethodNotAllowed(w, r, http.MethodPost) + + config := utils.Read_Config() + + var user schema.User + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&user) + if err != nil { + utils.HandleError(w, r, http.StatusBadRequest, "Error decoding JSON: ", nil) + } + + if user.Username != config.Admin.Username || user.Password != config.Admin.Password { + response := schema.Response_Type{ + Status: "ERROR", + Message: "Invalid credentials", + RequestID: r.Context().Value(schema.RequestIDKey{}).(string), + } + w.WriteHeader(http.StatusUnauthorized) + utils.SendResponse(w, r, response) + return + } + + expirationTime := time.Now().Add(30 * time.Minute) + claims := &schema.Claims{ + Username: user.Username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expirationTime), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString(config.JwtKey) + if err != nil { + utils.HandleError(w, r, http.StatusInternalServerError, "Error signing token: ", err) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: "token", + Value: tokenString, + Expires: expirationTime, + }) + + response := schema.Response_Type{ + Status: "OK", + Message: "Login successful", + RequestID: r.Context().Value(schema.RequestIDKey{}).(string), + Data: map[string]string{ + "token": tokenString, + "expires_at": expirationTime.String(), + }, + } + utils.SendResponse(w, r, response) +}
A
internal/projectKeys.go
@@ -0,0 +1,79 @@
+package internal + +import ( + "database/sql" + "encoding/json" + "math/rand" + "net/http" + "watchman/schema" + "watchman/utils" +) + +func GenerateKeys() (string, string) { + numbers := "1234567890" + smallAlphabets := "abcdefghijklmnopqrstuvwxyz" + capitalAlphabets := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + accessKeyChars := capitalAlphabets + numbers + secretKeyChars := capitalAlphabets + smallAlphabets + numbers + + accessKeyCharsLen := len(accessKeyChars) + secretKeyCharsLen := len(secretKeyChars) + + accessKey := make([]byte, 16) + secretKey := make([]byte, 32) + + for i := range accessKey { + accessKey[i] = accessKeyChars[rand.Intn(accessKeyCharsLen)] + } + + for i := range secretKey { + secretKey[i] = secretKeyChars[rand.Intn(secretKeyCharsLen)] + } + + return string(accessKey), string(secretKey) +} + +func CreateProjectKey(w http.ResponseWriter, r *http.Request, db *sql.DB, ProjectID string) { + AccessKey, SecretKey := GenerateKeys() + + var ProjectKey schema.ProjectKey + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&ProjectKey) + utils.HandleError(w, r, http.StatusBadRequest, "Error decoding JSON: ", err) + + stmt, err := db.Prepare("INSERT INTO ProjectKeys (ProjectID, AccessKey, SecretKey, Expiry) VALUES (?, ?, ?, ?)") + utils.HandleError(w, r, http.StatusInternalServerError, "Error preparing statement: ", err) + defer stmt.Close() + + _, err = stmt.Exec(ProjectID, AccessKey, SecretKey, ProjectKey.Expiry) + utils.HandleError(w, r, http.StatusInternalServerError, "Error executing statement: ", err) + + response := schema.Response_Type{ + Status: "OK", + Message: "Project key created successfully", + RequestID: r.Context().Value(schema.RequestIDKey{}).(string), + } + utils.SendResponse(w, r, response) +} + +func DeleteProjectKey(w http.ResponseWriter, r *http.Request, db *sql.DB) { + var ProjectKey schema.ProjectKey + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&ProjectKey) + utils.HandleError(w, r, http.StatusBadRequest, "Error decoding JSON: ", err) + + stmt, err := db.Prepare("DELETE FROM ProjectKeys WHERE AccessKey = ? AND SecretKey = ?") + utils.HandleError(w, r, http.StatusInternalServerError, "Error preparing statement: ", err) + defer stmt.Close() + + _, err = stmt.Exec(ProjectKey.AccessKey, ProjectKey.SecretKey) + utils.HandleError(w, r, http.StatusInternalServerError, "Error executing statement: ", err) + + response := schema.Response_Type{ + Status: "OK", + Message: "Project key deleted successfully", + RequestID: r.Context().Value(schema.RequestIDKey{}).(string), + } + utils.SendResponse(w, r, response) +}
M
main.go
→
main.go
@@ -16,23 +16,6 @@
_ "github.com/mattn/go-sqlite3" ) -const ( - RED = "\033[31m" - RESET_COLOUR = "\033[0m" -) - -func init() { - env_vars := []string{"PORT"} - - for _, env_var := range env_vars { - if !utils.Verify_ENV_Exists(env_var) { - log.Fatal(RED + "Error: " + env_var + " not found in .env file" + RESET_COLOUR) - } - } - - go middleware.CleanupVisitors() -} - func main() { config := utils.Read_Config()@@ -66,7 +49,6 @@ response := schema.Response_Type{
Status: "ERROR", Message: "Project ID not provided", RequestID: r.Context().Value(schema.RequestIDKey{}).(string), - Data: nil, } w.WriteHeader(http.StatusBadRequest)@@ -102,11 +84,14 @@ utils.Method_Not_Allowed_Handler(w, r)
} }) + multiplexer.HandleFunc("/login", internal.AdminLogin) + fmt.Println("Starting server on port " + strconv.Itoa(config.Port)) log.Fatal(http.ListenAndServe(":"+strconv.Itoa(config.Port), middleware.CorsMiddleware( middleware.RequestIDMiddleware( middleware.Ratelimit( + config, multiplexer, ))))) }
M
schema/db.go
→
schema/db.go
@@ -13,3 +13,10 @@ UserID string `json:"user_id"`
ProjectID string `json:"project_id"` Time int32 `json:"time"` } + +type ProjectKey struct { + ProjectID string `json:"project_id"` + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + Expiry int32 `json:"expiry"` +}
A
schema/schema.sql
@@ -0,0 +1,20 @@
+CREATE TABLE Projects ( + ID TEXT PRIMARY KEY, + Name TEXT NOT NULL +); +CREATE TABLE Logs ( + Time INTEGER NOT NULL, + Level TEXT NOT NULL, + Message TEXT NOT NULL, + Subject TEXT, + UserID TEXT, + ProjectID TEXT NOT NULL, + FOREIGN KEY (ProjectID) REFERENCES Project(ID) +); +CREATE TABLE ProjectKeys ( + ProjectID TEXT NOT NULL, + AccessKey TEXT NOT NULL, + SecretKey TEXT NOT NULL, + Expiry INTEGER, + FOREIGN KEY (ProjectID) REFERENCES Projects(ID) +);
M
schema/types.go
→
schema/types.go
@@ -1,5 +1,7 @@
package schema +import "github.com/golang-jwt/jwt/v5" + type Response_Type struct { Data interface{} `json:"data,omitempty"` Status string `json:"status"`@@ -9,6 +11,24 @@ }
type RequestIDKey struct{} +type UsernameKey struct{} + type ConfigType struct { - Port int `yaml:"port"` + Admin struct { + Username string `yaml:"username"` + Password string `yaml:"password"` + } `yaml:"admin"` + JwtKey string `yaml:"jwt_key"` + Port int `yaml:"port"` + RateLimitRequestsPerSecond int "yaml:\"rate_limit_req_per_sec\"" +} + +type User struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type Claims struct { + Username string `json:"username"` + jwt.RegisteredClaims }