diff --git a/.gitignore b/.gitignore index 50d8441..7f62d0b 100755 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ data/articles data/tmp data/neo4j import.sql +eula.txt diff --git a/backend/auth.go b/backend/auth.go new file mode 100644 index 0000000..41fbfa7 --- /dev/null +++ b/backend/auth.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "fmt" + // "log" + "net/http" + "os" + // "strconv" + "strings" + + "github.com/golang-jwt/jwt" +) + +// Define a custom context key for storing user information +var userCtxKey = &contextKey{"user"} + +type contextKey struct { + name string +} + +// AuthMiddleware is our JWT verification middleware +func auth_middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 1. Extract the JWT from the request (e.g., Authorization header) + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + tokenString := "" + parts := strings.Split(authHeader, " ") + if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" { + tokenString = parts[1] + } else { + http.Error(w, "Invalid Authorization header format", http.StatusUnauthorized) + return + } + + // 2. Verify the JWT + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Validate the signing method + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("invalid signing method: %v", token.Header["alg"]) + } + // Replace "your-secret-key" with your actual secret key + return []byte(os.Getenv("JWT_SECRET_KEY")), nil + }) + + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + // 3. Optionally, extract user information from the claims and add it to the request context + user_id_raw, ok := claims["user_id"].(float64) + if !ok { + http.Error(w, "Invalid user ID in token", http.StatusInternalServerError) + return + } + + user_id := int(user_id_raw) + + // Create a new context with the user information + ctx := context.WithValue(r.Context(), userCtxKey, user_id) + // Call the next handler in the chain, passing the new context + next.ServeHTTP(w, r.WithContext(ctx)) + } else { + http.Error(w, "Invalid token claims", http.StatusUnauthorized) + return + } + }) +} + +// Helper function to extract user information from the context in your handlers +func user_id_from_context(ctx context.Context) (int, bool) { + userID, ok := ctx.Value(userCtxKey).(int) + return userID, ok +} diff --git a/backend/go.mod b/backend/go.mod index 0d659af..c0fe95b 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,6 +3,7 @@ module the.hub/m go 1.24.2 require ( + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/neo4j/neo4j-go-driver/v5 v5.28.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index bd69801..210ae8c 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,3 +1,5 @@ +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= diff --git a/backend/posts.go b/backend/posts.go index 08a9228..d1b9133 100644 --- a/backend/posts.go +++ b/backend/posts.go @@ -2,46 +2,526 @@ package main import ( "context" + "encoding/json" + "strings" + // "encoding/json" // "fmt" - //"io" - // "log" + // "io" + "log" "net/http" + // "os" // "os/signal" + "strconv" //"strings" // "sync" // "time" - // "github.com/gorilla/mux" + "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 + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:2025") + + err := r.ParseMultipartForm(10 << 20) // Limit to 10MB + if err != nil { + log.Println("Unable to parse form:", err) + http.Error(w, "Unable to parse form", http.StatusBadRequest) + return + } + + space_id, err := strconv.Atoi(r.FormValue("space_id")) + if err != nil { + log.Println("Invalid space id:", err) + http.Error(w, "Invalid space id.", http.StatusBadRequest) + return + } + + // Required fields + user_id, ok := user_id_from_context(r.Context()) + if !ok { + log.Println("Invalid user ID") + http.Error(w, "Invalid user ID", http.StatusBadRequest) + return + } + + if !auth_user_in_space(driver, driver_ctx, user_id, space_id) { + log.Println("User", user_id, "attempted access to space", space_id, ", which is not allowed.") + http.Error(w, "You are not a member of that space.", http.StatusUnauthorized) + return + } + + post_type := r.FormValue("post_type") + post_status := r.FormValue("post_status") + content := r.FormValue("content") + //create_space := r.FormValue("create_space") + + if post_type == "" || content == "" || post_status == "" { + http.Error(w, "Missing required fields", http.StatusBadRequest) + return + } + + // Optional fields + title := r.FormValue("title") + excerpt := r.FormValue("excerpt") + location := r.FormValue("location") + raw_topics := r.FormValue("topics") + // files := r.FormFile("files") // Consider using r.FormFile for actual uploads + // images := r.Form("images") // Consider using r.Form for multiple URLs + // videos := r.Form("videos") // Consider using r.Form for multiple URLs + + // TODO: Implement space retrieval or creation logic based on space_name and create_space + + topics := []string{} + if raw_topics != "" { + topicList := strings.SplitSeq(raw_topics, ",") + for topic := range topicList { + trimmedTopic := strings.TrimSpace(topic) + if trimmedTopic != "" { + topics = append(topics, trimmedTopic) + } + } + } + + topics_rel_stmt := "" + if len(topics) > 0 { + topics_rel_stmt = ` + WITH p + FOREACH (topicName IN $topicsList | + MERGE (t:Topic {name: topicName}) + CREATE (p)-[:TAGGED_WITH]->(t) + ) + ` + } + + stmt := ` + MATCH (u:User) WHERE id(u) = $user_id + MATCH (t:PostType) WHERE t.name = $post_type + MATCH (s:Space) WHERE id(s) = $space_id + MATCH (st:PostStatus) WHERE st.name = $post_status + + CREATE + (u)-[:CREATED {time: datetime()}]->(p:Post { + content: $content, + title: $title, + excerpt: $excerpt, + location: $location + }) + + CREATE (p)-[:POST_TYPE]->(t) + CREATE (p)-[:IN_SPACE]->(s) + CREATE (p)-[:STATUS]->(st) + + ` + topics_rel_stmt + + stmt_args := map[string]any{ + "user_id": user_id, + "post_type": post_type, + "space_id": space_id, + "post_status": post_status, + "content": content, + "title": title, + "excerpt": excerpt, + "location": location, + "topicsList": topics, + } + + _, err = neo4j.ExecuteQuery(driver_ctx, driver, stmt, stmt_args, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + + if err != nil { + log.Println("Error creating post:", err) + http.Error(w, "Failed to create post", http.StatusInternalServerError) + return + } else { + w.WriteHeader(http.StatusCreated) + w.Write([]byte("Post created successfully!")) + return + } } } +// TODO: Need to fix bug where only one topic/topic name are being returned in JSON response. + func retrieve_all_posts(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:2025") + + // TODO: Handle anonymous users. + user_id, ok := user_id_from_context(r.Context()) + if !ok { + http.Error(w, "Invalid user ID.", http.StatusBadRequest) + return + } + + stmt := ` + MATCH (p:Post)-[:IN_SPACE]->(s:Space) + MATCH (u:User)-[:SPACE_MEMBER]->(s) + MATCH (s)-[:SPACE_TYPE]->(st:SpaceType) + MATCH (p)-[:POST_TYPE]->(pt:PostType) + MATCH (p)-[:STATUS]->(ps:PostStatus) + MATCH (p)-[:TAGGED_WITH]->(t:Topic) + MATCH (a:User)-[:CREATED]->(p) + + WHERE + id(u) = $user_id AND + ps.name = "Published" + + RETURN + id(a) AS author_id, + a.username AS author_username, + a.profile_picture AS author_picture, + + p.title AS title, + p.excerpt AS excerpt, + p.location AS location, + p.content AS content, + + pt.name AS post_type, + id(t) AS topic_id, + t.name AS topic_name, + + id(s) AS space_id, + s.name AS space_name, + st.name AS space_type + ` + + stmt_args := map[string]any{ + "user_id": user_id, + } + + result, err := neo4j.ExecuteQuery(driver_ctx, driver, stmt, stmt_args, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + + if err != nil { + log.Println("Error retrieving all posts:", err) + http.Error(w, "Failed to retrieve all posts", http.StatusInternalServerError) + return + } + + posts_obj := map[int]any{} + + for index, record := range result.Records { + space_id, _ := record.Get("space_id") + space_id = int(space_id.(int64)) + if !auth_user_in_space(driver, driver_ctx, user_id, space_id.(int)) { + http.Error(w, "You are not allowed in this space.", http.StatusUnauthorized) + return + } + + author_id, _ := record.Get("author_id") + author_username, _ := record.Get("author_username") + author_picture, _ := record.Get("author_picture") + + title, _ := record.Get("title") + excerpt, _ := record.Get("excerpt") + location, _ := record.Get("location") + content, _ := record.Get("content") + + post_type, _ := record.Get("post_type") + topic_id, _ := record.Get("topic_id") + topic_name, _ := record.Get("topic_name") + space_name, _ := record.Get("space_name") + + record_obj := map[string]any{ + "author_id": author_id, + "author_username": author_username, + "author_picture": author_picture, + "title": title, + "excerpt": excerpt, + "location": location, + "content": content, + "post_type": post_type, + "topic_id": topic_id, + "topic_name": topic_name, + "space_id": space_id, + "space_name": space_name, + } + + posts_obj[index] = record_obj + } + + obj, err := json.Marshal(posts_obj) + if err != nil { + http.Error(w, "Failed to parse into JSON", http.StatusInternalServerError) + log.Println("error marshalling json") + return + } + + w.WriteHeader(http.StatusOK) + w.Write(obj) return } } +// TODO: Need to fix bug where only one topic/topic name are being returned in JSON response. func retrieve_post(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:2025") + + // TODO: Handle anonymous users. + user_id, ok := user_id_from_context(r.Context()) + if !ok { + http.Error(w, "Invalid user ID.", http.StatusBadRequest) + return + } + + post_id, err := strconv.Atoi(mux.Vars(r)["post_id"]) + if err != nil { + http.Error(w, "Invalid Post ID.", http.StatusBadRequest) + return + } + + stmt := ` + MATCH (p:Post)-[:IN_SPACE]->(s:Space) + MATCH (s)-[:SPACE_TYPE]->(st:SpaceType) + MATCH (p)-[:POST_TYPE]->(pt:PostType) + MATCH (p)-[:STATUS]->(ps:PostStatus) + MATCH (p)-[:TAGGED_WITH]->(t:Topic) + MATCH (a:User)-[:CREATED]->(p) + + WHERE + id(p) = $post_id + + RETURN + id(a) AS author_id, + a.username AS author_username, + a.profile_picture AS author_picture, + + p.title AS title, + p.excerpt AS excerpt, + p.location AS location, + p.content AS content, + + pt.name AS post_type, + ps.name AS post_status, + id(t) AS topic_id, + t.name AS topic_name, + + id(s) AS space_id, + st.name AS space_type + ` + + stmt_args := map[string]any{ + "post_id": post_id, + } + + result, err := neo4j.ExecuteQuery(driver_ctx, driver, stmt, stmt_args, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + if err != nil { + http.Error(w, "Failed to retrieve Post.", http.StatusInternalServerError) + return + } + + if len(result.Records) != 2 { + http.Error(w, "Post does not exist.", http.StatusBadRequest) + return + } + + record := result.Records[0] + space_id, _ := record.Get("space_id") + space_id = int(space_id.(int64)) + if !auth_user_in_space(driver, driver_ctx, user_id, space_id.(int)) { + http.Error(w, "You are not allowed in this space.", http.StatusUnauthorized) + return + } + + author_id, _ := record.Get("author_id") + author_username, _ := record.Get("author_username") + author_picture, _ := record.Get("author_picture") + title, _ := record.Get("title") + excerpt, _ := record.Get("excerpt") + location, _ := record.Get("location") + content, _ := record.Get("content") + post_type, _ := record.Get("post_type") + post_status, _ := record.Get("post_status") + topic_id, _ := record.Get("topic_id") + topic_name, _ := record.Get("topic_name") + space_type, _ := record.Get("space_type") + + record_obj := map[string]any{ + "author_id": author_id, + "author_username": author_username, + "author_picture": author_picture, + "title": title, + "excerpt": excerpt, + "location": location, + "content": content, + "post_type": post_type, + "post_status": post_status, + "topic_id": topic_id, + "topic_name": topic_name, + "space_id": space_id, + "space_type": space_type, + } + + obj, err := json.Marshal(record_obj) + if err != nil { + http.Error(w, "Failed to parse into JSON", http.StatusInternalServerError) + log.Println("error marshalling json") + return + } + + w.WriteHeader(http.StatusOK) + w.Write(obj) return } } +// TODO: Need to test. func update_post(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:2025") + + err := r.ParseMultipartForm(10 << 20) // Limit to 10MB + if err != nil { + log.Println("Unable to parse form:", err) + http.Error(w, "Unable to parse form", http.StatusBadRequest) + return + } + + space_id, err := strconv.Atoi(r.FormValue("space_id")) + if err != nil { + log.Println("Invalid space id:", err) + http.Error(w, "Invalid space id.", http.StatusBadRequest) + return + } + + user_id, ok := user_id_from_context(r.Context()) + if !ok { + http.Error(w, "Invalid user ID.", http.StatusUnauthorized) + return + } + + if !auth_user_in_space(driver, driver_ctx, user_id, space_id) { + log.Println("User", user_id, "attempted access to space", space_id, ", which is not allowed.") + http.Error(w, "You are not a member of that space.", http.StatusUnauthorized) + return + } + + post_id, err := strconv.Atoi(mux.Vars(r)["post_id"]) + if err != nil { + http.Error(w, "Failed to get Post ID.", http.StatusInternalServerError) + return + } + + post_type := r.FormValue("post_type") + post_status := r.FormValue("post_status") + content := r.FormValue("content") + //create_space := r.FormValue("create_space") + + if post_type == "" || content == "" || post_status == "" { + http.Error(w, "Missing required fields.", http.StatusBadRequest) + return + } + + // Optional fields + title := r.FormValue("title") + excerpt := r.FormValue("excerpt") + location := r.FormValue("location") + raw_topics := r.FormValue("topics") + // files := r.FormFile("files") // Consider using r.FormFile for actual uploads + // images := r.Form("images") // Consider using r.Form for multiple URLs + // videos := r.Form("videos") // Consider using r.Form for multiple URLs + + // TODO: Implement space retrieval or creation logic based on space_name and create_space + + topics := []string{} + if raw_topics != "" { + topicList := strings.SplitSeq(raw_topics, ",") + for topic := range topicList { + trimmedTopic := strings.TrimSpace(topic) + if trimmedTopic != "" { + topics = append(topics, trimmedTopic) + } + } + } + + topics_rel_stmt := "" + if len(topics) > 0 { + topics_rel_stmt = ` + WITH p + FOREACH (topicName IN $topicsList | + MERGE (t:Topic {name: topicName}) + MERGE (p)-[:TAGGED_WITH]->(t) + ) + ` + } + + stmt := ` + MATCH (u:User) WHERE id(u) = $user_id + MATCH (t:PostType) WHERE t.name = $post_type + MATCH (s:Space) WHERE id(s) = $space_id + MATCH (st:PostStatus) WHERE st.name = $post_status + + SET + p.content = $content, + p.title = $title, + p.excerpt = $excerpt, + p.location = $location, + p.updated_at = datetime() + + + MERGE (p)-[:POST_TYPE]->(t) + MERGE (p)-[:IN_SPACE]->(s) + MERGE (p)-[:STATUS]->(st) + + ` + topics_rel_stmt + + stmt_args := map[string]any{ + "user_id": user_id, + "post_type": post_type, + "space_id": space_id, + "post_status": post_status, + "content": content, + "title": title, + "excerpt": excerpt, + "location": location, + "topicsList": topics, + } + + _, err = neo4j.ExecuteQuery(driver_ctx, driver, stmt, stmt_args, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + if err != nil { + log.Println("Attempted to update Post", post_id, "which failed.") + http.Error(w, "Unable to update Post.", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) return } } func delete_post(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:2025") + + user_id, ok := user_id_from_context(r.Context()) + if !ok { + http.Error(w, "Invalid user ID.", http.StatusUnauthorized) + return + } + + post_id, err := strconv.Atoi(mux.Vars(r)["post_id"]) + if err != nil { + http.Error(w, "Invalid Post ID.", http.StatusBadRequest) + return + } + + stmt := "MATCH (u:User)-[:CREATED]->(p:Post) WHERE id(u) = $user_id AND id(p) = $post_id DETACH DELETE p" + + stmt_args := map[string]any{ + "user_id": user_id, + "post_id": post_id, + } + + _, err = neo4j.ExecuteQuery(driver_ctx, driver, stmt, stmt_args, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + + if err != nil { + http.Error(w, "Unable to delete Post.", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) return } } diff --git a/backend/router.go b/backend/router.go index 66bfe7d..490178e 100644 --- a/backend/router.go +++ b/backend/router.go @@ -59,14 +59,16 @@ func serve() { "/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}", "/spaces/{space_id}/users", "/spaces/{space_id}/posts", + "/posts", "/posts", "/posts/{post_id}", @@ -83,14 +85,16 @@ func serve() { http.MethodDelete, http.MethodGet, http.MethodGet, + http.MethodPost, http.MethodGet, http.MethodGet, - http.MethodPost, + //http.MethodPost, http.MethodPatch, http.MethodDelete, http.MethodGet, http.MethodGet, + http.MethodPost, http.MethodGet, http.MethodGet, @@ -107,14 +111,16 @@ func serve() { 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), + //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), diff --git a/backend/serveAPI.go b/backend/serveAPI.go index afd224a..83a535f 100644 --- a/backend/serveAPI.go +++ b/backend/serveAPI.go @@ -24,6 +24,7 @@ func ServeApi( for i := range endpoints { apiR.HandleFunc(endpoints[i], functions[i]).Methods(methods[i]) } + apiR.Use(auth_middleware) apiSrv := &http.Server{ Addr: "0.0.0.0:" + port, diff --git a/backend/spaces.go b/backend/spaces.go index 1071c28..8259a57 100644 --- a/backend/spaces.go +++ b/backend/spaces.go @@ -2,26 +2,108 @@ package main import ( "context" - // "encoding/json" + "encoding/json" // "fmt" //"io" - // "log" + "log" "net/http" // "os" // "os/signal" + "strconv" //"strings" // "sync" // "time" - // "github.com/gorilla/mux" + "github.com/gorilla/mux" "github.com/neo4j/neo4j-go-driver/v5/neo4j" ) +// TODO: Need to test. func create_new_space(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:2025") + + // Must be a user to create a space. + user_id, ok := user_id_from_context(r.Context()) + if !ok { + log.Println("Invalid user ID") + http.Error(w, "Invalid user ID", http.StatusBadRequest) + return + } + + err := r.ParseMultipartForm(10 << 20) // Limit to 10MB + if err != nil { + log.Println("Unable to parse form:", err) + http.Error(w, "Unable to parse form", http.StatusBadRequest) + return + } + + var space map[string]any + err = json.NewDecoder(r.Body).Decode(&space) + + if space["id"] != -1 { + log.Println("Attempt to create a space with a defined ID") + http.Error(w, "Space ID cannot be provided.", http.StatusBadRequest) + return + } + + stmt := ` + MATCH (owner:User) + WHERE id(owner) = $user_id + + CREATE (s:Space { + created_at: datetime(), + name: $name, + description: $description + }) + + CREATE (owner)-[:SPACE_MEMBER {role: "Owner"}]->(s) + + UNWIND $members as member + MATCH (m:User) + WHERE id(m) = member.id + CREATE (m)-[:SPACE_MEMBER {role: member.role}]->(s) + + RETURN id(s) AS id, + s.name AS name + ` + + stmt_args := map[string]any{ + "user_id": user_id, + "name": space["name"], + "description": space["description"], + "members": space["members"], + } + + result, err := neo4j.ExecuteQuery(driver_ctx, driver, stmt, stmt_args, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + + if err != nil || len(result.Records) != 2 { + log.Println("Error creating Space:", err) + http.Error(w, "Failed to create Space", http.StatusInternalServerError) + return + } + + record := result.Records[0] + space_id, _ := record.Get("id") + space_name, _ := record.Get("name") + + record_obj := map[string]any{ + "space_id": space_id, + "space_name": space_name, + } + + obj, err := json.Marshal(record_obj) + if err != nil { + http.Error(w, "Failed to marshal JSON.", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + w.Write(obj) return } } +// TODO: Need to implement for ADMIN ONLY func retrieve_all_spaces(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { return @@ -34,30 +116,156 @@ func retrieve_space(driver neo4j.DriverWithContext, driver_ctx context.Context) } } -func auth_user_in_space(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - return +// TODO: Implement access for Public and Open Spaces +func auth_user_in_space(driver neo4j.DriverWithContext, driver_ctx context.Context, user_id int, space_id int) bool { + stmt := ` + MATCH (u:User), (s:Space) + WHERE id(u) = $user_id AND id(s) = $space_id + RETURN EXISTS((u)-[:SPACE_MEMBER]->(s)) + ` + + stmt_args := map[string]any{ + "user_id": user_id, + "space_id": space_id, + } + + result, err := neo4j.ExecuteQuery(driver_ctx, driver, stmt, stmt_args, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + if err != nil { + log.Println("Something went wrong authorizing the user to a space.") + return false + } + + if len(result.Records) > 0 { + return result.Records[0].Values[0].(bool) + } else { + return false } } +// TODO: Implement func update_space(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { return } } +// TODO: Implement func delete_space(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { return } } +// TODO: Need to handle multiple topics. func retrieve_spaces_posts(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:2025") + + // Must be a user to create a space. + user_id, ok := user_id_from_context(r.Context()) + if !ok { + log.Println("Invalid user ID") + http.Error(w, "Invalid user ID", http.StatusBadRequest) + return + } + + space_id, _ := strconv.Atoi(mux.Vars(r)["space_id"]) + + if !auth_user_in_space(driver, driver_ctx, user_id, space_id) { + log.Println("User", user_id, "attempted access to space", space_id, ", which is not allowed.") + http.Error(w, "You are not a member of that space.", http.StatusUnauthorized) + return + } + + stmt := ` + MATCH (p:Post)-[:IN_SPACE]->(s:Space) + MATCH (s)-[:SPACE_TYPE]->(st:SpaceType) + MATCH (p)-[:POST_TYPE]->(pt:PostType) + MATCH (p)-[:STATUS]->(ps:PostStatus) + MATCH (p)-[:TAGGED_WITH]->(t:Topic) + MATCH (a:User)-[:CREATED]->(p) + + WHERE + id(s) = $space_id AND + ps.name = "Published" + + RETURN + id(a) AS author_id, + a.username AS author_username, + a.profile_picture AS author_picture, + + p.title AS title, + p.excerpt AS excerpt, + p.location AS location, + p.content AS content, + + pt.name AS post_type, + id(t) AS topic_id, + t.name AS topic_name, + + s.name AS space_name, + ` + + stmt_args := map[string]any{ + "space_id": space_id, + } + + result, err := neo4j.ExecuteQuery(driver_ctx, driver, stmt, stmt_args, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + + if !(len(result.Records) >= 2) { + log.Println("Failed to retrieve Posts for Space", space_id, ". Error:", err) + http.Error(w, "Error retrieving Posts from Space.", http.StatusInternalServerError) + return + } + + posts_obj := map[int]any{} + for index, record := range result.Records { + + author_id, _ := record.Get("author_id") + author_username, _ := record.Get("author_username") + author_picture, _ := record.Get("author_picture") + + title, _ := record.Get("title") + excerpt, _ := record.Get("excerpt") + location, _ := record.Get("location") + content, _ := record.Get("content") + + post_type, _ := record.Get("post_type") + topic_id, _ := record.Get("topic_id") + topic_name, _ := record.Get("topic_name") + space_name, _ := record.Get("space_name") + + record_obj := map[string]any{ + "author_id": author_id, + "author_username": author_username, + "author_picture": author_picture, + "title": title, + "excerpt": excerpt, + "location": location, + "content": content, + "post_type": post_type, + "topic_id": topic_id, + "topic_name": topic_name, + "space_name": space_name, + } + + posts_obj[index] = record_obj + } + + obj, err := json.Marshal(posts_obj) + if err != nil { + http.Error(w, "Failed to parse into JSON", http.StatusInternalServerError) + log.Println("error marshalling json") + return + } + + w.WriteHeader(http.StatusOK) + w.Write(obj) return } } +// TODO: Implement for Space Member role == Administrator OR role == Owner 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/components/head.php b/components/head.php index 5f00ff6..73ac6b6 100644 --- a/components/head.php +++ b/components/head.php @@ -5,13 +5,13 @@ - + '; -else + else echo ''; -?> + ?>