diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..633f1f8 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,8 @@ +module the.hub/m + +go 1.24.2 + +require ( + github.com/gorilla/mux v1.8.1 // indirect + github.com/neo4j/neo4j-go-driver/v5 v5.28.0 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..2cbaca1 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,4 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/neo4j/neo4j-go-driver/v5 v5.28.0 h1:chDT68PHNa8JZRmjSkGzAbk1weLWo4rMtDvccvpobg0= +github.com/neo4j/neo4j-go-driver/v5 v5.28.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k= diff --git a/backend/posts.go b/backend/posts.go new file mode 100644 index 0000000..08a9228 --- /dev/null +++ b/backend/posts.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + // "encoding/json" + // "fmt" + //"io" + // "log" + "net/http" + // "os" + // "os/signal" + //"strings" + // "sync" + // "time" + // "github.com/gorilla/mux" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +func create_new_post(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func retrieve_all_posts(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func retrieve_post(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func update_post(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func delete_post(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} diff --git a/backend/router.go b/backend/router.go new file mode 100644 index 0000000..0f65a56 --- /dev/null +++ b/backend/router.go @@ -0,0 +1,172 @@ +package main + +import ( + "context" + // "encoding/json" + "fmt" + //"io" + "log" + "net/http" + "os" + "os/signal" + //"strings" + "sync" + "time" + + "github.com/gorilla/mux" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +func send_error(w http.ResponseWriter, json []byte) { + w.WriteHeader(http.StatusBadRequest) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, string(json)) +} + +func send_response(w http.ResponseWriter, json []byte) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, string(json)) +} + +func serve() { + // TODO: Authenticate user credentials to verify they are allowed to even access + var wg sync.WaitGroup + + driver_ctx := context.Background() + driver, err := neo4j.NewDriverWithContext("bolt://localhost:7687", neo4j.BasicAuth("neo4j", "jzLbsy2C23WHzQ-", "")) + if err != nil { + panic(err) + } + + defer driver.Close(driver_ctx) + + err = driver.VerifyConnectivity(driver_ctx) + if err != nil { + panic(err) + } + + fmt.Println("neo4j connection established.") + + rsrc_endpoints := []string{ + "/users", + "/users", + "/users/{user_id}", + "/users/{user_id}", + "/users/{user_id}", + "/users/{user_id}", + "/users/{user_id}/posts", + "/users/{user_id}/spaces", + "/spaces", + "/spaces", + "/spaces/{space_id}", + "/spaces/{space_id}", + "/spaces/{space_id}", + "/spaces/{space_id}", + "/spaces/{space_id}/users", + "/spaces/{space_id}/posts", + "/posts", + "/posts", + "/posts/{post_id}", + "/posts/{post_id}", + "/posts/{post_id}", + } + + methods := []string{ + http.MethodPost, + http.MethodGet, + http.MethodGet, + http.MethodPost, + http.MethodPatch, + http.MethodDelete, + http.MethodGet, + http.MethodGet, + http.MethodPost, + http.MethodGet, + http.MethodGet, + http.MethodPost, + http.MethodPatch, + http.MethodDelete, + http.MethodGet, + http.MethodGet, + http.MethodPost, + http.MethodGet, + http.MethodGet, + http.MethodPatch, + http.MethodDelete, + } + + functions := []func(http.ResponseWriter, *http.Request){ + create_new_user(driver, driver_ctx), + retrieve_all_users(driver, driver_ctx), + retrieve_user(driver, driver_ctx), + auth_user(driver, driver_ctx), + update_user(driver, driver_ctx), + delete_user(driver, driver_ctx), + retrieve_users_posts(driver, driver_ctx), + retrieve_users_spaces(driver, driver_ctx), + create_new_space(driver, driver_ctx), + retrieve_all_spaces(driver, driver_ctx), + retrieve_space(driver, driver_ctx), + auth_user_in_space(driver, driver_ctx), + update_space(driver, driver_ctx), + delete_space(driver, driver_ctx), + retrieve_spaces_users(driver, driver_ctx), + retrieve_spaces_posts(driver, driver_ctx), + create_new_post(driver, driver_ctx), + retrieve_all_posts(driver, driver_ctx), + retrieve_post(driver, driver_ctx), + update_post(driver, driver_ctx), + delete_post(driver, driver_ctx), + } + + r := mux.NewRouter() + + ServeApi(r, "api", rsrc_endpoints, methods, functions, "7477") + log.Println("API routes configured with prefix", "/api") + + addr := "0.0.0.0:7476" + + srv := &http.Server{ + Addr: addr, + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: r, + } + + // Run our server in a goroutine so that it doesn't block. + wg.Add(1) + go func() { + defer wg.Done() + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Println(err) + } + }() + + log.Println("Router is running on port 7476") + + c := make(chan os.Signal, 1) + // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+Shift+C) + signal.Notify(c, os.Interrupt) + + // Block until we receive our signal. + <-c + + // Create a deadline to wait for. + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(30)) + defer cancel() + // Doesn't block if no connections, but will otherwise wait + // until the timeout deadline. + srv.Shutdown(ctx) + // Optionally, you could run srv.Shutdown in a goroutine and block on + // <-ctx.Done() if your application should wait for other services + // to finalize based on context cancellation. + log.Println("shutting down") + wg.Wait() + os.Exit(0) +} + +func main() { + serve() +} diff --git a/backend/serveAPI.go b/backend/serveAPI.go new file mode 100644 index 0000000..afd224a --- /dev/null +++ b/backend/serveAPI.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/gorilla/mux" + "log" + "net/http" + "time" +) + +// Serve an API using mux.Router().Host({domain}).Subrouter(). +// Provide the router, domain, an array of endpoints and functions, and +// the port you would like the API accessible to. +func ServeApi( + router *mux.Router, + prefix string, + endpoints []string, + methods []string, + functions []func(http.ResponseWriter, *http.Request), + port string, +) { + + apiR := router.PathPrefix(prefix).Subrouter() + + for i := range endpoints { + apiR.HandleFunc(endpoints[i], functions[i]).Methods(methods[i]) + } + + apiSrv := &http.Server{ + Addr: "0.0.0.0:" + port, + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: apiR, + } + + go func() { + if err := apiSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Println(err) + } + }() + + log.Println("API server is running on port " + port) +} diff --git a/backend/spaces.go b/backend/spaces.go new file mode 100644 index 0000000..1071c28 --- /dev/null +++ b/backend/spaces.go @@ -0,0 +1,65 @@ +package main + +import ( + "context" + // "encoding/json" + // "fmt" + //"io" + // "log" + "net/http" + // "os" + // "os/signal" + //"strings" + // "sync" + // "time" + // "github.com/gorilla/mux" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +func create_new_space(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func retrieve_all_spaces(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func retrieve_space(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func auth_user_in_space(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func update_space(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func delete_space(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func retrieve_spaces_posts(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func retrieve_spaces_users(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} diff --git a/backend/users.go b/backend/users.go new file mode 100644 index 0000000..f97a551 --- /dev/null +++ b/backend/users.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "encoding/json" + // "fmt" + "strconv" + //"io" + // "log" + "net/http" + // "os" + // "os/signal" + // "reflect" + "strings" + // "sync" + // "time" + "github.com/gorilla/mux" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +func create_new_user(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func retrieve_all_users(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func retrieve_user(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user_id, _ := strconv.Atoi(mux.Vars(r)["user_id"]) + + result, _ := neo4j.ExecuteQuery(driver_ctx, driver, + "MATCH (u:User) WHERE id(u) = $user_id RETURN u.username AS username, u.email AS email, u.profile_picture AS profile_picture", + map[string]any{ + "user_id": user_id, + }, neo4j.EagerResultTransformer, + neo4j.ExecuteQueryWithDatabase("neo4j")) + + record := result.Records[0] + vals := record.AsMap() + user := struct { + Username string `json:"username"` + Email string `json:"email"` + ProfilePicture string `json:"profile_picture"` + }{ + Username: vals["username"].(string), + Email: vals["email"].(string), + ProfilePicture: vals["profile_picture"].(string), + } + + json, err := json.Marshal(user) + if err != nil { + return + } + + send_response(w, json) + } +} + +func auth_user(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + username := r.FormValue("username") + password_hash := r.FormValue("password_hash") + + result, _ := neo4j.ExecuteQuery(driver_ctx, driver, + "MATCH (u:User) WHERE u.username = $username RETURN u.password_hash AS password_hash, elementId(u) AS id", + map[string]any{ + "username": username, + }, neo4j.EagerResultTransformer, + neo4j.ExecuteQueryWithDatabase("neo4j")) + + if len(result.Records) > 0 { + record := result.Records[0] + vals := record.AsMap() + if vals["password_hash"] != password_hash { + return + } + + id := strings.Split(vals["id"].(string), ":")[2] + + ret := struct { + Auth bool `json:"auth"` + Id string `json:"id"` + }{ + Auth: true, + Id: id, + } + + json, err := json.Marshal(ret) + if err != nil { + return + } + + send_response(w, json) + } else { + return + } + + } +} + +func update_user(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func delete_user(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func retrieve_users_posts(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} + +func retrieve_users_spaces(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + return + } +} diff --git a/composer.json b/composer.json index 61c1fa2..2306b61 100644 --- a/composer.json +++ b/composer.json @@ -5,12 +5,18 @@ "require": { "erusev/parsedown": "^1.7", "vlucas/phpdotenv": "^5.6", - "wildbit/postmark-php": "^6.0" + "wildbit/postmark-php": "^6.0", + "laudis/neo4j-php-client": "^3.2" }, "authors": [ { "name": "Josh Ashton", "email": "me@joshashton.dev" } - ] + ], + "config": { + "allow-plugins": { + "php-http/discovery": true + } + } } diff --git a/functions/account_functions.php b/functions/account_functions.php index 2a10775..2b652a8 100644 --- a/functions/account_functions.php +++ b/functions/account_functions.php @@ -1,5 +1,5 @@