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 '';
-?>
+ ?>