From 5cbe0043185cdcdbf9407c21b7cc7019aa5a5310 Mon Sep 17 00:00:00 2001 From: Azpect3120 <104033825+Azpect3120@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:04:02 -0700 Subject: [PATCH] FEAT: Sessions are complete! Queries are also complete... Needs some bug testing and such but the queries are running and displaying! --- go.mod | 2 +- go.sum | 2 + internal/database/connect.go | 43 +++ internal/database/create.go | 27 +- internal/database/query.go | 112 ++++++++ internal/http/router.go | 45 ++- internal/http/server.go | 22 +- internal/http/session.go | 18 -- internal/templates/connections.go | 29 ++ internal/templates/results.go | 63 +++++ tools/styles/compile.sh | 2 +- web/static/styles/main.css | 23 +- web/templates/index.html | 438 +++++++++++++++--------------- 13 files changed, 560 insertions(+), 266 deletions(-) create mode 100644 internal/database/connect.go create mode 100644 internal/database/query.go delete mode 100644 internal/http/session.go create mode 100644 internal/templates/connections.go create mode 100644 internal/templates/results.go diff --git a/go.mod b/go.mod index 3ad990b..f362dfb 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/goccy/go-json v0.10.2 // indirect github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/gorilla/sessions v1.2.2 // indirect + github.com/gorilla/sessions v1.3.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/kr/text v0.2.0 // indirect diff --git a/go.sum b/go.sum index 8ef1e80..59a7f7f 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= +github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= diff --git a/internal/database/connect.go b/internal/database/connect.go new file mode 100644 index 0000000..f08c1c7 --- /dev/null +++ b/internal/database/connect.go @@ -0,0 +1,43 @@ +package database + +import ( + "encoding/json" + "fmt" + + "github.com/Azpect3120/Web-Database-Viewer/internal/templates" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +func ChangeConnection(c *gin.Context) { + conn := c.PostForm("connected-database") + + // Do something to change the connection + + session := sessions.Default(c) + conn_bytes, ok := session.Get("connections").([]byte) + if !ok { + c.String(200, templates.ConnectionsList(nil, "")) + return + } + + var connections map[string]string + if err := json.Unmarshal(conn_bytes, &connections); err != nil { + c.String(200, templates.ConnectionsList(nil, "")) + fmt.Println(err) + return + } + + var name string + for n, c := range connections { + if c == conn { + name = n + break + } + } + + session.Set("current", name) + session.Save() + + c.String(200, templates.ConnectionsList(connections, name)) +} diff --git a/internal/database/create.go b/internal/database/create.go index b4a3f15..dce0286 100644 --- a/internal/database/create.go +++ b/internal/database/create.go @@ -1,31 +1,46 @@ package database import ( + "encoding/json" "fmt" + "github.com/Azpect3120/Web-Database-Viewer/internal/templates" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" ) +// Create a new connection to a database and store the details +// in the session. func CreateConnection(c *gin.Context) { - session := sessions.Default(c) var ( url string = c.PostForm("db-url") database string = c.PostForm("db-database") ) - connections, ok := session.Get("connections").(map[string]string) + session := sessions.Default(c) + + var connections map[string]string + + session_bytes, ok := session.Get("connections").([]byte) if !ok { - fmt.Println("Creating new connections map /internal/database/create.go:19") connections = make(map[string]string) + } else { + if err := json.Unmarshal(session_bytes, &connections); err != nil { + fmt.Println(err) + } } connections[database] = url - session.Set("connections", connections) - err := session.Save() + conn_bytes, err := json.Marshal(connections) if err != nil { - fmt.Println("Failed to save session /internal/database/create.go:29") fmt.Println(err) } + + session.Set("connections", []byte(conn_bytes)) + session.Set("current", database) + session.Save() + + html := templates.ConnectionsList(connections, database) + c.String(200, html) } diff --git a/internal/database/query.go b/internal/database/query.go new file mode 100644 index 0000000..efddd33 --- /dev/null +++ b/internal/database/query.go @@ -0,0 +1,112 @@ +package database + +import ( + "database/sql" + "encoding/json" + "fmt" + + "github.com/Azpect3120/Web-Database-Viewer/internal/templates" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + + _ "github.com/lib/pq" +) + +func QueryCurrent(c *gin.Context) { + query := c.PostForm("sql") + conn := getConnection(c) + + cols, data, err := queryConnection(query, conn) + if err != nil { + c.String(200, templates.ErrorQueryResults(err)) + return + } + + c.String(200, templates.QueryResults(cols, data)) +} + +func queryConnection(query, url string) ([]string, []map[string]interface{}, error) { + db, err := sql.Open("postgres", url) + if err != nil { + return []string{}, []map[string]interface{}{}, err + } + + rows, err := db.Query(query) + if err != nil { + return []string{}, []map[string]interface{}{}, err + } + defer rows.Close() + + cols, err := rows.Columns() + if err != nil { + return []string{}, []map[string]interface{}{}, err + } + + // Create values pointer and value pointer slices + // We need to use pointers because the Scan function + // requires a pointer to the value. However, there + // is no simple way to create an array of pointers. + values := make([]interface{}, len(cols)) + valuePtrs := make([]interface{}, len(cols)) + + for i := range cols { + valuePtrs[i] = &values[i] + } + + // Final data structure to store the results + // An array of maps, where each map is a row + // and the keys are the column names. + var result []map[string]interface{} + + for rows.Next() { + // Scan the result into the value pointers + err := rows.Scan(valuePtrs...) + if err != nil { + fmt.Println(err) + } + + // Create a map to store the column data + row := make(map[string]interface{}) + for i, col := range cols { + var v interface{} + val := values[i] + + // Convert the value to a string representation + // I can't see when this would fail and return + // something that isn't a string, but it's better + // to be safe than sorry. However, this might be + // hard to handle in the frontend. + b, ok := val.([]byte) + if ok { + v = string(b) + } else { + v = val + } + + row[col] = v + } + + // Append the row to the result set + result = append(result, row) + } + + return cols, result, nil +} + +func getConnection(c *gin.Context) (url string) { + session := sessions.Default(c) + conn_bytes, ok := session.Get("connections").([]byte) + curr, ok := session.Get("current").(string) + if !ok { + fmt.Println("No current connection") + return "" + } + + var connections map[string]string + if err := json.Unmarshal(conn_bytes, &connections); err != nil { + fmt.Println(err) + return "" + } + + return connections[curr] +} diff --git a/internal/http/router.go b/internal/http/router.go index d85675b..0052f41 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -1,9 +1,12 @@ package http import ( + "encoding/json" + "fmt" "time" "github.com/Azpect3120/Web-Database-Viewer/internal/database" + "github.com/Azpect3120/Web-Database-Viewer/internal/templates" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" ) @@ -21,18 +24,48 @@ func populate(web, api *gin.RouterGroup) { }) }) - api.POST("/query", func(c *gin.Context) { - sql := c.PostForm("sql") - c.JSON(200, gin.H{"sql": sql}) - }) + api.POST("/query", database.QueryCurrent) api.POST("/connections/test", database.TestConnectionURL) api.POST("/connections", database.CreateConnection) api.GET("/connections", func(c *gin.Context) { session := sessions.Default(c) - connections, ok := session.Get("connections").(map[string]string) + connections_bytes, ok := session.Get("connections").([]byte) + if !ok { + c.JSON(200, gin.H{"connections": make(map[string]string), "current": "", "count": 0, "time": time.Now().String(), "status": 200}) + return + } + current := session.Get("current") - c.JSON(200, gin.H{"okay": ok, "connections": connections}) + var connections map[string]string + if err := json.Unmarshal(connections_bytes, &connections); err != nil { + fmt.Println(err) + } + c.JSON(200, gin.H{ + "connections": connections, + "current": current, + "count": len(connections), + "time": time.Now().String(), + "status": 200, + }) }) + web.GET("/connections", func(c *gin.Context) { + session := sessions.Default(c) + connections_bytes, conn_ok := session.Get("connections").([]byte) + current, curr_ok := session.Get("current").(string) + + var connections map[string]string + if conn_ok || curr_ok { + if err := json.Unmarshal(connections_bytes, &connections); err != nil { + fmt.Println(err) + } + } else { + connections = make(map[string]string) + } + + html := templates.ConnectionsList(connections, current) + c.String(200, html) + }) + api.POST("/connections/connect", database.ChangeConnection) } diff --git a/internal/http/server.go b/internal/http/server.go index ddee301..a4c756d 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -1,9 +1,13 @@ package http import ( + "encoding/gob" "fmt" + "net/http" "github.com/gin-contrib/cors" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" ) @@ -22,18 +26,28 @@ func New(port string) *Server { Router: gin.Default(), config: cors.DefaultConfig(), } - server.config.AllowOrigins = []string{"*"} server.Router.Use(cors.New(server.config)) + // Session configuration + gob.Register([]byte{}) + gob.Register(map[string]string{}) + store := cookie.NewStore([]byte("secret")) + store.Options(sessions.Options{ + Path: "/", + Domain: "", + MaxAge: 86400 * 7, // 7 days + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) + server.Router.Use(sessions.Sessions("mysession", store)) + return server } // Setup the server with the necessary configurations func (s *Server) Setup() *Server { - // This has to be first ALWAYS for some stupid reason :| - s.initSession() - v1 := s.Router.Group("/v1") web_g := v1.Group("/web") api_g := v1.Group("/api") diff --git a/internal/http/session.go b/internal/http/session.go deleted file mode 100644 index 5c4197b..0000000 --- a/internal/http/session.go +++ /dev/null @@ -1,18 +0,0 @@ -package http - -import ( - "encoding/gob" - - "github.com/gin-contrib/sessions" - "github.com/gin-contrib/sessions/cookie" -) - -const SESSION_SECRET string = "lqMVy04h8KWll6lCU9XCDbfUsBIjD1eG" - -// Initialize the session -// Session is a map of "db name": "connection url" -func (s *Server) initSession() { - gob.Register(&map[string]string{}) - store := cookie.NewStore([]byte(SESSION_SECRET)) - s.Router.Use(sessions.Sessions("mysession", store)) -} diff --git a/internal/templates/connections.go b/internal/templates/connections.go new file mode 100644 index 0000000..760c6b5 --- /dev/null +++ b/internal/templates/connections.go @@ -0,0 +1,29 @@ +package templates + +import "fmt" + +const LIST_OPEN string = `` + +// Generate a list of connections to display in the drop-down. +// Current connection will be toggled as selected +func ConnectionsList(connections map[string]string, current string) string { + var html string = LIST_OPEN + + if len(connections) == 0 || connections == nil { + html += fmt.Sprintf(LIST_ITEM, "", " selected", "No connections") + return html + LIST_CLOSE + } + + for name, url := range connections { + if name == current { + html += fmt.Sprintf(LIST_ITEM, url, " selected", name) + } else { + html += fmt.Sprintf(LIST_ITEM, url, "", name) + } + } + + html += LIST_CLOSE + return html +} diff --git a/internal/templates/results.go b/internal/templates/results.go new file mode 100644 index 0000000..23d3ea6 --- /dev/null +++ b/internal/templates/results.go @@ -0,0 +1,63 @@ +package templates + +import "fmt" + +// Table wrapper definitions +const table_open string = `` +const table_close string = `
` + +// Header definitions +const table_head_open string = `` +const table_head_close string = `` +const table_head_row string = `%s` + +// Body definitions +const table_body_open string = `` +const table_body_close string = `` +const table_body_row string = `%v` + +// Error message +const query_error_message string = `

Query Error: %s

` +const query_error_message_blank string = `` + +func ErrorQueryResults(e error) string { + return table_open + table_close + fmt.Sprintf(query_error_message, e.Error()) +} + +func QueryResults(cols []string, rows []map[string]interface{}) string { + head := generateHead(cols) + + body := table_body_open + for _, row := range rows { + body += generateRow(cols, row) + } + + body += table_body_close + + return table_open + head + body + table_close + query_error_message_blank +} + +// Generate the tables head row +func generateHead(cols []string) string { + html := table_head_open + + for _, col := range cols { + html += fmt.Sprintf(table_head_row, col) + } + + return html + table_head_close +} + +func generateRow(cols []string, data map[string]interface{}) string { + row := "" + + for _, col := range cols { + str, ok := data[col].(string) + if !ok { + str = "NULL" + } + row += fmt.Sprintf(table_body_row, str) + } + + return row + "" +} diff --git a/tools/styles/compile.sh b/tools/styles/compile.sh index b9ebb48..90a7395 100755 --- a/tools/styles/compile.sh +++ b/tools/styles/compile.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -./tools/tailwind -i ./public/assets/styles/config.css -o ./public/assets/styles/main.css --minify +./tools/tailwind -i ./web/static/styles/config.css -o ./web/static/styles/main.css --minify diff --git a/web/static/styles/main.css b/web/static/styles/main.css index 3fa7037..4d0dc51 100644 --- a/web/static/styles/main.css +++ b/web/static/styles/main.css @@ -829,6 +829,11 @@ video { background-color: rgb(229 231 235 / var(--tw-bg-opacity)); } +.bg-gray-400 { + --tw-bg-opacity: 1; + background-color: rgb(156 163 175 / var(--tw-bg-opacity)); +} + .bg-gray-50 { --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity)); @@ -839,11 +844,6 @@ video { background-color: rgb(75 85 99 / var(--tw-bg-opacity)); } -.bg-green-500 { - --tw-bg-opacity: 1; - background-color: rgb(34 197 94 / var(--tw-bg-opacity)); -} - .bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); @@ -900,6 +900,11 @@ video { padding-bottom: 1rem; } +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + .pb-2 { padding-bottom: 0.5rem; } @@ -937,6 +942,10 @@ video { font-weight: 700; } +.font-light { + font-weight: 300; +} + .font-medium { font-weight: 500; } @@ -964,9 +973,9 @@ video { color: rgb(55 65 81 / var(--tw-text-opacity)); } -.text-green-500 { +.text-red-500 { --tw-text-opacity: 1; - color: rgb(34 197 94 / var(--tw-text-opacity)); + color: rgb(239 68 68 / var(--tw-text-opacity)); } .text-white { diff --git a/web/templates/index.html b/web/templates/index.html index 0c21191..b0abaed 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1,244 +1,236 @@ - - - - Database Query Tool - - - - - -
- -
-
-

Database Query Tool

-

Connect and query your databases effortlessly.

-
+ + + + Database Query Tool + + + + + + + +
+ +
+
+

Database Query Tool

+

Connect and query your databases effortlessly.

+
- - +
+ + +
-
- -
-
-

Database 1 Tables

-
-
-
    -
  • - -
      -
    • - -
    • -
    • - -
    • -
    -
  • -
  • - -
      -
    • - -
    • -
    • - -
    • -
    -
  • -
-
+
+ +
+
+

Database 1 Tables

- - -
-
- -
- - -
- - -
- - - - - - - - - - - - - - - - - -
Column 1Column 2
Data 1Data 2
Data AData B
-
-
-
-
-
- - -