From bd59f428ba04fe90b10b78a648404d5f02a76f26 Mon Sep 17 00:00:00 2001 From: Josh Ashton Date: Thu, 1 May 2025 14:42:12 -0600 Subject: [PATCH] sync --- backend/go.mod | 1 + backend/go.sum | 2 + backend/router.go | 18 ++-- backend/users.go | 142 +++++++++++++++++++++++++++----- components/nav.php | 2 + components/user/login.php | 7 +- css/rewrite.css | 34 ++++++++ director.php | 19 +++++ functions/account_functions.php | 32 ------- functions/init.php | 2 +- js/login.js | 72 ++++++++++++++++ js/nav.js | 16 ++++ js/todo.js | 65 +-------------- 13 files changed, 288 insertions(+), 124 deletions(-) create mode 100644 js/login.js create mode 100644 js/nav.js diff --git a/backend/go.mod b/backend/go.mod index 633f1f8..0d659af 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,5 +4,6 @@ go 1.24.2 require ( github.com/gorilla/mux v1.8.1 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/neo4j/neo4j-go-driver/v5 v5.28.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 2cbaca1..bd69801 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,4 +1,6 @@ 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/router.go b/backend/router.go index 0f65a56..66bfe7d 100644 --- a/backend/router.go +++ b/backend/router.go @@ -14,6 +14,7 @@ import ( "time" "github.com/gorilla/mux" + "github.com/joho/godotenv" "github.com/neo4j/neo4j-go-driver/v5/neo4j" ) @@ -34,7 +35,7 @@ func serve() { var wg sync.WaitGroup driver_ctx := context.Background() - driver, err := neo4j.NewDriverWithContext("bolt://localhost:7687", neo4j.BasicAuth("neo4j", "jzLbsy2C23WHzQ-", "")) + driver, err := neo4j.NewDriverWithContext(os.Getenv("NEO4J_BOLT"), neo4j.BasicAuth("neo4j", os.Getenv("NEO4J_PASSWORD"), "")) if err != nil { panic(err) } @@ -43,18 +44,19 @@ func serve() { 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}", "/users/{user_id}/posts", "/users/{user_id}/spaces", "/spaces", @@ -74,9 +76,9 @@ func serve() { methods := []string{ http.MethodPost, - http.MethodGet, - http.MethodGet, http.MethodPost, + http.MethodGet, + http.MethodGet, http.MethodPatch, http.MethodDelete, http.MethodGet, @@ -97,10 +99,10 @@ func serve() { } 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), - auth_user(driver, driver_ctx), update_user(driver, driver_ctx), delete_user(driver, driver_ctx), retrieve_users_posts(driver, driver_ctx), @@ -168,5 +170,11 @@ func serve() { } func main() { + err := godotenv.Load("../.env") + if err != nil { + log.Println("Error loading .env.") + return + } + serve() } diff --git a/backend/users.go b/backend/users.go index f97a551..a21b452 100644 --- a/backend/users.go +++ b/backend/users.go @@ -3,24 +3,112 @@ package main import ( "context" "encoding/json" - // "fmt" + "fmt" + "io" + "os" "strconv" + //"io" - // "log" + "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) { - return + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:2025") + + err := r.ParseMultipartForm(10 << 20) // Limit to 10MB + if err != nil { + log.Println("Unable to parse form.") + } + + 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 + } } } @@ -32,38 +120,47 @@ func retrieve_all_users(driver neo4j.DriverWithContext, driver_ctx context.Conte func retrieve_user(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - user_id, _ := strconv.Atoi(mux.Vars(r)["user_id"]) + id, _ := strconv.Atoi(mux.Vars(r)["user_id"]) result, _ := neo4j.ExecuteQuery(driver_ctx, driver, "MATCH (u:User) WHERE id(u) = $user_id RETURN u.username AS username, u.email AS email, u.profile_picture AS profile_picture", map[string]any{ - "user_id": user_id, + "user_id": id, }, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) - record := result.Records[0] - vals := record.AsMap() - user := struct { - Username string `json:"username"` - Email string `json:"email"` - ProfilePicture string `json:"profile_picture"` - }{ - Username: vals["username"].(string), - Email: vals["email"].(string), - ProfilePicture: vals["profile_picture"].(string), - } + 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 { + 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 } - - send_response(w, json) } } 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") @@ -96,7 +193,10 @@ func auth_user(driver neo4j.DriverWithContext, driver_ctx context.Context) http. return } - send_response(w, json) + 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 } diff --git a/components/nav.php b/components/nav.php index 0e08abe..1b59cfb 100644 --- a/components/nav.php +++ b/components/nav.php @@ -54,6 +54,8 @@ if (isset($_SESSION['username'])) { $nav .= ' + + '; diff --git a/components/user/login.php b/components/user/login.php index 613e589..5681f61 100644 --- a/components/user/login.php +++ b/components/user/login.php @@ -4,11 +4,12 @@ function login_form($error_msg = '') $form = '

Log In

-
+ + - + @@ -21,6 +22,8 @@ function login_form($error_msg = '') ' . $error_msg . '
+ +
'; diff --git a/css/rewrite.css b/css/rewrite.css index d342297..2d19761 100644 --- a/css/rewrite.css +++ b/css/rewrite.css @@ -172,3 +172,37 @@ .modal_activation_area:hover { cursor: pointer; } + +/********** Animations **********/ +@view-transition { + navigation: auto; + animation-duration: 1s; +} + +::view-transition-old(root) { + animation: fade-out 750ms forwards; +} + +::view-transition-new(root) { + animation: fade-in 750ms forwards; +} + +@keyframes fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes fade-out { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} diff --git a/director.php b/director.php index 85214ab..7dfb535 100644 --- a/director.php +++ b/director.php @@ -6,6 +6,9 @@ if (isset($_POST['ajax_id'])) { case 'todo': handle_todo(); break; + case 'login': + handle_login(); + break; } } else { $response = [ @@ -15,6 +18,22 @@ if (isset($_POST['ajax_id'])) { echo json_encode($response); } +function handle_login() +{ + $response = ['log' => []]; + + if (isset($_POST['user_id']) && isset($_POST['username']) && isset($_POST['role'])) { + $response['log'][] = 'Required parameters met. Logging user in.'; + $_SESSION['user_id'] = $_POST['user_id']; + $_SESSION['username'] = $_POST['username']; + $_SESSION['role_id'] = $_POST['role']; + + $response['redirect'] = 'articles.php?view=list'; + } + + echo json_encode($response); +} + function handle_todo() { include 'functions/todo_functions.php'; diff --git a/functions/account_functions.php b/functions/account_functions.php index 2b652a8..2c3f4a5 100644 --- a/functions/account_functions.php +++ b/functions/account_functions.php @@ -40,38 +40,6 @@ function logout() header('Location: .'); } -function login($username, $password, $stay_logged_in) -{ - $password_hash = hash('sha256', $password); - - $user = exec_stmt('SELECT user_id, username, role_id FROM user WHERE username = ? OR email = ? AND password_hash = ?', 'sss', $username, $username, $password_hash)->fetch_assoc(); - - if ($user == null) { - $error_msg = ' -
- Username and/or Password are invalid! -
- '; - - return $error_msg; - } - - $_SESSION['user_id'] = $user['user_id']; - $_SESSION['username'] = $user['username']; - $_SESSION['role_id'] = $user['role_id']; - - if ($stay_logged_in) { - $token = bin2hex(random_bytes(64)); - // echo $token . '
'; - // echo strlen($token); - setcookie('remember_user', $token, time() + 60 * 60 * 24 * 30, '/', '', true, true); - - exec_stmt('INSERT INTO remember_user (token, user_id, remote_addr, http_forward) VALUES (?, ?, ?, ?)', 'siss', $token, $user['user_id'], hash('sha256', $_SERVER['REMOTE_ADDR']), (isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? hash('sha256', $_SERVER['HTTP_X_FORWARDED_FOR']) : null)); - } - - return true; -} - function create_user($username, $email, $password, $verify_password, $role_id, $upload = null, $x = null, $y = null, $crop_width = null, $honeypot = null) { if (!empty($honeypot)) { diff --git a/functions/init.php b/functions/init.php index 9de7878..917c977 100644 --- a/functions/init.php +++ b/functions/init.php @@ -1,6 +1,6 @@ { + e.preventDefault(); + const password_hash = await hashString(password_field.value); + const urlParams = new URLSearchParams(document.location.search); + const redirect = decodeURI(urlParams.get('redirect')); + requestAuth(username_field.value, password_hash, redirect); +}); + +function requestAuth(username, password_hash, redirect) { + fetch('http://localhost:7477/auth', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: 'username=' + username + '&password_hash=' + password_hash, + }) + .then(response => { + if (!response.ok) { + console.log('Login Failed!'); + } + + return response.json(); // Parse response body as JSON + }) + .then(data => { + notifyServer(data["id"], username, 1, redirect); + }); +} + +function notifyServer(id, username, role, redirect) { + fetch('director.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: 'ajax_id=login&user_id=' + id + '&username=' + username + '&role=' + role, + }) + .then(response => { + return response.json(); + }) + .then(data => { + if ('startViewTransition' in document) { + document.startViewTransition(() => { + if (redirect == null) { + window.location.href = 'articles.php?view=list&sort=new'; + } else { + window.location.href = redirect; + } + }); + } else { + // Fallback for browsers that don't support View Transitions + if (redirect == null) { + window.location.href = 'articles.php?view=list&sort=new'; + } else { + window.location.href = redirect; + } + } + }); +} + +async function hashString(str) { + const encoded = new TextEncoder().encode(str); + const digest = await crypto.subtle.digest('SHA-256', encoded); + const hashArray = Array.from(new Uint8Array(digest)); + const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); + return hashHex; +} diff --git a/js/nav.js b/js/nav.js new file mode 100644 index 0000000..7c362f9 --- /dev/null +++ b/js/nav.js @@ -0,0 +1,16 @@ +const nav = document.querySelector('nav'); +const nav_links = nav.querySelectorAll('ul li a'); +const theme_toggle = nav.querySelector('button'); + +nav_links.forEach((el) => { + el.addEventListener('click', () => { + el.preventDefault(); + if ('startViewTransition' in document) { + document.startViewTransition(() => { + window.location.href = el.getAttribute('href'); + }); + } else { + window.location.href = el.getAttribute('href'); + } + }); +}); diff --git a/js/todo.js b/js/todo.js index 4374e7e..984c7ee 100644 --- a/js/todo.js +++ b/js/todo.js @@ -15,72 +15,11 @@ document.addEventListener('DOMContentLoaded', function() { var todos = document.getElementsByClassName('todo_item'); - var action_items = (document.getElementById('todo_actions')).querySelectorAll('button'); - - // General Modal handling - function handle_modal(modal) { - console.log('handling modal'); - let modal_close = modal.querySelector('.modal_close'); - let submit_button = modal.querySelector('input[type="submit"]'); - modal.style.display = 'block'; - - modal_close.addEventListener('click', function() { - modal.style.display = "none"; - }); - - submit_button.addEventListener('click', function(e) { - e.preventDefault(); - let todo = { - 'title': modal.querySelector('#title').value, - 'description': modal.querySelector('#description'), - 'priority': modal.querySelector('#priority'), - 'due_date': modal.querySelector('#due_date'), - }; - - fetch('director.php', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: 'ajax_id=todo&action_id=create_update_todo&todo_id=' + this.todo_id + '&todo=' + encodeURIComponent(JSON.stringify(todo)), - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response.json(); // Or handle other response formats - }) - .then(responseData => { - console.log('Response received:', responseData); - // Handle the server's response - }); - }); - } - - for (let i = 0; i < action_items.length; i++) { - let action = action_items[i]; - - action.addEventListener('click', function(e) { - e.preventDefault(); - - switch (action.value) { - case 'new': - let new_todo_item = document.querySelector('#new_todo_item'); - handle_modal(new_todo_item); - break; - } - }); - } + //var todos = document.querySelectorAll('todo_item'); for (let i = 0; i < todos.length; i++) { - let item = todos[i]; + var item = todos[i]; - // Detailed todo item view via a modal - let modal_button = item.querySelector('.modal_activation_area'); - let modal = item.querySelector('.modal'); - modal_button.addEventListener('click', () => handle_modal(modal)); - - // Checkboxes & AJAX for updating the DB var checkbox = item.querySelector('input[type="checkbox"]'); checkbox.todo_id = item.querySelector('input[name="todo_id"]').value; checkbox.is_checked = checkbox.checked;