diff --git a/.gitignore b/.gitignore index f042913..6d67f63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.env !.env-template bin/ +.vscode diff --git a/API-REFERENCE.md b/API-REFERENCE.md new file mode 100644 index 0000000..5d251a6 --- /dev/null +++ b/API-REFERENCE.md @@ -0,0 +1,14 @@ +# API Reference Documentation +This guide hopefully will explain how the API works. Any method undocumented, you can assume it will return a 405 error. If you go to a page that doesn't exist, it will return a 404 error. + +## Index + * / "GET" -> A page designed for web users to go to in order to learn about creating hooks + +## Hooks + * /hook/ "GET" -> Returns a list of all hooks in json form + * /hook/ "GET" -> Does whatever your hook is configured to do on a GET. If nothing configured, return the hook in json form. If it doesn't exist, return 404 + * /hook/ "POST" -> Does whatever your hook is configured to do on a GET. If nothing configured, return the hook in json form. If it doesn't exist, return 404 + * /hook/ "PUT" -> Submit the hook parameters and this will create the hook if it doesn't exist, or update it if it does. (CURRENTLY NOT IMPLEMENTED) + * /hook/ "DELETE" -> Delete the hook if it exists, does nothing if it doesn't + * /hook/ "" -> Delete the hook if it exists, do nothing if it doesn't + * /hook//configure "GET" -> Gives the web friendly configuration page for your hook (CURRENTLY NOT IMPLEMENTED) \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4f392d4..89c2c2c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,9 +2,8 @@ FROM golang:latest LABEL maintainer="Ryan Stewart " # USER www-data WORKDIR /srv/website -EXPOSE 5000 -COPY .env *sql go.mod go.sum ./ +EXPOSE 3000 +RUN go get "github.com/codegangsta/gin" +COPY go.mod go.sum ./ RUN go mod download -COPY src/* src/ -RUN go build -o main src/* -CMD ./main \ No newline at end of file +CMD gin -t src -b bin -a 5000 -i run main.go \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 6a4a240..2019629 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -# Use root/example as user/password credentials version: '3.7' services: @@ -16,8 +15,11 @@ services: restart: unless-stopped env_file: .env ports: - - "5000:5000" + - "3000:3000" depends_on: - "db" volumes: - - ./html:/srv/website/html \ No newline at end of file + - ./html:/srv/website/html + - ./src:/srv/website/src + - ./sql:/srv/website/sql + - ./.env:/srv/website/.env \ No newline at end of file diff --git a/go.mod b/go.mod index e5a2ffd..47bb608 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.14 require ( github.com/go-sql-driver/mysql v1.5.0 - github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/errors v0.9.1 ) diff --git a/html/css/index.css b/html/css/index.css index 50231ae..e5c59d6 100644 --- a/html/css/index.css +++ b/html/css/index.css @@ -1,16 +1,67 @@ -/* Style page content */ +html, body { + margin: 0; + padding: 0; + height: 100%; + background-color: #252525; +} .main { - margin-left: 160px; /* Same as the width of the sidebar */ - padding: 0px 20px; + margin: 0 12.5vw; + padding: 2vh 2vw; + min-height: 100%; + display: flex; + flex: 1 +} + +.main h1 { + font-weight: bold; + margin-bottom: 2vh; } .main h2 { - margin-top: 5vh; - margin-bottom: 2vh; + font-size: 1.25em; + font-weight: bold; + margin-top: 3vh; + margin-bottom: 2vh; +} + +.main button { + padding: 0; + height: 50px; + min-width: 90px; + font-size: 1.2rem; + text-transform: uppercase; + color: white; + border: none; +} + +.delete-button { + background-color: red; +} +.modify-button { + background-color: blue; +} +.create-button { + background-color: green; +} +.list-group-item { + padding: 0; + border: 1px solid; +} + +.create, .hook { + display: flex; +} + +.create input { + width: 100%; + margin: 0; } -/* Debug */ -.center { - margin-left: 160px; /* Same as the width of the sidebar */ - position: absolute center; +.hook p { + margin: 0; + padding: 0 20px; + height: 100%; + width: 100%; + display: flex; + align-items: center; } \ No newline at end of file diff --git a/html/css/sidenav.css b/html/css/sidenav.css deleted file mode 100644 index cc9fa27..0000000 --- a/html/css/sidenav.css +++ /dev/null @@ -1,32 +0,0 @@ - /* The sidebar menu */ -.sidenav { - height: 100%; /* Full-height: remove this if you want "auto" height */ - width: 160px; /* Set the width of the sidebar */ - position: fixed; /* Fixed Sidebar (stay in place on scroll) */ - z-index: 1; /* Stay on top */ - top: 0; /* Stay at the top */ - left: 0; - background-color: #111; /* Black */ - overflow-x: hidden; /* Disable horizontal scroll */ - padding-top: 20px; -} - -/* The navigation menu links */ -.sidenav a { - padding: 6px 8px 6px 16px; - text-decoration: none; - font-size: 25px; - color: #818181; - display: block; -} - -/* When you mouse over the navigation links, change their color */ -.sidenav a:hover { - color: #f1f1f1; -} - -/* On smaller screens, where height is less than 450px, change the style of the sidebar (less padding and a smaller font size) */ -@media screen and (max-height: 450px) { - .sidenav {padding-top: 15px;} - .sidenav a {font-size: 18px;} -} diff --git a/html/imgs/something.png b/html/imgs/something.png new file mode 100644 index 0000000..e69de29 diff --git a/html/index.html b/html/index.html index 77d99c8..09a6a8d 100644 --- a/html/index.html +++ b/html/index.html @@ -1,44 +1,54 @@ - - - - - - + + Captain Hook + + + + - -
- Create - Delete -
- -
-
- -
Testing
- - - - - + + + + + + diff --git a/html/js/index.js b/html/js/index.js new file mode 100644 index 0000000..c07b176 --- /dev/null +++ b/html/js/index.js @@ -0,0 +1,4 @@ +// document.getElementById("aStupidButton").onclick = function (event) { +// event.preventDefault(); +// // do stuff +// }; \ No newline at end of file diff --git a/sql/drop.sql b/sql/drop.sql index e3aaa7a..4d1300d 100644 --- a/sql/drop.sql +++ b/sql/drop.sql @@ -1 +1 @@ -DROP TABLE IF EXISTS HOOKS; \ No newline at end of file +DROP TABLE IF EXISTS Hooks; \ No newline at end of file diff --git a/sql/setup.sql b/sql/setup.sql index 1c820ae..5a2d265 100644 --- a/sql/setup.sql +++ b/sql/setup.sql @@ -1,4 +1,4 @@ -CREATE TABLE IF NOT EXISTS HOOKS +CREATE TABLE IF NOT EXISTS Hooks ( ID varchar(64) PRIMARY KEY ); \ No newline at end of file diff --git a/src/db.go b/src/db.go index 1759442..7243472 100644 --- a/src/db.go +++ b/src/db.go @@ -42,17 +42,16 @@ func connectDB() (*sql.DB, error) { if !exists { attemptsStr = "5" } - attempts, err := strconv.Atoi(attemptsStr) + attempts, err := strconv.Atoi(attemptsStr) if err != nil { attempts = 5 } - timeoutStr, exists := os.LookupEnv("DB_CONNECTION_TIMEOUT") if !exists { timeoutStr = "5" } - timeout, err := strconv.Atoi(timeoutStr) + timeout, err := strconv.Atoi(timeoutStr) if err != nil { timeout = 5 } @@ -61,6 +60,7 @@ func connectDB() (*sql.DB, error) { if err != nil { return db, err } + for i := 1; i <= attempts; i++ { //// DEBUG: Making sure table is good // if err := dropDB(db); err != nil { @@ -93,17 +93,14 @@ func connectDB() (*sql.DB, error) { return db, nil } -// func queryRow(db *sql.DB, name string) (*sql.Rows, error) { -// b, err := ioutil.ReadFile(name) -// if err != nil { -// return nil, err -// } -// return db.QueryRow(string(b)), nil -// } - -// func query(db *sql.DB, name string) (interface{}, error) { -// db.Qu -// } +func pingDB(db *sql.DB) { + for { + if err := db.Ping(); err != nil { + log.Println("Failed to connect to database, Connection died") + } + time.Sleep(time.Second*5) + } +} func execSQL(db *sql.DB, name string) error { b, err := ioutil.ReadFile(name) diff --git a/src/helper.go b/src/helper.go index 3071818..7abd950 100644 --- a/src/helper.go +++ b/src/helper.go @@ -2,6 +2,7 @@ package main import ( "log" + "net/http" ) // Check errors and fail if they're bad @@ -11,6 +12,17 @@ func check(err error) { } } +// Log weird errors for web stuff +func logError(w http.ResponseWriter, err error) { + if err != nil { + log.Println(err) + writeError(w, errorMessage{ + StatusCode: http.StatusInternalServerError, + Msg: err.Error(), + }) + } +} + // Check if a value exists and fail if it doesn't func checkExists(exists bool, msg string) { if !exists { diff --git a/src/hook.go b/src/hook.go index 424dcc1..38f04f5 100644 --- a/src/hook.go +++ b/src/hook.go @@ -6,39 +6,34 @@ import ( "net/http" ) - +// HookHandler is an interface that satisfies an http handler. +// Something else here type HookHandler interface { ServeHTTP(http.ResponseWriter, *http.Request) } +// Condition for a hook. It contains something to select. +// This could be a piece of json post data, a method, or http header +// The tester is the operation to do on the selector. +// For example: AND, OR, NOT, LESS THAN, GREATER THAN, EQUALS, CONTAINS type Condition struct { selector string tester string - value string -} - -type Hook struct { - Name string - params []Condition - actions []string -} - -type ByName []Hook - -func (h ByName) Len() int { - return len(h) + actions []Action } -func (h ByName) Less(i, j int) bool { - return h[i].Name < h[j].Name +// Action results from when a condition is expressed +// For example, an action could run if a selector returns TRUE when tested +// while another action could be run when FALSE is returned +type Action interface { + Run() error } -func (h ByName) Swap(i, j int) { - h[i], h[j] = h[j], h[i] -} - -func (h Hook) String() string { - return h.Name +// Hook that will be run when a user hits the appropriate URL +// with the right methods and parameters. +type Hook struct { + Name string + conditions []Condition } func (h Hook) ServeHTTP(w http.ResponseWriter, r *http.Request) { diff --git a/src/main.go b/src/main.go index 0cd4256..9b35447 100644 --- a/src/main.go +++ b/src/main.go @@ -1,12 +1,11 @@ package main import ( - "net/http" "log" + "net/http" "os" ) - // func init() { // // Loads values from .env into the system // if err := godotenv.Load(); err != nil { @@ -16,11 +15,12 @@ import ( func main() { var err error - db, err = connectDB(); + db, err = connectDB() if err != nil { log.Fatal(err) } defer db.Close() + go pingDB(db) // Get listen address and port addr, exists := os.LookupEnv("LISTEN") @@ -32,8 +32,10 @@ func main() { http.HandleFunc("/hook/", hookHandler) http.HandleFunc("/", index) http.Handle("/css/", http.StripPrefix("/css/", http.FileServer(http.Dir("html/css")))) + http.Handle("/imgs/", http.StripPrefix("/imgs/", http.FileServer(http.Dir("html/imgs")))) + http.Handle("/js/", http.StripPrefix("/js/", http.FileServer(http.Dir("html/js")))) // Start the server log.Println("Starting server") log.Fatal(http.ListenAndServe(addr+":"+port, nil)) -} \ No newline at end of file +} diff --git a/src/server.go b/src/server.go index 104b202..1b36cc7 100644 --- a/src/server.go +++ b/src/server.go @@ -2,10 +2,13 @@ package main import ( "database/sql" + "encoding/json" "fmt" "html/template" "log" "net/http" + "os" + _ "github.com/go-sql-driver/mysql" ) @@ -15,7 +18,7 @@ var db *sql.DB // Used in writing out errors when they occur type errorMessage struct { StatusCode int - Msg string + Msg string } // Writes the given error message in the template. If not, just writes the message raw @@ -34,64 +37,60 @@ func writeError(w http.ResponseWriter, e errorMessage) { // Get all the currently available hooks func getHooks(w http.ResponseWriter) []Hook { // Get all hooks from db - rows, err := db.Query("SELECT ID FROM HOOKS") + rows, err := db.Query("SELECT ID FROM Hooks") if err != nil { - writeError(w, errorMessage { - StatusCode: http.StatusInternalServerError, - Msg: err.Error(), - }) + logError(w, err) } defer rows.Close() // Look through hooks - var hooks []Hook + hooks := make([]Hook, 0) for rows.Next() { var hook Hook err = rows.Scan(&hook.Name) // Hook doesn't have a name (This shouldn't really happen) if err != nil { - writeError(w, errorMessage { - StatusCode: http.StatusInternalServerError, - Msg: err.Error(), - }) + logError(w, err) } hooks = append(hooks, hook) } return hooks } +// Run a particular hook (will probably come up with a better solution) +func runHook(w http.ResponseWriter, r *http.Request, hook Hook) { + b, err := json.Marshal(hook) + if err != nil { + logError(w, err) + } else { + fmt.Fprint(w, string(b)) + } +} + // Show the index func index(w http.ResponseWriter, r *http.Request) { // Log request to this url - log.Println(r.URL.String()) + log.Println(r.URL.String(), r.Method) if r.URL.String() == "/" { // Serve index template t := template.Must(template.New("index.html").ParseFiles("html/index.html")) err := t.Execute(w, getHooks(w)) if err != nil { - writeError(w, errorMessage { - StatusCode: http.StatusInternalServerError, - Msg: err.Error(), - }) + logError(w, err) } - // Can't find the page + // Can't find the page } else { - writeError(w, errorMessage { + writeError(w, errorMessage{ StatusCode: http.StatusNotFound, - Msg: "Not found", + Msg: "Not Found", }) } } -// Run a particular hook (will probably come up with a better solution) -func runHook(w http.ResponseWriter, r *http.Request, hook Hook) { - fmt.Fprintln(w, hook.Name) -} - // Handles requests to /hook/ func hookHandler(w http.ResponseWriter, r *http.Request) { // Log request to this url - log.Println(r.URL.String()) + log.Println(r.URL.String(), r.Method) if r.URL.String() == "/hook/" { switch r.Method { // Display all hooks @@ -100,9 +99,9 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { // Method unsupported default: log.Println("y tho") - writeError(w, errorMessage { + writeError(w, errorMessage{ StatusCode: http.StatusMethodNotAllowed, - Msg: "Method Unsupported", + Msg: "Method Unsupported", }) } return @@ -115,23 +114,22 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { // Handle the hook case http.MethodGet, http.MethodPost: // See if the hook exists - row := db.QueryRow("SELECT EXISTS(SELECT id FROM HOOKS WHERE HOOKS.id = ?)", name) + row := db.QueryRow("SELECT EXISTS(SELECT id FROM Hooks WHERE Hooks.id = ?)", name) var res int row.Scan(&res) // Hook doesn't exist if res == 0 { - writeError(w, errorMessage { + writeError(w, errorMessage{ StatusCode: http.StatusNotFound, - Msg: "Not Found", + Msg: "Not Found", }) return } // Construct the hook from the results - hook := Hook { - Name: name, - params: nil, - actions: nil, + hook := Hook{ + Name: name, + conditions: nil, } // Run the user's hook @@ -147,9 +145,9 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { // Method unsupported default: - writeError(w, errorMessage { + writeError(w, errorMessage{ StatusCode: http.StatusMethodNotAllowed, - Msg: "Method Not Allowed", + Msg: "Method Not Allowed", }) } } @@ -157,27 +155,21 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { // Shows all of the hooks func showHooks(w http.ResponseWriter) { hooks := getHooks(w) - allHooks := "" - if hooks != nil { - for _, h := range hooks { - allHooks += h.Name + "\n" - } + b, err := json.Marshal(hooks) + if err != nil { + logError(w, err) } else { - allHooks = "Nothing Found\n" + fmt.Fprint(w, string(b)) } - fmt.Fprint(w, allHooks) } // Creates a new hook func createHook(w http.ResponseWriter, name string) { - statement, err := db.Prepare("INSERT IGNORE INTO HOOKS(id) VALUES(?)") + statement, err := db.Prepare("INSERT IGNORE INTO Hooks(id) VALUES(?)") // Shouldn't really happen, but incase the insert goes wrong if err != nil { - log.Fatal(err) - writeError(w, errorMessage { - StatusCode: http.StatusInternalServerError, - Msg: err.Error(), - }) + logError(w, err) + os.Exit(1) return } defer statement.Close() @@ -187,14 +179,10 @@ func createHook(w http.ResponseWriter, name string) { // Delete a hook func deleteHook(w http.ResponseWriter, name string) { - statement, err := db.Prepare("DELETE FROM HOOKS where ID=(?)") + statement, err := db.Prepare("DELETE FROM Hooks WHERE ID=(?)") //Shouldn't ever error if err != nil { - log.Println(err) - writeError(w, errorMessage { - StatusCode: http.StatusInternalServerError, - Msg: err.Error(), - }) + logError(w, err) return } defer statement.Close() @@ -203,30 +191,16 @@ func deleteHook(w http.ResponseWriter, name string) { res, err := statement.Exec(name) // Shouldn't ever error if err != nil { - log.Println(err) - writeError(w, errorMessage { - StatusCode: http.StatusInternalServerError, - Msg: err.Error(), - }) + logError(w, err) return } // Probably also shouldn't error - n, err := res.RowsAffected() + _, err = res.RowsAffected() if err != nil { - log.Println(err) - fmt.Fprintln(w, err) - return - } - - // Nothing was deleted - if n == 0 { - writeError(w, errorMessage { - StatusCode: http.StatusInternalServerError, - Msg: "Hook was not deleted", - }) + logError(w, err) return } fmt.Fprintln(w, name) -} \ No newline at end of file +}