| Column 1 | -Column 2 | -
|---|---|
| Data 1 | -Data 2 | -
| Data A | -Data B | -
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 = `
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 := "Connect and query your databases effortlessly.
-Connect and query your databases effortlessly.
+| Column 1 | -Column 2 | -
|---|---|
| Data 1 | -Data 2 | -
| Data A | -Data B | -