This commit is contained in:
2025-05-01 14:42:12 -06:00
parent 693d11c506
commit bd59f428ba
13 changed files with 288 additions and 124 deletions
+1
View File
@@ -4,5 +4,6 @@ go 1.24.2
require ( require (
github.com/gorilla/mux v1.8.1 // 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 github.com/neo4j/neo4j-go-driver/v5 v5.28.0 // indirect
) )
+2
View File
@@ -1,4 +1,6 @@
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 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 h1:chDT68PHNa8JZRmjSkGzAbk1weLWo4rMtDvccvpobg0=
github.com/neo4j/neo4j-go-driver/v5 v5.28.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k= github.com/neo4j/neo4j-go-driver/v5 v5.28.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
+13 -5
View File
@@ -14,6 +14,7 @@ import (
"time" "time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/joho/godotenv"
"github.com/neo4j/neo4j-go-driver/v5/neo4j" "github.com/neo4j/neo4j-go-driver/v5/neo4j"
) )
@@ -34,7 +35,7 @@ func serve() {
var wg sync.WaitGroup var wg sync.WaitGroup
driver_ctx := context.Background() 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 { if err != nil {
panic(err) panic(err)
} }
@@ -43,18 +44,19 @@ func serve() {
err = driver.VerifyConnectivity(driver_ctx) err = driver.VerifyConnectivity(driver_ctx)
if err != nil { if err != nil {
log.Println("Error connecting to neo4j.")
panic(err) panic(err)
} }
fmt.Println("neo4j connection established.") fmt.Println("neo4j connection established.")
rsrc_endpoints := []string{ rsrc_endpoints := []string{
"/auth",
"/users", "/users",
"/users", "/users",
"/users/{user_id}", "/users/{user_id}",
"/users/{user_id}", "/users/{user_id}",
"/users/{user_id}", "/users/{user_id}",
"/users/{user_id}",
"/users/{user_id}/posts", "/users/{user_id}/posts",
"/users/{user_id}/spaces", "/users/{user_id}/spaces",
"/spaces", "/spaces",
@@ -74,9 +76,9 @@ func serve() {
methods := []string{ methods := []string{
http.MethodPost, http.MethodPost,
http.MethodGet,
http.MethodGet,
http.MethodPost, http.MethodPost,
http.MethodGet,
http.MethodGet,
http.MethodPatch, http.MethodPatch,
http.MethodDelete, http.MethodDelete,
http.MethodGet, http.MethodGet,
@@ -97,10 +99,10 @@ func serve() {
} }
functions := []func(http.ResponseWriter, *http.Request){ functions := []func(http.ResponseWriter, *http.Request){
auth_user(driver, driver_ctx),
create_new_user(driver, driver_ctx), create_new_user(driver, driver_ctx),
retrieve_all_users(driver, driver_ctx), retrieve_all_users(driver, driver_ctx),
retrieve_user(driver, driver_ctx), retrieve_user(driver, driver_ctx),
auth_user(driver, driver_ctx),
update_user(driver, driver_ctx), update_user(driver, driver_ctx),
delete_user(driver, driver_ctx), delete_user(driver, driver_ctx),
retrieve_users_posts(driver, driver_ctx), retrieve_users_posts(driver, driver_ctx),
@@ -168,5 +170,11 @@ func serve() {
} }
func main() { func main() {
err := godotenv.Load("../.env")
if err != nil {
log.Println("Error loading .env.")
return
}
serve() serve()
} }
+106 -6
View File
@@ -3,25 +3,113 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
// "fmt" "fmt"
"io"
"os"
"strconv" "strconv"
//"io" //"io"
// "log" "log"
"net/http" "net/http"
// "os" // "os"
// "os/signal" // "os/signal"
// "reflect" // "reflect"
"math/rand"
"strings" "strings"
// "sync" // "sync"
// "time" // "time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/neo4j/neo4j-go-driver/v5/neo4j" "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 { func create_new_user(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { 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 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 { func retrieve_all_users(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc {
@@ -32,15 +120,16 @@ func retrieve_all_users(driver neo4j.DriverWithContext, driver_ctx context.Conte
func retrieve_user(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { func retrieve_user(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { 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, 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", "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{ map[string]any{
"user_id": user_id, "user_id": id,
}, neo4j.EagerResultTransformer, }, neo4j.EagerResultTransformer,
neo4j.ExecuteQueryWithDatabase("neo4j")) neo4j.ExecuteQueryWithDatabase("neo4j"))
if len(result.Records) > 0 {
record := result.Records[0] record := result.Records[0]
vals := record.AsMap() vals := record.AsMap()
user := struct { user := struct {
@@ -58,12 +147,20 @@ func retrieve_user(driver neo4j.DriverWithContext, driver_ctx context.Context) h
return 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: Retrieval")
} else {
return
}
} }
} }
func auth_user(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc { func auth_user(driver neo4j.DriverWithContext, driver_ctx context.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:2025")
username := r.FormValue("username") username := r.FormValue("username")
password_hash := r.FormValue("password_hash") password_hash := r.FormValue("password_hash")
@@ -96,7 +193,10 @@ func auth_user(driver neo4j.DriverWithContext, driver_ctx context.Context) http.
return 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 { } else {
return return
} }
+2
View File
@@ -54,6 +54,8 @@ if (isset($_SESSION['username'])) {
$nav .= ' $nav .= '
</div> </div>
</div> </div>
<script src="js/nav.js" defer></script>
</nav> </nav>
'; ';
+5 -2
View File
@@ -4,11 +4,12 @@ function login_form($error_msg = '')
$form = ' $form = '
<div class="form_card"> <div class="form_card">
<h3 class="underline">Log In</h3> <h3 class="underline">Log In</h3>
<form method="post"> <form method="post" id="login_form">
<input type="hidden" name="form_id" value="login_form"> <input type="hidden" name="form_id" value="login_form">
<input type="hidden" name="password_hash" value="" id="password_hash">
<label for="username">Username / Email</label> <label for="username">Username / Email</label>
<input id="username" name="username" spellcheck="false" required> <input id="username" name="username" spellcheck="false" required autofocus>
<label for="password">Password</label> <label for="password">Password</label>
<input id="password" name="password" type="password" spellcheck="false" required> <input id="password" name="password" type="password" spellcheck="false" required>
@@ -21,6 +22,8 @@ function login_form($error_msg = '')
<input class="button" type="submit" value="Log In"> <input class="button" type="submit" value="Log In">
' . $error_msg . ' ' . $error_msg . '
</form> </form>
<script src="js/login.js" defer></script>
</div> </div>
'; ';
+34
View File
@@ -172,3 +172,37 @@
.modal_activation_area:hover { .modal_activation_area:hover {
cursor: pointer; 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;
}
}
+19
View File
@@ -6,6 +6,9 @@ if (isset($_POST['ajax_id'])) {
case 'todo': case 'todo':
handle_todo(); handle_todo();
break; break;
case 'login':
handle_login();
break;
} }
} else { } else {
$response = [ $response = [
@@ -15,6 +18,22 @@ if (isset($_POST['ajax_id'])) {
echo json_encode($response); 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() function handle_todo()
{ {
include 'functions/todo_functions.php'; include 'functions/todo_functions.php';
-32
View File
@@ -40,38 +40,6 @@ function logout()
header('Location: .'); 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 = '
<div class="error">
Username and/or Password are invalid!
</div>
';
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 . '<br>';
// 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) function create_user($username, $email, $password, $verify_password, $role_id, $upload = null, $x = null, $y = null, $crop_width = null, $honeypot = null)
{ {
if (!empty($honeypot)) { if (!empty($honeypot)) {
+1 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
session_start(); session_start();
header("Content-Security-Policy: default-src 'self'; font-src 'self' https://www.nerdfonts.com/assets/fonts/Symbols-2048-em%20Nerd%20Font%20Complete.woff2; style-src 'self' 'unsafe-inline' https://nerdfonts.com/assets/css/webfont.css https://www.nerdfonts.com/assets/css/webfont.css; script-src 'self' 'unsafe-inline'; img-src 'self';"); header("Content-Security-Policy: default-src 'self' localhost:7477; font-src 'self' https://www.nerdfonts.com/assets/fonts/Symbols-2048-em%20Nerd%20Font%20Complete.woff2; style-src 'self' 'unsafe-inline' https://nerdfonts.com/assets/css/webfont.css https://www.nerdfonts.com/assets/css/webfont.css; script-src 'self' 'unsafe-inline'; img-src 'self';");
// Initialize Composer. // Initialize Composer.
require_once 'vendor/autoload.php'; require_once 'vendor/autoload.php';
+72
View File
@@ -0,0 +1,72 @@
const form = document.querySelector('#login_form');
const username_field = form.querySelector('#username');
const password_field = form.querySelector('#password');
const password_hash_field = form.querySelector('#password_hash');
const submit_button = form.querySelector('input[type="submit"]');
submit_button.addEventListener('click', async (e) => {
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;
}
+16
View File
@@ -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');
}
});
});
+2 -63
View File
@@ -15,72 +15,11 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
var todos = document.getElementsByClassName('todo_item'); var todos = document.getElementsByClassName('todo_item');
var action_items = (document.getElementById('todo_actions')).querySelectorAll('button'); //var todos = document.querySelectorAll('todo_item');
// 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;
}
});
}
for (let i = 0; i < todos.length; i++) { 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"]'); var checkbox = item.querySelector('input[type="checkbox"]');
checkbox.todo_id = item.querySelector('input[name="todo_id"]').value; checkbox.todo_id = item.querySelector('input[name="todo_id"]').value;
checkbox.is_checked = checkbox.checked; checkbox.is_checked = checkbox.checked;