diff --git a/articles.php b/articles.php deleted file mode 100644 index 261f097..0000000 --- a/articles.php +++ /dev/null @@ -1,87 +0,0 @@ -fetch_assoc(); - notify_users($emails, $_SESSION['username'], $id, $_POST['title'], $_POST['excerpt']); - header('Location: articles.php?article_id=' . $id); - } - - break; - case 'follow': - follow_user($_SESSION['user_id'], $_POST['author_id'], ($_POST['follow'] == 'email_notify' ? 1 : 0)); - break; - case 'unfollow': - unfollow_user($_SESSION['user_id'], $_POST['author_id']); - break; - } -} - -if (isset($_GET['view'])) - $view = $_GET['view']; -else - $view = 'list'; - -switch ($view) { - case 'list': - echo article_list(); - break; - case 'filter': - if (isset($_GET['tag'])) - echo article_list(article_ids_by_tag($_GET['tag'])); - else if (isset($_GET['author_id'])) - echo article_list(articles_ids_by_author($_GET['author_id'])); - break; - case 'sort': - if (isset($_GET['sort']) && $_GET['sort'] == 'oldest') - echo article_list(article_ids_by_oldest()); - else if (isset($_GET['sort']) && $_GET['sort'] == 'newest') - echo article_list(article_ids_by_newest()); - break; - case 'read': - if (isset($_SESSION['user_id']) && isset($_GET['article_id'])) { - increment_read_counter($_GET['article_id']); - echo article_page_from_markdown($_GET['article_id']); - } else { - $redirect = (isset($_GET['article_id']) ? 'articles.php?view=read&article_id=' . $_GET['article_id'] : ''); - if ($redirect) - header('Location: user.php?view=login&redirect=' . urlencode($redirect)); - else - header('Location: articles.php'); - } - break; - case 'compose': - echo compose_view((isset($_GET['article_id']) ? $_GET['article_id'] : '')); - break; -} - -if (($view == 'list' || $view == 'sort') && (isset($_SESSION['role_id']) && $_SESSION['role_id'] <= contributor)) { - echo ' -
- '; -} - -include_once 'components/foot.php'; diff --git a/backend/auth.go b/backend/auth.go deleted file mode 100644 index 41fbfa7..0000000 --- a/backend/auth.go +++ /dev/null @@ -1,81 +0,0 @@ -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 deleted file mode 100644 index c0fe95b..0000000 --- a/backend/go.mod +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 210ae8c..0000000 --- a/backend/go.sum +++ /dev/null @@ -1,8 +0,0 @@ -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= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -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 deleted file mode 100644 index d1b9133..0000000 --- a/backend/posts.go +++ /dev/null @@ -1,527 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "strings" - - // "encoding/json" - // "fmt" - // "io" - "log" - "net/http" - - // "os" - // "os/signal" - "strconv" - //"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) { - 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 deleted file mode 100644 index 490178e..0000000 --- a/backend/router.go +++ /dev/null @@ -1,186 +0,0 @@ -package main - -import ( - "context" - // "encoding/json" - "fmt" - //"io" - "log" - "net/http" - "os" - "os/signal" - //"strings" - "sync" - "time" - - "github.com/gorilla/mux" - "github.com/joho/godotenv" - "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(os.Getenv("NEO4J_BOLT"), neo4j.BasicAuth("neo4j", os.Getenv("NEO4J_PASSWORD"), "")) - if err != nil { - panic(err) - } - - defer driver.Close(driver_ctx) - - err = driver.VerifyConnectivity(driver_ctx) - if err != nil { - log.Println("Error connecting to neo4j.") - panic(err) - } - - fmt.Println("neo4j connection established.") - - rsrc_endpoints := []string{ - "/auth", - "/users", - "/users", - "/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.MethodPost, - http.MethodGet, - http.MethodGet, - 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){ - auth_user(driver, driver_ctx), - create_new_user(driver, driver_ctx), - retrieve_all_users(driver, driver_ctx), - retrieve_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() { - err := godotenv.Load("../.env") - if err != nil { - log.Println("Error loading .env.") - return - } - - serve() -} diff --git a/backend/serveAPI.go b/backend/serveAPI.go deleted file mode 100644 index 83a535f..0000000 --- a/backend/serveAPI.go +++ /dev/null @@ -1,44 +0,0 @@ -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]) - } - apiR.Use(auth_middleware) - - 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 deleted file mode 100644 index 8259a57..0000000 --- a/backend/spaces.go +++ /dev/null @@ -1,273 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - // "fmt" - //"io" - "log" - "net/http" - // "os" - // "os/signal" - "strconv" - //"strings" - // "sync" - // "time" - "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 - } -} - -func retrieve_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/backend/users.go b/backend/users.go deleted file mode 100644 index a21b452..0000000 --- a/backend/users.go +++ /dev/null @@ -1,229 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "os" - "strconv" - - //"io" - "log" - "net/http" - - // "os" - // "os/signal" - // "reflect" - "math/rand" - "strings" - - // "sync" - // "time" - "github.com/gorilla/mux" - "github.com/neo4j/neo4j-go-driver/v5/neo4j" -) - -func id_exists(id int, driver neo4j.DriverWithContext, driver_ctx context.Context) bool { - result, _ := neo4j.ExecuteQuery(driver_ctx, driver, - "MATCH (u:User) WHERE id(u) = $id RETURN count(u)", - map[string]any{ - "id": id, - }, neo4j.EagerResultTransformer, - neo4j.ExecuteQueryWithDatabase("neo4j")) - - if len(result.Records) > 0 { - return true - } else { - return false - } -} - -func create_new_user(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.") - } - - username := r.FormValue("username") - email := r.FormValue("email") - password_hash := r.FormValue("password_hash") - - id := rand.Intn(999999999999) + 1 - for id_exists(id, driver, driver_ctx) { - id = rand.Intn(999999999999) + 1 - } - - profile_picture_file, header, err := r.FormFile("profile_picture") - if err != nil { - log.Println("Error retrieving file from form.") - } - - defer profile_picture_file.Close() - filename_parts := strings.Split(header.Filename, ".") - ext := filename_parts[len(filename_parts)-1] - - dst, err := os.Create(os.Getenv("PROFILE_IMAGES_FQ_PATH") + strconv.Itoa(id) + ext) - if err != nil { - log.Println("Error saving profile picture.") - } - defer dst.Close() - - _, err = io.Copy(dst, profile_picture_file) - if err != nil { - log.Println("Error writing file to filesystem.") - } - - result, _ := neo4j.ExecuteQuery(driver_ctx, driver, - "CREATE (u:User {id: $id, username: $username, email: $email, password_hash: $password_hash, profile_picture: $profile_picture, created_at: datetime()})-[:USER_TYPE]->(:Role {name: 'User'}) RETURN elementId(u) AS id", - map[string]any{ - "id": id, - "username": username, - "email": email, - "password_hash": password_hash, - "profile_picture": strconv.Itoa(id) + ext, - }, neo4j.EagerResultTransformer, - neo4j.ExecuteQueryWithDatabase("neo4j")) - - if len(result.Records) > 0 { - ret := struct { - Auth bool `json:"auth"` - Id string `json:"id"` - }{ - Auth: true, - Id: strconv.Itoa(id), - } - - json, err := json.Marshal(ret) - if err != nil { - return - } - - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, string(json)) - log.Println("User ID:", id, "Action: Signup") - } else { - 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) { - 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": id, - }, neo4j.EagerResultTransformer, - neo4j.ExecuteQueryWithDatabase("neo4j")) - - if len(result.Records) > 0 { - 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 - } - - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, string(json)) - log.Println("User ID:", id, "Action: Retrieval") - } else { - return - } - } -} - -func auth_user(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") - - 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 - } - - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, string(json)) - log.Println("User ID:", id, "Action: Login") - } 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/components/article/card.php b/components/article/card.php deleted file mode 100644 index e403915..0000000 --- a/components/article/card.php +++ /dev/null @@ -1,49 +0,0 @@ -fetch_assoc(); - - $tags_results = exec_stmt('SELECT tag FROM article_tags INNER JOIN tags ON article_tags.tag_id = tags.tag_id WHERE article_id = ?', 'i', $article_id); - - $tags_html = ''; - if ($tags_results != null) { - $tags_html .= '' . $article['excerpt'] . '
-' . $essay['excerpt'] . '
+