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 .= '
'; - - while ($row = $tags_results->fetch_assoc()) - $tags_html .= '#' . $row['tag'] . ''; - - $tags_html .= '
'; - } - - $author = exec_stmt('SELECT user_id, username, profile_picture FROM user WHERE user_id = ?', 'i', $article['author_id'])->fetch_assoc(); - - $card = ' -
-
-

' . $article['title'] . '

-
-
- Written by: ' . $author['username'] . ' - Published: ' . $article['published_at'] . ' - Updated: ' . $article['updated_at'] . ' -
-
- -
-
-
-
-

' . $article['excerpt'] . '

-
- Read More -
' . $tags_html . '
-
-
- '; - - return $card; -} diff --git a/components/core/card.php b/components/core/card.php new file mode 100644 index 0000000..4074277 --- /dev/null +++ b/components/core/card.php @@ -0,0 +1,84 @@ +'; + $topics_html .= '#' . $row['tag'] . ''; + $topics_html .= ''; + + $author = exec_stmt('SELECT user_id, username, profile_picture FROM user WHERE user_id = ?', 'i', $essay['author_id'])->fetch_assoc(); + + $html = ' +
+
+

' . $essay['title'] . '

+
+
+ Written by: ' . $author['username'] . ' + Published: ' . $essay['published_at'] . ' + Updated: ' . $essay['updated_at'] . ' +
+
+ +
+
+
+
+

' . $essay['excerpt'] . '

+
+ Read More +
' . $topics_html . '
+
+
+ '; + + return $html; +} diff --git a/components/foot.php b/components/core/foot.php similarity index 100% rename from components/foot.php rename to components/core/foot.php diff --git a/components/head.php b/components/core/head.php similarity index 86% rename from components/head.php rename to components/core/head.php index 73ac6b6..575cd78 100644 --- a/components/head.php +++ b/components/core/head.php @@ -7,11 +7,11 @@ '; - else +else echo ''; - ?> +?> diff --git a/components/nav.php b/components/core/nav.php similarity index 72% rename from components/nav.php rename to components/core/nav.php index 1b59cfb..feb2b89 100644 --- a/components/nav.php +++ b/components/core/nav.php @@ -6,20 +6,20 @@ $nav = ' '; - return $content; + return $content; } diff --git a/scripts/dummy_data.sql b/scripts/dummy_data.sql deleted file mode 100644 index ac688c2..0000000 --- a/scripts/dummy_data.sql +++ /dev/null @@ -1,14 +0,0 @@ -USE vintagecoding; - -INSERT INTO user (user_id, username, email, password_hash, role_id, profile_picture) - VALUES (5749248, 'jsmith', 'jsmith@email.com', 'f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b', 4, '2025.jpg'), - (5789203, 'dummy', 'dummy@iamdumb.com', 'f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b', 4, '2025.jpg'), - (5723948, 'writer', 'writer@something.com', 'f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b', 3, '5723948.jpg') -; - -INSERT INTO article (author_id, title, excerpt, published_at) - VALUES (2025, 'Testing vintagecoding.net', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas a mauris sit amet dui pellentesque sodales. Pellentesque non viverra leo, et congue metus.', CURRENT_TIMESTAMP), - (5723948, 'Something Else', 'Here goes some random crap. Lorem ipsum akdfljskjlag IDK what to put here. I am just filling the space requirement. Blah blah blah blah, what does this look like? Is vintagecoding.net cool?', CURRENT_TIMESTAMP) -; - -INSERT INTO article_tags (article_id, tag_id) VALUES (21, 2), (21, 6); diff --git a/scripts/init.cypher b/scripts/init.cypher deleted file mode 100644 index 5a17b2b..0000000 --- a/scripts/init.cypher +++ /dev/null @@ -1,209 +0,0 @@ -CREATE - (owner:Role {name: 'Owner'}), - (developer:Role {name: 'Developer'}), - (user:Role {name: 'User'}), - (essay:essay_postType {name: 'Essay'}), - (moment:essay_postType {name: 'Moment'}), - (thought:essay_postType {name: 'Thought'}), - (web_dev:essay_postCategory {name: 'Web-dev'}), - (devlog:essay_postCategory {name: 'Devlog'}), - (published:essay_postStatus {name: 'Published'}), - (draft:essay_postStatus {name: 'Draft'}), - (open:SpaceType {name: 'Open'}), - (public:SpaceType {name: 'Public'}), - (shared:SpaceType {name: 'Shared'}), - (private:SpaceType {name: 'Private'}), - (open_space:Space - { - name: 'Open Space', - description: 'World Wide access, no account required.', - created_at: datetime() - })- - [:SPACE_TYPE]-> - (open), - (public_space:Space - { - name: 'Public Space', - description: 'Available to everyone with an account.', - created_at: datetime() - })- - [:SPACE_TYPE]-> - (public), - (shared_space:Space - { - name: 'Dummy Shared Space', - description: 'Dummy shared space. Weird.', - created_at: datetime() - })- - [:SPACE_TYPE]-> - (shared), - (owner_space:Space - { - name: 'Owner Space', - description: 'My space. Wait. Is that what this is?', - created_at: datetime() - })- - [:SPACE_TYPE]-> - (private), - (dummy_space:Space - { - name: 'Dummy Space', - description: 'Dummy space. Weird.', - created_at: datetime() - })- - [:SPACE_TYPE]-> - (private), - (owner_user:User - { - username: 'joshashton', - email: 'me@joshashton.dev', - password_hash: - '057ba03d6c44104863dc7361fe4578965d1887360f90a0895882e58a6248fc86', - created_at: datetime(), - profile_picture: '2025.jpg' - })- - [:USER_TYPE]-> - (owner), - (owner_user)-[:USER_TYPE]->(developer), - (owner_user)-[:USER_TYPE]->(user), - (owner_user)-[:SPACE_MEMBER]->(open_space), - (owner_user)-[:SPACE_MEMBER]->(owner_space), - (owner_user)-[:SPACE_MEMBER]->(shared_space), - (dummy_user:User - { - username: 'dummy', - email: 'joshuatashton@gmail.com', - password_hash: - '057ba03d6c44104863dc7361fe4578965d1887360f90a0895882e58a6248fc86', - created_at: datetime(), - profile_picture: '2025.jpg' - })- - [:USER_TYPE]-> - (user), - (dummy_user)-[:FOLLOWS {start_at: datetime()}]->(owner_user), - (dummy_user)-[:SPACE_MEMBER]->(open_space), - (dummy_user)-[:SPACE_MEMBER]->(dummy_space), - (dummy_user)-[:SPACE_MEMBER]->(shared_space), - (essay_post:Post - { - title: 'Building vintagecoding.net', - view_count: 0, - excerpt: - 'Follow the journey of the creation of vintagecoding.net, where I discuss roadblocks and solutions. Hello world!', - content: - " -#### Overview: -This site is built in PHP with MariaDB and a Golang HTTP server. Composer is being used to supply the following PHP packages: - -- `vlucas/phpdotenv` -- `erusev/parsedown` -- `wildbit/postmark-php` - ---- -#### 19/03/2025 - Foundations: -Tonight, I [pushed](https://github.com/quaxlyqueen/vintagecoding.net/tree/5af0547864f730091c98d64cc18014bc69d0a041) the first major feature implementation. The database is designed, and a component has been created for article cards, and markdown parsing into HTML for the articles themselves. - ---- -#### 20/03/2025 - Refining UX: -Just now, I [pushed](https://github.com/quaxlyqueen/vintagecoding.net/tree/c0fc9e9d0053975837ae48fa1db93b5095d33146) the second major feature implementation. I've narrowed in on the database design with small tweaks, setup some dynamic page loading on articles.php, created a navigation bar, a dark/light mode button, and styled everything up. It's shaping up pretty well! - -There's a lot more work to be done. The entire login system needs to be implemented, user homepages, account management, admin panels, the article composer...the list goes on. - ---- -#### 25/03/2025 - Account Management: -Just now, I [pushed](https://github.com/quaxlyqueen/vintagecoding.net/tree/9a45075010434a4dedd0c5d48a78ff6be3bcfddc) the third major feature implementation: account management! Not the most exciting thing ever, but it is important for the long term success of this project. With that completed, v0.0.9 is completed! Though I prematurely labeled the commit v0.1.0...But in any case there's a lot to go before release. - -There is now a sign up and login user flow, a user page with basic settings, and authorization for accessing things like the article composer. Oh yeah, I got a basic version of that running in [this commit](https://github.com/quaxlyqueen/vintagecoding.net/tree/0a27cd1f5cd02735d06a64090a05937a5d6bcf3a) a few hours ago. - -Two major tasks remain prior to v0.1.0. First, I still need to finish implementing the image upload on new sign ups (and for changing a profile picture), but that should be relatively minor. Second, I need to use author information to flesh out article pages. - -I'm now setting my sights on v0.2.0, and the myriad of features I'm planning on implementing for that. It'll be the account and customization overhaul. Tools for the owner and administrator(s), enabling users to request their role to be changed to contributor or administrator, user home pages with their own custom CSS, the ability to theme the site however they please, and mobile support! I'm excited just thinking about it. It'll require a MAJOR overhaul of the CSS, but thanks to the minimalist aesthetic, the CSS file is only a few hundred disorganized lines. - -I've put maybe 30 hours into this project so far. I'm not sure if that's good or bad, but not getting crazy with the CSS has saved a **ton** of time. And, I really like how it's looking. I'm really excited about this project and I am giddy to see how much I can get implemented by the release on 25/05/2025 -- two months! I'm starting an *unpaid* internship tomorrow, so I probably won't have the time to put 30 hours a week into this. For now, I'm just excited to see what the future holds. - ---- -#### 01/04/2025 - Basic Security & Administration: -Earlier today, I [pushed](https://github.com/quaxlyqueen/vintagecoding.net/tree/cec9091ee01839b133ecbbc5616463a759837dda) a major security fix along with getting CI/CD setup, and last night I [pushed](https://github.com/quaxlyqueen/vintagecoding.net/tree/b9bb620ae01134431ff1941e7577ba58eee84e42) a secure overhaul of the 'Remember Me' functionality. I didn't write about it then, but a few days ago I also [pushed](https://github.com/quaxlyqueen/vintagecoding.net/tree/ff7e799b7fd2822cf27e0afe47274de222b2b508) the image upload along with a image cropper. Still buggy as hell, but its a start. And a couple days ago I [committed](https://github.com/quaxlyqueen/vintagecoding.net/tree/e6735a9a206a207b6fab704785451a6309944ddd) admin functionality to manage users and articles. I would've thought I'd slow down, but I'm just trucking along. - -Specifically, the 'Remember Me' functionality is now using a token system, a randomly generated 1024 bit string, associated with the user ID and hashed values of both the remote address and (if used) forwarded for headers. If these don't match what is stored in the database, this should mean that the client browser/device is not the original browser/device used to create the token. - -For the other security fix, I converted all of the `mysqli` statements to prepared statements. This is to mitigate the risk of SQL Injection attacks. Particularly since this site is live (though I'm not promoting it so it is still 'unreleased'), it is important to ensure that despite the lack of sensitive information, would-be attackers are unable to access user information, gain admin access, or compromise the server in any way. - -Onto the user home pages! Here's a general breakdown of what I have in mind. Every user has a homepage that they can customize the CSS for. Effectively, give the users a place to make their own. Eventually even change the layout and structure of their page. For now, the page is going to have some pretty simple information: a list of articles written by the user, the number of articles they have read, and the number their articles have been read. More is to come in the future, but that's a long way out. - ---- -#### 08/04/2025 - Image Cropping & Editing Articles: -Yesterday, I [pushed](https://github.com/quaxlyqueen/vintagecoding.net/tree/e5b3e048b8dc6731fa383778c26eba47186e5310) an update to the image cropper. In other words, it *actually* works now lol. I was driving home from class and I just realized that I needed to both offset the position from the window to the image, and scale the coordinates based on the size of the image. And just a moment ago, I [pushed](https://github.com/quaxlyqueen/vintagecoding.net/tree/e829e2e56695af5b77074f926c8d0c270e501f4b) editing articles. And here and there, I've worked on some basic Cross Site Scripting preventative measures, honeypots on signup to help prevent bots, and basic mobile responsiveness. - -With some work being done with security now, I'm having second thoughts about allowing users to have custom CSS on their homepages. I'll punt that off till later. - -All in all, things are going smoothly! I'm pretty proud of what I've built so far, and it has been quite educational. From prepared SQL statements, to XSS, form processing, and secure 'remember me' functionality. I'm not sure how much this site will be used by anyone else, and if that is the case I'll eventually just use this as a personal blog. - ---- -#### 12/04/2025 - Enhanced Markdown Editing Experience: -This was my first foray into the world of AJAX. I just [pushed](https://github.com/quaxlyqueen/vintagecoding.net/tree/fb35e9322647878386bcc3be572cd404b38fd91b) the implementation of a markdown preview while composing or editing articles. The possibilities are **very** exciting, I'm already thinking of other places that the AJAX would be useful for. - -Next, I'm looking to introduce Vim motions to the site, for general navigation and also for article composition and editing. It'll be an interesting challenge I think, and I could use the JavaScript practice. - -I haven't said this previously, but I am *loving* PHP. Such a joy to work with, and pair that with AJAX for a very interactive experience? Phenomenal DX. I've been looking at an interesting project, [NativePHP](https://nativephp.com), that I'd like to eventually use for another project. - " - })- - [:STATUS]-> - (published), - (essay_post)-[:POST_TYPE]->(essay), - (essay_post)-[:POST_CATEGORY]->(web_dev), - (essay_post)-[:POST_CATEGORY]->(devlog), - (essay_post)-[:IN_SPACE]->(open_space), - (owner_user)-[:WRITTEN {created_at: datetime(), updated_at: datetime()}]-> - (essay_post), - (moment_post:Post - { - media_filepath: 'moments/open.png', - text_content: - 'A hub for everything. A social network and messaging hub. A documentation hub. A photos and video hub. A note taking and task management hub. A private hub. An AI hub. Welcome to the.hub.' - })- - [:STATUS]-> - (published), - (moment_post)-[:POST_TYPE]->(moment), - (moment_post)-[:POST_CATEGORY]->(devlog), - (moment_post)-[:IN_SPACE]->(shared_space), - (moment_post_2:Post - { - media_filepath: 'moments/test.png', - text_content: - 'Here is a photo from a hike I went on a while back. Nothing quite beats the look over a long trail.' - })- - [:STATUS]-> - (published), - (moment_post_2)-[:POST_TYPE]->(moment), - (moment_post_2)-[:POST_CATEGORY]->(devlog), - (moment_post_2)-[:IN_SPACE]->(shared_space), - (owner_user)-[:WRITTEN {created_at: datetime(), updated_at: datetime()}]-> - (moment_post_2), - (thought_post:Post - { - content: - 'I wonder if this is really smart or really dumb. Guess we will see...' - })- - [:STATUS]-> - (published), - (thought_post)-[:POST_TYPE]->(thought), - (thought_post)-[:POST_CATEGORY]->(devlog), - (thought_post)-[:IN_SPACE]->(owner_space), - (owner_user)-[:WRITTEN {created_at: datetime(), updated_at: datetime()}]-> - (thought_post), - (thought_post_2:Post - {content: 'I wonder if it was a bad idea to share my idea with Izzi.'})- - [:STATUS]-> - (draft), - (thought_post_2)-[:POST_TYPE]->(thought), - (thought_post_2)-[:POST_CATEGORY]->(devlog), - (thought_post_2)-[:IN_SPACE]->(owner_space), - (owner_user)-[:WRITTEN {created_at: datetime(), updated_at: datetime()}]-> - (thought_post_2), - (thought_post_3:Post {content: 'Hello cruel world!'})-[:STATUS]->(published), - (thought_post_3)-[:POST_TYPE]->(thought), - (thought_post_3)-[:POST_CATEGORY]->(devlog), - (thought_post_3)-[:IN_SPACE]->(public_space), - (owner_user)-[:WRITTEN {created_at: datetime(), updated_at: datetime()}]-> - (thought_post_3); diff --git a/scripts/init.sql b/scripts/init.sql deleted file mode 100644 index e2620a9..0000000 --- a/scripts/init.sql +++ /dev/null @@ -1,130 +0,0 @@ -CREATE DATABASE vintagecoding; -USE vintagecoding; - -CREATE TABLE roles ( - role_id INT PRIMARY KEY AUTO_INCREMENT, - role VARCHAR(16) NOT NULL -); - -INSERT INTO roles (role) - VALUES ('owner'), ('developer'), ('user'); - -CREATE TABLE user ( - user_id INT PRIMARY KEY, - username VARCHAR(32) NOT NULL UNIQUE, - email VARCHAR(64) NOT NULL UNIQUE, - password_hash VARCHAR(256) NOT NULL, - role_id INT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - last_active TIMESTAMP DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - is_active BOOLEAN DEFAULT FALSE, - profile_picture VARCHAR(256), - bio VARCHAR(256), - website VARCHAR(64), - FOREIGN KEY (role_id) REFERENCES roles (role_id) ON DELETE CASCADE -); - -INSERT INTO user (user_id, username, email, password_hash, role_id, profile_picture) - VALUES (1, 'quaxlyqueen', 'me@joshashton.dev', '057ba03d6c44104863dc7361fe4578965d1887360f90a0895882e58a6248fc86', 1, '2025.jpg'); - -CREATE TABLE remember_user ( - token VARCHAR(128) PRIMARY KEY, - user_id INT NOT NULL, - remote_addr VARCHAR(64) NOT NULL, - http_forward VARCHAR(64), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES user (user_id) ON DELETE CASCADE -); -- TODO: Need cron job to auto-delete after 30 days of inactivity - -CREATE TABLE post_types ( - post_type_id INT PRIMARY KEY AUTO_INCREMENT, - post_type VARCHAR(16) -); - -INSERT INTO post_types (post_type) - VALUES ('article'), ('moment'), ('thought'); - -CREATE TABLE post ( - post_id INT PRIMARY KEY, - author_id INT NOT NULL, - post_type INT NOT NULL, - title VARCHAR(26) NOT NULL, - slug VARCHAR(32), - read_count INT DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - published_at TIMESTAMP DEFAULT NULL, - published BOOLEAN DEFAULT TRUE, - excerpt VARCHAR(256), - FOREIGN KEY (author_id) REFERENCES user(user_id) ON DELETE CASCADE, - FOREIGN KEY (post_type) REFERENCES post_types(post_type_id) -); - -INSERT INTO post (author_id, post_type, title, excerpt, published_at) - VALUES (1, 1, 'Building vintagecoding.net', 'A devlog of the creation of vintagecoding.net and a test at the formatting of post cards and posts. Let the great experiment begin!', CURRENT_TIMESTAMP); - -CREATE TABLE categories ( - category_id INT PRIMARY KEY AUTO_INCREMENT, - category VARCHAR(16) NOT NULL -); - -INSERT INTO categories (category) - VALUES ('web-dev'), ('devlog'); - -CREATE TABLE post_categories ( - post_id INT NOT NULL, - category_id INT NOT NULL, - PRIMARY KEY (post_id, category_id), - FOREIGN KEY (post_id) REFERENCES post(post_id) ON DELETE CASCADE, - FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE CASCADE -); - -INSERT INTO post_tags (post_id, tag_id) VALUES (1, 1), (1, 2); - -CREATE TABLE follows ( - follower_user_id INT NOT NULL, - following_user_id INT NOT NULL, - notify_user BOOLEAN DEFAULT FALSE, - followed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (follower_user_id, following_user_id), - FOREIGN KEY (follower_user_id) REFERENCES user(user_id) ON DELETE CASCADE, - FOREIGN KEY (following_user_id) REFERENCES user(user_id) ON DELETE CASCADE -); - -CREATE TABLE space_types ( - space_type_id INT PRIMARY KEY AUTO_INCREMENT, - space_type VARCHAR(16) -); - -INSERT INTO space_types (space_type) - VALUES ('open', 'public', 'shared', 'private'); - -CREATE TABLE spaces ( - space_id INT PRIMARY KEY AUTO_INCREMENT, - space_type INT NOT NULL, - name VARCHAR(64) NOT NULL, - slug VARCHAR(64) UNIQUE, - description TEXT, - parent_space_id INT DEFAULT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (parent_space_id) REFERENCES spaces(space_id) ON DELETE SET NULL, - FOREIGN KEY (space_type) REFERENCES space_types(space_type_id) -); - -CREATE TABLE space_posts ( - post_id INT NOT NULL, - space_id INT NOT NULL, - PRIMARY KEY (post_id, space_id), - FOREIGN KEY (post_id) REFERENCES post(post_id) ON DELETE CASCADE, - FOREIGN KEY (space_id) REFERENCES spaces(space_id) ON DELETE CASCADE -); - -CREATE TABLE space_members ( - space_id INT NOT NULL, - user_id INT NOT NULL, - added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (space_id, user_id), - FOREIGN KEY (space_id) REFERENCES spaces(space_id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES user(user_id) ON DELETE CASCADE -); diff --git a/scripts/init_db.sh b/scripts/init_db.sh deleted file mode 100755 index fbbb69c..0000000 --- a/scripts/init_db.sh +++ /dev/null @@ -1,54 +0,0 @@ -ARGS_COUNT=$# -help () { - echo "CLI Tool used to initialize the database for vintagecoding.net." - echo "With no arguments, it creates the application user in MariaDB," - echo "and creates the following tables:" - echo "- roles (populated);" - echo "- tags (populated);" - echo "- user;" - echo "- articles;" - echo "- article_tags;" - echo "" - echo "The following flags are accepted:" - echo " init_db.sh { --help | -h }" - echo " init_db.sh { --delete | -d } { --verbose | -v }" -} - -if [[ $ARGS_COUNT > 0 ]]; then - if [[ "$1" == "--help" || "$1" == "-h" ]]; then - help - exit - fi - - # Delete the existing database and start over. - if [[ "$1" == "--delete" || "$1" == "-d" ]]; then - if [[ "$2" == "--verbose" || "$2" == "-v" ]]; then - echo "Deleting the existing database vintagecoding..." - fi - echo "vintage user pw" - mariadb -u vintage -p -e "DROP DATABASE vintagecoding;" - - if [[ "$2" == "--verbose" || "$2" == "-v" ]]; then - echo "Deleting the vintage user..." - fi - echo "root user pw" - mariadb -u root -p -e "DROP USER vintage@localhost;" - - if [[ "$2" == "--verbose" || "$2" == "-v" ]]; then - echo "Creating the vintage user..." - fi - echo "root user pw" - mariadb -u root -p < setup.sql; - - if [[ "$2" == "--verbose" || "$2" == "-v" ]]; then - echo "Creating (and populating enum/static data) tables..." - fi - echo "vintage user pw" - mariadb -u vintage -p < init.sql; - - exit - fi -fi - -#mariadb -u root -p < setup.sql; -mariadb -u vintage -p < init.sql; diff --git a/scripts/setup.sql b/scripts/setup.sql deleted file mode 100644 index b0dece4..0000000 --- a/scripts/setup.sql +++ /dev/null @@ -1,3 +0,0 @@ ---This must be ran as the root user in MariaDB -CREATE USER vintage@localhost IDENTIFIED BY 'changeme'; -GRANT ALL PRIVILEGES ON vintagecoding.* TO vintage@localhost IDENTIFIED BY 'changeme'; diff --git a/spaces.php b/spaces.php new file mode 100644 index 0000000..f7b3103 --- /dev/null +++ b/spaces.php @@ -0,0 +1,47 @@ +' . $space->getId() . '

+ '; + + return $html; +} diff --git a/todo.php b/todo.php deleted file mode 100644 index f3a6698..0000000 --- a/todo.php +++ /dev/null @@ -1,29 +0,0 @@ -