From 7323418d6b77baf52eda84897a9b2b42f1f17fb7 Mon Sep 17 00:00:00 2001 From: John David White <122345776@umail.ucc.ie> Date: Mon, 30 Mar 2026 14:35:29 +0100 Subject: [PATCH] Create Docs page --- management/main.go | 89 ++--- management/static/auth.html | 3 +- management/static/config.html | 3 +- management/static/docs.html | 59 +++ management/static/index.html | 1 + management/static/styles.css | 39 ++ src/config/db.go | 652 +++++++++++++++++----------------- 7 files changed, 475 insertions(+), 371 deletions(-) create mode 100644 management/static/docs.html diff --git a/management/main.go b/management/main.go index 7a5f136..c35a5f3 100644 --- a/management/main.go +++ b/management/main.go @@ -1,43 +1,46 @@ -package main - -import ( - "fyp-api-gateway/management/auth" - "fyp-api-gateway/management/handler" - "log/slog" - "net/http" -) - -func main() { - mux := http.NewServeMux() - - // frontend routes - mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) - //mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // http.Redirect(w, r, "/", http.StatusSeeOther) - //}) - mux.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./static/auth.html") - }) - mux.HandleFunc("/config", auth.RequireSession(func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./static/config.html") - })) - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./static/index.html") - }) - - // account initialisation routes - mux.HandleFunc("/api/signup", auth.Signup) - mux.HandleFunc("/api/login", auth.Login) - mux.HandleFunc("/file/gateway", auth.RequireSession(handler.Gateway)) - - // config routes - mux.HandleFunc("/file/upload", handler.HandleNewConfig) - mux.HandleFunc("/file/findings", handler.RecvFindings) - mux.HandleFunc("/file/retrieve", handler.Findings) - mux.HandleFunc("/file/accept", handler.HandleAcceptChanges) - - err := http.ListenAndServe(":81", mux) - if err != nil { - slog.Error("could not start management plane", "error", err) - } -} +package main + +import ( + "fyp-api-gateway/management/auth" + "fyp-api-gateway/management/handler" + "log/slog" + "net/http" +) + +func main() { + mux := http.NewServeMux() + + // frontend routes + mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) + //mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // http.Redirect(w, r, "/", http.StatusSeeOther) + //}) + mux.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/auth.html") + }) + mux.HandleFunc("/config", auth.RequireSession(func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/config.html") + })) + mux.HandleFunc("/docs", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/docs.html") + }) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/index.html") + }) + + // account initialisation routes + mux.HandleFunc("/api/signup", auth.Signup) + mux.HandleFunc("/api/login", auth.Login) + mux.HandleFunc("/file/gateway", auth.RequireSession(handler.Gateway)) + + // config routes + mux.HandleFunc("/file/upload", handler.HandleNewConfig) + mux.HandleFunc("/file/findings", handler.RecvFindings) + mux.HandleFunc("/file/retrieve", handler.Findings) + mux.HandleFunc("/file/accept", handler.HandleAcceptChanges) + + err := http.ListenAndServe(":81", mux) + if err != nil { + slog.Error("could not start management plane", "error", err) + } +} diff --git a/management/static/auth.html b/management/static/auth.html index 7c4f0d5..8db10e2 100644 --- a/management/static/auth.html +++ b/management/static/auth.html @@ -1,7 +1,7 @@ - Login + API Gateway - Login @@ -12,6 +12,7 @@ Signup/Login Config File Dashboard + Docs diff --git a/management/static/config.html b/management/static/config.html index 211399a..c38601e 100644 --- a/management/static/config.html +++ b/management/static/config.html @@ -1,7 +1,7 @@ - API Gateway Management + API Gateway - Config @@ -12,6 +12,7 @@ Signup/Login Config File Dashboard + Docs diff --git a/management/static/docs.html b/management/static/docs.html new file mode 100644 index 0000000..0b789db --- /dev/null +++ b/management/static/docs.html @@ -0,0 +1,59 @@ + + + + API Gateway + + + + +
+ +
+ +
+

Docs

+

The API Gateway for Micro-services is a system that allows developers of distributed systems to aggregate their micro-service + API endpoints under a single point of entry. It facilitates consistent application of security policies across groups of endpoints, + routes traffic to the correct API endpoints, and gives detailed feedback on the behaviour of changes to a developers gateway + configuration. + + The gateway provides a suite of core features, including request routing, Basic Auth authentication, rate limiting, logging and + observability through a Grafana dashboard. A management UI was developed to allow developers to configure routes and policies + associated with their micro-services. A key difference between this API Gateway and existing solutions is how the gateway handles + errors using semantics analysis. Changes are analysed by the system and presented to the user as feedback on how the changes are + expected to affect the system, as well as what errors are likely to occur. +

+ +

Gateway Config File

+ + connections: + routes: + - path: + url: + rate-limit: + zone: + rate: + auth: + +

Below is an explanation of each part of the gateway configuration file

+ +
+ + \ No newline at end of file diff --git a/management/static/index.html b/management/static/index.html index 677b794..40fba28 100644 --- a/management/static/index.html +++ b/management/static/index.html @@ -12,6 +12,7 @@ Signup/Login Config File Dashboard + Docs diff --git a/management/static/styles.css b/management/static/styles.css index 0eabd58..76f5de4 100644 --- a/management/static/styles.css +++ b/management/static/styles.css @@ -432,3 +432,42 @@ aside section:nth-child(3) { .modal-list li.error::before { content: '✕'; } .modal-list li.warning { background: var(--warn-bg); color: var(--warn); } .modal-list li.warning::before { content: '!'; font-weight: 700; } + +/* Docs */ +#docs h1{ + font-size: 3em; + margin-bottom: 0.5em; +} + +#docs p{ + margin-bottom: 2em; +} + +code{ + font-family: 'Courier New', monospace; + font-size: 0.875em; + background: #f5f5f5; + padding: 0.1em 0.3em; + border-radius: 2px; +} + +pre, code:not(li code) { + display: block; + background: #f5f5f5; + padding: 1rem 1.25rem; + margin: 1rem 0; + white-space: pre; + overflow-x: auto; + line-height: 1.6; + border-left: 3px solid #ddd; +} + +ul { + padding-left: 1.5rem; + margin: 0 0 1.25rem; +} + +li { + margin-bottom: 0.4rem; + color: #333; +} \ No newline at end of file diff --git a/src/config/db.go b/src/config/db.go index 6ae1845..53eeeb6 100644 --- a/src/config/db.go +++ b/src/config/db.go @@ -1,326 +1,326 @@ -package config - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "fyp-api-gateway/src/utils" - "log/slog" - "net/http" - "os" - "time" - - uuid "github.com/google/uuid" - _ "github.com/jackc/pgx/v5/stdlib" - "golang.org/x/crypto/bcrypt" -) - -type Database struct { - Conn *sql.DB -} - -type Server struct { - DB *Database -} - -type LoginInfo struct { - Name string `json:"name"` - Password string `json:"password"` -} - -func NewDatabase(dsn string) (*Database, error) { - db, err := sql.Open("pgx", dsn) - if err != nil { - slog.Error("error opening database connection:", "error", err) - return nil, err - } - - db.SetMaxOpenConns(25) - db.SetMaxIdleConns(25) - db.SetConnMaxLifetime(5 * time.Minute) - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - if err = db.PingContext(ctx); err != nil { - return nil, err - } - - return &Database{Conn: db}, nil -} - -func (d *Database) StartDB(path string) error { - content, err := os.ReadFile(path) - if err != nil { - slog.Error("error reading database file", "error", err) - return err - } - - if _, err = d.Conn.Exec(string(content)); err != nil { - slog.Error("error executing database statement", "error", err) - return err - } - - return nil -} - -func (s *Server) Signup(w http.ResponseWriter, r *http.Request) { - slog.Info("attempting to sign up new user...") - loginInfo := &LoginInfo{} - - if r.Method != http.MethodPost { - slog.Error("invalid method", "method", r.Method) - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { - slog.Error("error decoding loginInfo", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - var id string - err := s.DB.Conn.QueryRow( - "SELECT id FROM users WHERE username = $1", - loginInfo.Name, - ).Scan(&id) - - if err == nil || id != "" { - slog.Error("error querying user", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - hashed_password, err := bcrypt.GenerateFromPassword([]byte(loginInfo.Password), 14) - _, err = s.DB.Conn.Exec(` - INSERT INTO users (username, password, config_yaml) - VALUES ($1, $2, $3);`, - loginInfo.Name, string(hashed_password), utils.DefaultConfigContent, - ) - if err != nil { - slog.Error("error inserting user", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - err = InitUserNGINX(loginInfo.Name) - if err != nil { - slog.Error("error initializing user", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) -} - -/* -Receive the login info from the management plane and decode it -Check the password used is the same as the one in the database, also check the username is there -If the user has no session, create one and send it back, otherwise return the existing session -*/ -func (s *Server) VerifyLoginInfo(w http.ResponseWriter, r *http.Request) { - slog.Info("validating login information") - loginInfo := &LoginInfo{} - - if r.Method != http.MethodPost { - slog.Error("invalid method", "method", r.Method) - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { - slog.Error("error decoding loginInfo", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - var storedHash string - err := s.DB.Conn.QueryRow( - "SELECT password FROM users WHERE username = $1", - loginInfo.Name, - ).Scan(&storedHash) - - if errors.Is(err, sql.ErrNoRows) { - slog.Error("user not found", "username", loginInfo.Name) - http.Error(w, "invalid credentials", http.StatusUnauthorized) - return - } - - err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(loginInfo.Password)) - if err != nil { - slog.Error("error verifying loginInfo", "error", err) - http.Error(w, "invalid credentials", http.StatusUnauthorized) - return - } - - // check is the user has a session already - var sessionId string - sessionId, isSession := s.sessionExists(loginInfo.Name) - - if !isSession { - sessionId, err = s.createSession(loginInfo.Name) - if err != nil { - slog.Error("error creating session", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "sessionId": sessionId, - }) -} - -func (s *Server) sessionExists(username string) (string, bool) { - var sessionId string - err := s.DB.Conn.QueryRow( - "SELECT id FROM sessions WHERE username=$1", - username, - ).Scan(&sessionId) - - if err != nil { - return "", false - } - return sessionId, true -} - -func (s *Server) createSession(name string) (string, error) { - sessionId := uuid.New().String() - expires := time.Now().Add(24 * time.Hour) - - _, err := s.DB.Conn.Exec( - "INSERT INTO sessions(id, username, expires) VALUES ($1, $2, $3)", - sessionId, name, expires, - ) - - if err != nil { - slog.Error("error creating session", "error", err) - return "", err - } - - return sessionId, nil -} - -func (s *Server) ValidateSession(w http.ResponseWriter, r *http.Request) { - slog.Info("validating user session") - sessionId := r.Header.Get("X-Session-ID") - - var username string - err := s.DB.Conn.QueryRow( - "SELECT username FROM sessions WHERE id=$1 AND expires > NOW()", - sessionId, - ).Scan(&username) - - if err != nil { - slog.Error("error querying session", "error", err) - http.Error(w, "unauthorized", http.StatusUnauthorized) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (s *Server) UserConfig(w http.ResponseWriter, r *http.Request) { - slog.Info("received request for user config") - - cookie, err := r.Cookie("session") - if err != nil { - slog.Error("failed getting session id", "error", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - sessionId := cookie.Value - - var gatewayCfg string - err = s.DB.Conn.QueryRow(` - SELECT u.config_yaml - FROM users AS u - JOIN sessions AS s ON u.username = s.username - WHERE s.id = $1 AND s.expires > NOW()`, - sessionId).Scan(&gatewayCfg) - - if err != nil { - slog.Error("error querying session", "error", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - w.Header().Set("Content-Type", "text/yaml") - w.WriteHeader(http.StatusOK) - _, err = w.Write([]byte(gatewayCfg)) - if err != nil { - slog.Error("error writing response", "error", err) - return - } -} - -func RetrieveUserBySessionId(sessionId string) string { - dsn := os.Getenv("DATABASE_URL") - - db, err := sql.Open("pgx", dsn) - if err != nil { - slog.Error("error opening database connection:", "error", err) - return "" - } - - var username string - err = db.QueryRow(` - SELECT username - FROM sessions - WHERE id = $1`, - sessionId).Scan(&username) - - return username -} - -func RetrieveUserConfig(username string) string { - dsn := os.Getenv("DATABASE_URL") - - db, err := sql.Open("pgx", dsn) - if err != nil { - slog.Error("error opening database connection:", "error", err) - return "" - } - - var gatewayCfg string - err = db.QueryRow(` - SELECT config_yaml - FROM users - WHERE username = $1`, - username, - ).Scan(&gatewayCfg) - - return gatewayCfg -} - -func InsertNewConfig(sessionId, gatewayCfg string) error { - dsn := os.Getenv("DATABASE_URL") - - db, err := sql.Open("pgx", dsn) - if err != nil { - return err - } - - _, err = db.Exec(` - UPDATE users AS u - SET config_yaml = $1 - FROM sessions AS s - WHERE s.id = $2 - AND s.expires > NOW() - AND u.username = s.username`, - gatewayCfg, sessionId, - ) - slog.Info("inserted new config to database") - - if err != nil { - return err - } - - return nil -} +package config + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fyp-api-gateway/src/utils" + "log/slog" + "net/http" + "os" + "time" + + uuid "github.com/google/uuid" + _ "github.com/jackc/pgx/v5/stdlib" + "golang.org/x/crypto/bcrypt" +) + +type Database struct { + Conn *sql.DB +} + +type Server struct { + DB *Database +} + +type LoginInfo struct { + Name string `json:"name"` + Password string `json:"password"` +} + +func NewDatabase(dsn string) (*Database, error) { + db, err := sql.Open("pgx", dsn) + if err != nil { + slog.Error("error opening database connection:", "error", err) + return nil, err + } + + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(25) + db.SetConnMaxLifetime(5 * time.Minute) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err = db.PingContext(ctx); err != nil { + return nil, err + } + + return &Database{Conn: db}, nil +} + +func (d *Database) StartDB(path string) error { + content, err := os.ReadFile(path) + if err != nil { + slog.Error("error reading database file", "error", err) + return err + } + + if _, err = d.Conn.Exec(string(content)); err != nil { + slog.Error("error executing database statement", "error", err) + return err + } + + return nil +} + +func (s *Server) Signup(w http.ResponseWriter, r *http.Request) { + slog.Info("attempting to sign up new user...") + loginInfo := &LoginInfo{} + + if r.Method != http.MethodPost { + slog.Error("invalid method", "method", r.Method) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { + slog.Error("error decoding loginInfo", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var id string + err := s.DB.Conn.QueryRow( + "SELECT id FROM users WHERE username = $1", + loginInfo.Name, + ).Scan(&id) + + if err == nil || id != "" { + slog.Error("error querying user", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + hashed_password, err := bcrypt.GenerateFromPassword([]byte(loginInfo.Password), 14) + _, err = s.DB.Conn.Exec(` + INSERT INTO users (username, password, config_yaml) + VALUES ($1, $2, $3);`, + loginInfo.Name, string(hashed_password), utils.DefaultConfigContent, + ) + if err != nil { + slog.Error("error inserting user", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + err = InitUserNGINX(loginInfo.Name) + if err != nil { + slog.Error("error initializing user", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +/* +Receive the login info from the management plane and decode it +Check the password used is the same as the one in the database, also check the username is there +If the user has no session, create one and send it back, otherwise return the existing session +*/ +func (s *Server) VerifyLoginInfo(w http.ResponseWriter, r *http.Request) { + slog.Info("validating login information") + loginInfo := &LoginInfo{} + + if r.Method != http.MethodPost { + slog.Error("invalid method", "method", r.Method) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { + slog.Error("error decoding loginInfo", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var storedHash string + err := s.DB.Conn.QueryRow( + "SELECT password FROM users WHERE username = $1", + loginInfo.Name, + ).Scan(&storedHash) + + if errors.Is(err, sql.ErrNoRows) { + slog.Error("user not found", "username", loginInfo.Name) + http.Error(w, "invalid credentials", http.StatusUnauthorized) + return + } + + err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(loginInfo.Password)) + if err != nil { + slog.Error("error verifying loginInfo", "error", err) + http.Error(w, "invalid credentials", http.StatusUnauthorized) + return + } + + // check is the user has a session already + var sessionId string + sessionId, isSession := s.sessionExists(loginInfo.Name) + + if !isSession { + sessionId, err = s.createSession(loginInfo.Name) + if err != nil { + slog.Error("error creating session", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "sessionId": sessionId, + }) +} + +func (s *Server) sessionExists(username string) (string, bool) { + var sessionId string + err := s.DB.Conn.QueryRow( + "SELECT id FROM sessions WHERE username=$1", + username, + ).Scan(&sessionId) + + if err != nil { + return "", false + } + return sessionId, true +} + +func (s *Server) createSession(name string) (string, error) { + sessionId := uuid.New().String() + expires := time.Now().Add(24 * time.Hour) + + _, err := s.DB.Conn.Exec( + "INSERT INTO sessions(id, username, expires) VALUES ($1, $2, $3)", + sessionId, name, expires, + ) + + if err != nil { + slog.Error("error creating session", "error", err) + return "", err + } + + return sessionId, nil +} + +func (s *Server) ValidateSession(w http.ResponseWriter, r *http.Request) { + slog.Info("validating user session") + sessionId := r.Header.Get("X-Session-ID") + + var username string + err := s.DB.Conn.QueryRow( + "SELECT username FROM sessions WHERE id=$1 AND expires > NOW()", + sessionId, + ).Scan(&username) + + if err != nil { + slog.Error("error querying session", "error", err) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + w.WriteHeader(http.StatusOK) +} + +func (s *Server) UserConfig(w http.ResponseWriter, r *http.Request) { + slog.Info("received request for user config") + + cookie, err := r.Cookie("session") + if err != nil { + slog.Error("failed getting session id", "error", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + sessionId := cookie.Value + + var gatewayCfg string + err = s.DB.Conn.QueryRow(` + SELECT u.config_yaml + FROM users AS u + JOIN sessions AS s ON u.username = s.username + WHERE s.id = $1 AND s.expires > NOW()`, + sessionId).Scan(&gatewayCfg) + + if err != nil { + slog.Error("error querying session", "error", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "text/yaml") + w.WriteHeader(http.StatusOK) + _, err = w.Write([]byte(gatewayCfg)) + if err != nil { + slog.Error("error writing response", "error", err) + return + } +} + +func RetrieveUserBySessionId(sessionId string) string { + dsn := os.Getenv("DATABASE_URL") + + db, err := sql.Open("pgx", dsn) + if err != nil { + slog.Error("error opening database connection:", "error", err) + return "" + } + + var username string + err = db.QueryRow(` + SELECT username + FROM sessions + WHERE id = $1`, + sessionId).Scan(&username) + + return username +} + +func RetrieveUserConfig(username string) string { + dsn := os.Getenv("DATABASE_URL") + + db, err := sql.Open("pgx", dsn) + if err != nil { + slog.Error("error opening database connection:", "error", err) + return "" + } + + var gatewayCfg string + err = db.QueryRow(` + SELECT config_yaml + FROM users + WHERE username = $1`, + username, + ).Scan(&gatewayCfg) + + return gatewayCfg +} + +func InsertNewConfig(sessionId, gatewayCfg string) error { + dsn := os.Getenv("DATABASE_URL") + + db, err := sql.Open("pgx", dsn) + if err != nil { + return err + } + + _, err = db.Exec(` + UPDATE users AS u + SET config_yaml = $1 + FROM sessions AS s + WHERE s.id = $2 + AND s.expires > NOW() + AND u.username = s.username`, + gatewayCfg, sessionId, + ) + slog.Info("inserted new config to database") + + if err != nil { + return err + } + + return nil +}