From 6be3d1c2fd2b0426be8cfab2ff6c6b5b39c0893f Mon Sep 17 00:00:00 2001 From: Juno Date: Thu, 8 Jun 2023 12:17:15 -0400 Subject: [PATCH 1/4] add photos stuff in backend also start frontend stuff for displaying and editing them --- .../jmeifert/fsuvius/FsuviusController.java | 70 +++-- .../fsuvius/data/DatabaseController.java | 272 ++++++++++++++++++ .../jmeifert/fsuvius/user/UserRegistry.java | 145 ---------- src/main/resources/static/index.js | 15 +- 4 files changed, 334 insertions(+), 168 deletions(-) create mode 100644 src/main/java/org/jmeifert/fsuvius/data/DatabaseController.java delete mode 100644 src/main/java/org/jmeifert/fsuvius/user/UserRegistry.java diff --git a/src/main/java/org/jmeifert/fsuvius/FsuviusController.java b/src/main/java/org/jmeifert/fsuvius/FsuviusController.java index 956d7af..83475f7 100644 --- a/src/main/java/org/jmeifert/fsuvius/FsuviusController.java +++ b/src/main/java/org/jmeifert/fsuvius/FsuviusController.java @@ -6,9 +6,10 @@ import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bucket; import io.github.bucket4j.Refill; +import org.jmeifert.fsuvius.data.DatabaseController; +import org.jmeifert.fsuvius.error.NotFoundException; import org.jmeifert.fsuvius.error.RateLimitException; import org.jmeifert.fsuvius.user.User; -import org.jmeifert.fsuvius.user.UserRegistry; import org.jmeifert.fsuvius.util.Log; import org.springframework.web.bind.annotation.*; @@ -18,9 +19,9 @@ @RestController public class FsuviusController { private final Log log; - private final int MAX_REQUESTS_PER_5S = 100; + private final int MAX_REQUESTS_PER_5S = 200; private final Bucket bucket; - private UserRegistry userRegistry; + private DatabaseController databaseController; /** * Instantiates a FsuviusController. @@ -28,13 +29,15 @@ public class FsuviusController { public FsuviusController() { log = new Log("FsuviusController"); log.print("Starting up..."); - userRegistry = new UserRegistry(); + databaseController = new DatabaseController(); Bandwidth limit= Bandwidth.classic(MAX_REQUESTS_PER_5S, Refill.greedy(MAX_REQUESTS_PER_5S, Duration.ofSeconds(5))); this.bucket = Bucket.builder().addLimit(limit).build(); - log.print("Initialization complete. Welcome to Mount Fsuvius."); + log.print("===== Init complete. Welcome to Mount Fsuvius. ====="); } + /* ===== BANK TOTALS ===== */ + /** * Gets the total amount of FSU in the bank. * @return The total amount of FSU in the bank @@ -43,7 +46,7 @@ public FsuviusController() { public float getBankBalance() { if(bucket.tryConsume(1)) { float bal = 0.0F; - for(User i : userRegistry.getAll()) { + for(User i : databaseController.getUsers()) { bal += i.getBalance(); } return bal; @@ -51,6 +54,8 @@ public float getBankBalance() { throw new RateLimitException(); } + /* ===== USERS ===== */ + /** * Gets all registered Users. * @return All Users @@ -58,10 +63,9 @@ public float getBankBalance() { @GetMapping("/api/users") public List getUsers() { if(bucket.tryConsume(1)) { - return userRegistry.getAll(); + return databaseController.getUsers(); } throw new RateLimitException(); - } /** @@ -73,10 +77,9 @@ public List getUsers() { public User newUser(@RequestBody String name) { if(bucket.tryConsume(1)) { log.print("Handling request to create new user with name \"" + name + "\"."); - return userRegistry.createUser(name); + return databaseController.createUser(name); } throw new RateLimitException(); - } /** @@ -84,10 +87,10 @@ public User newUser(@RequestBody String name) { * @param id The ID of the user to get * @return The User with the specified ID */ - @GetMapping("/api/user/{id}") + @GetMapping("/api/users/{id}") public User getUser(@PathVariable String id) { if(bucket.tryConsume(1)) { - return userRegistry.getUser(id); + return databaseController.getUser(id); } throw new RateLimitException(); } @@ -98,11 +101,11 @@ public User getUser(@PathVariable String id) { * @param id ID of the User to edit * @return The edited User */ - @PutMapping("/api/user/{id}") + @PutMapping("/api/users/{id}") public User editUser(@RequestBody User newUser, @PathVariable String id) { if(bucket.tryConsume(1)) { log.print("Handling request to edit user at ID \"" + id + "\"."); - return userRegistry.editUser(id, newUser); + return databaseController.editUser(id, newUser); } throw new RateLimitException(); } @@ -111,13 +114,48 @@ public User editUser(@RequestBody User newUser, @PathVariable String id) { * Deletes a User with the given ID. * @param id The ID of the user to delete */ - @DeleteMapping("/api/user/{id}") + @DeleteMapping("/api/users/{id}") public void deleteUser(@PathVariable String id) { if(bucket.tryConsume(1)) { log.print("Handling request to delete user at ID \"" + id + "\"."); - userRegistry.deleteUser(id); + databaseController.deleteUser(id); } else { throw new RateLimitException(); } } + + /* ===== PHOTOS ===== */ + + /** + * Gets a photo by ID. + * Will result in an HTTP 404 if the photo cannot be found. + * @param id ID of the photo to get + * @return The photo with the specified ID + */ + @GetMapping(value = "api/photos/{id}") + public byte[] getPhoto(@PathVariable String id) { + if(bucket.tryConsume(1)) { + try { + return databaseController.readPhoto(id); + } catch(NotFoundException e) { + log.print(1, "Couldn't find photo " + id + " in database."); + throw new NotFoundException(); + } + } + throw new RateLimitException(); + } + + /** + * Updates a photo by ID. Will create it if it does not already exist. + * @param item New content of the photo + * @param id ID of the photo to update + */ + @PostMapping("api/photos/{id}") + public void putPhoto(@RequestBody String item, @PathVariable String id) { + if(bucket.tryConsume(1)) { + databaseController.writePhoto(item, id); + return; + } + throw new RateLimitException(); + } } diff --git a/src/main/java/org/jmeifert/fsuvius/data/DatabaseController.java b/src/main/java/org/jmeifert/fsuvius/data/DatabaseController.java new file mode 100644 index 0000000..eb3405e --- /dev/null +++ b/src/main/java/org/jmeifert/fsuvius/data/DatabaseController.java @@ -0,0 +1,272 @@ +package org.jmeifert.fsuvius.data; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Vector; + +import org.jmeifert.fsuvius.error.NotFoundException; +import org.jmeifert.fsuvius.util.Log; +import org.jmeifert.fsuvius.user.User; + + +/** + * UserRegistry handles the storage and retrieval of users. + */ +public class DatabaseController { + private final Log log; + private final String USERS_STORE_FILE = "data/users.dat"; + private Vector users; + + /** + * Instantiates a UserRegistry. + */ + public DatabaseController() { + log = new Log("UserRegistry"); + try { + Files.createDirectories(Paths.get("data/photos/")); + } catch(IOException e) { + log.print(2, "Failed to ensure presence of data directories."); + } + log.print("Loading users..."); + this.users = loadUsersFromFile(); + log.print("Done."); + } + + /** + * Gets all users. + * @return All users as a list + */ + public synchronized List getUsers() { + return Collections.list(users.elements()); + } + + /** + * Gets a single user by ID + * @param id User's ID + * @return The user with the specified ID + */ + public synchronized User getUser(String id) { + for(User i : users) { + if(i.getID().equals(id)) { + return i; + } + } + throw new NotFoundException(); + } + + /** + * Creates a user with the specified name + * @param name Name of the user to be created + * @return The user with the specified name + */ + public synchronized User createUser(String name) { + User userToAdd = new User(name); + users.add(userToAdd); + saveUsersToFile(users); + return userToAdd; + } + + /** + * Updates a user. + * @param id The user ID to update + * @param user The user to replace with + * @return The updated user + */ + public synchronized User editUser(String id, User user) { + for(int i = 0; i < users.size(); i++) { + if(users.get(i).getID().equals(id)) { + users.set(i, user); + saveUsersToFile(users); + return user; + } + } + throw new NotFoundException(); + } + + /** + * Deletes a user. + * @param id User ID to delete + */ + public synchronized void deleteUser(String id) { + for(int i = 0; i < users.size(); i++) { + if(users.get(i).getID().equals(id)) { + users.remove(i); + saveUsersToFile(users); + deletePhoto(id); + return; + } + } + throw new NotFoundException(); + } + + /** + * Deletes everything. (used by tests) + */ + public synchronized void reset() { + log.print(1, "Resetting database."); + users = new Vector<>(); + saveUsersToFile(users); + cleanupPhotos(); + log.print("Database reset complete."); + } + + /** + * Loads users from a file. + * @return Users loaded from the file + */ + private synchronized Vector loadUsersFromFile() { + try { + FileInputStream f = new FileInputStream(USERS_STORE_FILE); + ObjectInputStream o = new ObjectInputStream(f); + Vector output = (Vector) o.readObject(); + o.close(); + f.close(); + return output; + } catch(FileNotFoundException e) { + log.print(1, "UserRegistry: FileNotFoundException upon reading " + USERS_STORE_FILE); + return new Vector<>(); + } catch(IOException e) { + log.print(2, "UserRegistry: IOException upon reading " + USERS_STORE_FILE); + throw new RuntimeException("Failed to read users!"); + } catch(ClassNotFoundException e) { + log.print(2, "UserRegistry: ClassNotFoundException upon reading " + USERS_STORE_FILE); + throw new RuntimeException("Failed to read users!"); + } + + } + + /** + * Saves users to a file. + * @param users Users to save to the file + */ + private synchronized void saveUsersToFile(Vector users) { + try { + log.print("Syncing writes to " + USERS_STORE_FILE + "..."); + FileOutputStream f = new FileOutputStream(USERS_STORE_FILE); + ObjectOutputStream o = new ObjectOutputStream(f); + o.writeObject(users); + o.close(); + f.close(); + } catch(FileNotFoundException e) { + log.print(2, "UserRegistry: FileNotFoundException upon writing " + USERS_STORE_FILE); + } catch(IOException e) { + log.print(2, "UserRegistry: IOException upon writing " + USERS_STORE_FILE); + } + } + + /** + * Reads a single photo with a given ID + * @param id The ID of the photo to read + * @return The photo as bytes + */ + public synchronized byte[] readPhoto(String id) { + return loadBytesFromFile("data/photos/"+id); + } + + /** + * Writes a single photo with a given ID + * @param item The photo as bytes + * @param id The ID of the photo to write + */ + public synchronized void writePhoto(String item, String id) { + try { + byte[] image = Base64.getDecoder().decode(item.split(",")[1]); + saveBytesToFile(image, "data/photos/"+id); + } catch(RuntimeException e) { + log.print(2, "Failed to write image."); + throw new RuntimeException("Failed to write image."); + } + } + + /** + * Deletes a single photo with a given ID + * @param id The ID of the photo to delete + */ + public synchronized void deletePhoto(String id) { + String filename = "data/photos/" + id; + File f = new File(filename); + if(f.delete()) { return; } + log.print(2, "I/O error deleting " + filename + "."); + } + + /** + * Loads bytes from a file. + * @param STORE_FILE Filename to load bytes from + * @return The loaded bytes + */ + private synchronized byte[] loadBytesFromFile(String STORE_FILE) { + try { + File f = new File(STORE_FILE); + FileInputStream fis = new FileInputStream(f); + byte[] fb = new byte[(int) f.length()]; + fis.read(fb); + fis.close(); + return fb; + } catch(FileNotFoundException e) { + log.print(1, "Couldn't find " + STORE_FILE + ". Will attempt to create it on write."); + throw new NotFoundException(); + } catch(IOException e) { + log.print(2, "Error reading " + STORE_FILE + "."); + throw new RuntimeException("Failed to read " + STORE_FILE + "."); + } + } + + /** + * Saves bytes to a file. + * @param item Bytes to save + * @param filename Filename to save to + */ + private synchronized void saveBytesToFile(byte[] item, String filename) { + try { + log.print("Writing " + filename + "."); + FileOutputStream f = new FileOutputStream(filename); + f.write(item); + f.close(); + } catch(FileNotFoundException e) { + log.print(2, "Couldn't find " + filename + " on write."); + throw new RuntimeException("Couldn't find " + filename + "on write."); + } catch(IOException e) { + log.print(2, "I/O error writing " + filename + "."); + throw new RuntimeException("I/O error writing " + filename + "."); + } + } + + /** + * Cleans up unused photos from disk. + */ + private synchronized void cleanupPhotos() { + log.print(0, "Cleaning up photo database..."); + Vector user_ids = new Vector<>(); + for(User i : users) { + user_ids.add(i.getID()); + } + File[] files = (new File("data/photos/")).listFiles(); + if(files == null || files.length == 0) { return; } + int n_photos = 0; + for(File i : files) { if(i.isFile()) { n_photos++; } } + if(n_photos <= user_ids.size()) { return; } + log.print(1, "Found unused photos."); + for(File i : files) { + if(i.isFile()) { + boolean hasParentPhoto = false; + for(String j : user_ids) { + if(i.getName().equals(j)) { + hasParentPhoto = true; + user_ids.remove(j); + break; + } + } + if(!hasParentPhoto) { + log.print(0, "Deleting unused photo " + i.getName()); + if(!i.delete()) { + log.print(1, "Failed to delete unused photo " + i.getName()); + } + } + } + } + } +} diff --git a/src/main/java/org/jmeifert/fsuvius/user/UserRegistry.java b/src/main/java/org/jmeifert/fsuvius/user/UserRegistry.java deleted file mode 100644 index 876367f..0000000 --- a/src/main/java/org/jmeifert/fsuvius/user/UserRegistry.java +++ /dev/null @@ -1,145 +0,0 @@ -package org.jmeifert.fsuvius.user; - -import org.jmeifert.fsuvius.error.NotFoundException; -import org.jmeifert.fsuvius.util.Log; - -import java.io.*; -import java.util.Collections; -import java.util.List; -import java.util.Vector; - -/** - * UserRegistry handles the storage and retrieval of users. - */ -public class UserRegistry { - private final Log log; - private final String STORE_FILE = "users.dat"; - private Vector users; - - /** - * Instantiates a UserRegistry. - */ - public UserRegistry() { - log = new Log("UserRegistry"); - log.print("Loading users into cache..."); - users = loadUsersFromFile(); - } - - /** - * Gets all registered users. - * @return All users as a list - */ - public synchronized List getAll() { - return Collections.list(users.elements()); - } - - /** - * Gets a single user by ID - * @param id User's ID - * @return The user with the specified ID - */ - public synchronized User getUser(String id) { - for(User i : users) { - if(i.getID().equals(id)) { - return i; - } - } - throw new NotFoundException(); - } - - /** - * Creates a user with the specified name - * @param name Name of the user to be created - * @return The user with the specified name - */ - public synchronized User createUser(String name) { - User userToAdd = new User(name); - users.add(userToAdd); - saveUsersToFile(users); - return userToAdd; - } - - /** - * Updates a user. - * @param id The user ID to update - * @param user The user to replace with - * @return The updated user - */ - public synchronized User editUser(String id, User user) { - for(int i = 0; i < users.size(); i++) { - if(users.get(i).getID().equals(id)) { - users.set(i, user); - saveUsersToFile(users); - return user; - } - } - throw new NotFoundException(); - } - - /** - * Deletes a user. - * @param id User ID to delete - */ - public synchronized void deleteUser(String id) { - for(int i = 0; i < users.size(); i++) { - if(users.get(i).getID().equals(id)) { - users.remove(i); - saveUsersToFile(users); - return; - } - } - throw new NotFoundException(); - } - - /** - * Deletes all users (used by tests). - */ - public synchronized void reset() { - users = new Vector<>(); - saveUsersToFile(users); - } - - /** - * Loads users from a file. - * @return Users loaded from the file - */ - private synchronized Vector loadUsersFromFile() { - try { - FileInputStream f = new FileInputStream(STORE_FILE); - ObjectInputStream o = new ObjectInputStream(f); - Vector output = (Vector) o.readObject(); - o.close(); - f.close(); - return output; - } catch(FileNotFoundException e) { - log.print(1, "UserRegistry: FileNotFoundException upon reading " + STORE_FILE); - return new Vector<>(); - } catch(IOException e) { - log.print(2, "UserRegistry: IOException upon reading " + STORE_FILE); - throw new RuntimeException("Failed to read users!"); - } catch(ClassNotFoundException e) { - log.print(2, "UserRegistry: ClassNotFoundException upon reading " + STORE_FILE); - throw new RuntimeException("Failed to read users!"); - } - - } - - /** - * Saves users to a file. - * @param users Users to save to the file - */ - private synchronized void saveUsersToFile(Vector users) { - try { - log.print("Syncing writes to " + STORE_FILE + "..."); - FileOutputStream f = new FileOutputStream(STORE_FILE); - ObjectOutputStream o = new ObjectOutputStream(f); - o.writeObject(users); - o.close(); - f.close(); - } catch(FileNotFoundException e) { - log.print(2, "UserRegistry: FileNotFoundException upon writing " + STORE_FILE); - } catch(IOException e) { - log.print(2, "UserRegistry: IOException upon writing " + STORE_FILE); - } - } -} diff --git a/src/main/resources/static/index.js b/src/main/resources/static/index.js index 8d961ee..5b212d3 100644 --- a/src/main/resources/static/index.js +++ b/src/main/resources/static/index.js @@ -2,7 +2,10 @@ const USERS_URL = "api/users" /* The URL prefix for performing operations on single users */ -const USER_URL = "api/user/" +const USER_URL = "api/users/" + +/* The URL prefix for performing operations on single photos */ +const PHOTO_URL = "api/photos/" /* Handle creating a user */ function handle_create() { @@ -111,12 +114,10 @@ function display_list() { console.log(user); var user_HTML = `
- - - - - - + +

${user.balance}

+
` formatted_result += user_HTML; From 9b8cd0ba8bef10cb960add281962adf6091a304d Mon Sep 17 00:00:00 2001 From: Juno Date: Mon, 7 Aug 2023 09:31:34 -0400 Subject: [PATCH 2/4] re-add +/- buttons to list --- src/main/resources/static/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/static/index.js b/src/main/resources/static/index.js index 5b212d3..df790ae 100644 --- a/src/main/resources/static/index.js +++ b/src/main/resources/static/index.js @@ -117,6 +117,8 @@ function display_list() {

${user.balance}

+ + ` From b48752ec6b7a146fc03956aa419d4318a27aaef3 Mon Sep 17 00:00:00 2001 From: Juno Date: Mon, 7 Aug 2023 09:31:57 -0400 Subject: [PATCH 3/4] remove broken tests for now --- .../fsuvius/FsuviusApplicationTests.java | 64 ------------------- 1 file changed, 64 deletions(-) diff --git a/src/test/java/org/jmeifert/fsuvius/FsuviusApplicationTests.java b/src/test/java/org/jmeifert/fsuvius/FsuviusApplicationTests.java index 9a918e9..7c68a85 100644 --- a/src/test/java/org/jmeifert/fsuvius/FsuviusApplicationTests.java +++ b/src/test/java/org/jmeifert/fsuvius/FsuviusApplicationTests.java @@ -1,85 +1,21 @@ package org.jmeifert.fsuvius; -import org.jmeifert.fsuvius.user.UserRegistry; import org.jmeifert.fsuvius.util.Log; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.jmeifert.fsuvius.user.User; @SpringBootTest class FsuviusApplicationTests { - @Test - void testUserRegistry() { - Log log = new Log("FsuviusApplicationTests"); - log.print("Starting test of UserRegistry..."); - - log.print("Resetting UserRegistry..."); - UserRegistry ur = new UserRegistry(); - ur.reset(); - - log.print("Confirming proper reset of UserRegistry..."); - assert ur.getAll().size() == 0; - - log.print("Creating a user..."); - User a = ur.createUser("User"); - - log.print("Checking for proper creation of this User..."); - assert ur.getAll().get(0) == a && ur.getUser(a.getID()) == a; - - log.print("Editing the user..."); - a.setName("New Name"); - a.setBalance(5.0F); - ur.editUser(a.getID(), a); - - log.print("Checking for proper edit of this User..."); - assert ur.getAll().get(0) == a && ur.getUser(a.getID()) == a; - - log.print("Deleting the user..."); - ur.deleteUser(a.getID()); - - log.print("Checking for proper deletion of this User..."); - assert ur.getAll().size() == 0; - - log.print("Test of UserRegistry completed."); - } - @Test void testFsuviusController() { Log log = new Log("FsuviusApplicationTests"); log.print("Starting test of FsuviusController..."); - log.print("Resetting UserRegistry..."); - UserRegistry u = new UserRegistry(); - u.reset(); - log.print("Creating a FsuviusController..."); FsuviusController fc = new FsuviusController(); - log.print("Confirming that there are no users..."); - assert fc.getUsers().size() == 0; - - log.print("Creating a user..."); - User a = fc.newUser("User"); - - log.print("Checking for proper creation of this User..."); - assert fc.getUsers().get(0) == a && fc.getUser(a.getID()) == a; - - log.print("Editing the user..."); - a.setName("New Name"); - a.setBalance(5.0F); - fc.editUser(a, a.getID()); - - log.print("Checking for proper edit of this User..."); - assert fc.getUsers().get(0) == a && fc.getUser(a.getID()) == a; - - log.print("Deleting the user..."); - fc.deleteUser(a.getID()); - - log.print("Checking for proper deletion of this User..."); - assert fc.getUsers().size() == 0; - log.print("Test of FsuviusController completed."); } } From 9441bf9d8de0fb2166ef603f7ca8128bd5fc0d8d Mon Sep 17 00:00:00 2001 From: Juno Date: Fri, 8 Sep 2023 13:27:53 -0400 Subject: [PATCH 4/4] Add photo functionality in frontend --- .gitignore | 1 + .idea/misc.xml | 1 - .../jmeifert/fsuvius/FsuviusController.java | 6 +- .../java/org/jmeifert/fsuvius/FsuviusMap.java | 22 +++ .../fsuvius/data/DatabaseController.java | 9 +- .../java/org/jmeifert/fsuvius/util/Log.java | 14 +- src/main/resources/static/editor.html | 42 +++++ src/main/resources/static/editor.js | 129 +++++++++++++++ src/main/resources/static/index.html | 8 +- src/main/resources/static/index.js | 150 ++++++++++-------- src/main/resources/static/style.css | 89 +++++++---- 11 files changed, 352 insertions(+), 119 deletions(-) create mode 100644 src/main/java/org/jmeifert/fsuvius/FsuviusMap.java create mode 100644 src/main/resources/static/editor.html create mode 100644 src/main/resources/static/editor.js diff --git a/.gitignore b/.gitignore index c09f25a..1a42c56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ users.dat +data/photos/* .idea/workspace.xml .idea/vcs.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index 72a728e..ee7e0d0 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/src/main/java/org/jmeifert/fsuvius/FsuviusController.java b/src/main/java/org/jmeifert/fsuvius/FsuviusController.java index 83475f7..393a9fe 100644 --- a/src/main/java/org/jmeifert/fsuvius/FsuviusController.java +++ b/src/main/java/org/jmeifert/fsuvius/FsuviusController.java @@ -19,7 +19,7 @@ @RestController public class FsuviusController { private final Log log; - private final int MAX_REQUESTS_PER_5S = 200; + private final Bucket bucket; private DatabaseController databaseController; @@ -30,8 +30,8 @@ public FsuviusController() { log = new Log("FsuviusController"); log.print("Starting up..."); databaseController = new DatabaseController(); - Bandwidth limit= Bandwidth.classic(MAX_REQUESTS_PER_5S, - Refill.greedy(MAX_REQUESTS_PER_5S, Duration.ofSeconds(5))); + Bandwidth limit= Bandwidth.classic(FsuviusMap.MAX_REQUESTS_PER_SECOND, + Refill.greedy(FsuviusMap.MAX_REQUESTS_PER_SECOND, Duration.ofSeconds(1))); this.bucket = Bucket.builder().addLimit(limit).build(); log.print("===== Init complete. Welcome to Mount Fsuvius. ====="); } diff --git a/src/main/java/org/jmeifert/fsuvius/FsuviusMap.java b/src/main/java/org/jmeifert/fsuvius/FsuviusMap.java new file mode 100644 index 0000000..9a84d59 --- /dev/null +++ b/src/main/java/org/jmeifert/fsuvius/FsuviusMap.java @@ -0,0 +1,22 @@ +package org.jmeifert.fsuvius; + +/** + * FsuviusMap contains user-modifiable constants that are used elsewhere in the program. + */ +public class FsuviusMap { + /** + * The maximum amount of requests per second the server will handle + * before refusing additional requests. + */ + public static final int MAX_REQUESTS_PER_SECOND = 50; + + /** + * The default photo for new users as base64. + */ + public static final String DEFAULT_PHOTO = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAE1ElEQVR42u2aSSxzXRjH/17S1xxi2hQxJKKhK7ERmyKIjQS7mrc2iAgLiaRiQcSiIcFGKjEtiBJi2phJDA2q2mKB1jxEWzqdb9d8l1YRXrTnn9zFc+5z7rnnl3Oe+5yndSGEEDix/sDJRQFQABQABUABUAAUAAVAAVAAFIBzyu21m2azGTKZDKurq1hfX4dMJsPGxgbOz88tPru7u4iNjf29BIgNXV1dkaqqKgLg1Wt3d5f8ZlkFoNPpSGlpqd3JOyyAqakpxiRLS0uJVColWq2WmM1m4kiyCqCsrIwBQKVSEUfVCwCPj4+Myefn5xNH1ovPoEajYdhRUVHOlQfo9Xrmd9LNzXnzgI9Kr9dje3sbEokEW1tbkMvlkMvlCAoKQnh4OEJCQuDv74/Q0FCEh4cjMjISbDYbrq6u35MHsNnsN33ynl8KhYKxn8xmM5mfnyfJycnvftbs7KzNfXpwcMDwbW9vf9c+fx7X+Hy+5Z7bJ4JEd3c3iouLnfMssLi4yJg8m82GSCTC/v4+7u7uYDAYYDKZoNVqoVarsba2ho6ODnC53J+VCqtUKsZyEQgEdpeYyWQiWVlZlj6RkZHk4ODgzctzenqaLC8v/94tcHx8jLGxMYstEAgQERHxpr5///5FSkrK794C19fXDDs6Otq5YoDBYGDYOp3OuQD4+Pgw7MHBwRcJlUMDCAsLQ3BwsMUWCoWorq7G3t4ezGaz4wPw9PREc3Mzo621tRWxsbHIzs5GT08Ptra2cHt767ipcF5eHjY3N9HS0sJoF4vFEIvFFpvP5yM1NRUJCQmIiYn59rPGpyVC7u7uaGxsxMDAANhstk2/np4eFBUVIS4uDunp6ZicnHwRRH9tVZjFYiEvLw87OzuYmZlBXV3dqzBmZ2eRnp6OiooK3N/fO05Z3NfXFzweD/X19VAqlVAoFBgfH0dDQ4NVIEKhEAKBAF/1d6XXAvGX/y7AYrEQFRWFjIwM1NbWQi6XY2VlBYWFhQy/pqYm7OzsWH/JP8zXNJlM73qHx8fHf1sPsBcrEhMTweVy4eXlhba2Nsu9zc1NxMXFWe3zf713uzzPVP/pCngNREFBAaNNpVJZ9fXw8GDYEonkXWPJZLKfBwAA/Pz8XhyMbMWUzMxMi93X14ebm5s3jWE0GtHV1fUzAZycnDDs0NBQm75ZWVkMe2Fh4U1jjIyMYGho6GsBjI+PQywW4+rq6s19zs7O0NDQwGhLSEiw6c/j8Rh2ZWUljo6OXq1QTUxMICcn5+sLIr29vRb/srIyMjw8TCQSCTk/PydarZYYjUZiNBrJw8MDUSqVZGBggHC5XMY4nZ2ddosuNTU1jD4cDoeMjo6Ss7MzotfricFgINfX12RtbY1UVFRY/Pr7+20WRD4dwEeu5uZm8vT0ZHeci4sLkpmZ+a5ni0QiotPpbAL41hiQlpaGubk5lJeXg8Vi2fUPDAyESCRCZWWlXd+kpCTMz8+Dz+fDxcXla/OA3Nxc8Hg8qNVqnJyc4PDwEGq1GpeXlzg9PYVUKoW3tzciIiLA4XDA4XAQHx//ocNQQEAAmpqaUFJSgtXVVSwtLUEqlUKj0VgOWMnJyYiPj3+RP1iTC/27vJOLAqAAKAAKgAKgACgACoACoAAoAAqAAqAAnFD/AZZ5oHgkh6rAAAAAAElFTkSuQmCC"; + + /** + * The maximum allowed size for photos. 1MB recommended. + */ + public static final int MAX_PHOTO_SIZE = 1024 * 1024; +} diff --git a/src/main/java/org/jmeifert/fsuvius/data/DatabaseController.java b/src/main/java/org/jmeifert/fsuvius/data/DatabaseController.java index eb3405e..9f692d9 100644 --- a/src/main/java/org/jmeifert/fsuvius/data/DatabaseController.java +++ b/src/main/java/org/jmeifert/fsuvius/data/DatabaseController.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Vector; +import org.jmeifert.fsuvius.FsuviusMap; import org.jmeifert.fsuvius.error.NotFoundException; import org.jmeifert.fsuvius.util.Log; import org.jmeifert.fsuvius.user.User; @@ -66,6 +67,7 @@ public synchronized User getUser(String id) { public synchronized User createUser(String name) { User userToAdd = new User(name); users.add(userToAdd); + writePhoto(FsuviusMap.DEFAULT_PHOTO, userToAdd.getID()); saveUsersToFile(users); return userToAdd; } @@ -174,8 +176,11 @@ public synchronized byte[] readPhoto(String id) { */ public synchronized void writePhoto(String item, String id) { try { - byte[] image = Base64.getDecoder().decode(item.split(",")[1]); - saveBytesToFile(image, "data/photos/"+id); + /* if user bypasses frontend upload size limit just don't do anything */ + if(item.length() < FsuviusMap.MAX_PHOTO_SIZE * 1.33 + 24) { + byte[] image = Base64.getDecoder().decode(item.split(",")[1]); + saveBytesToFile(image, "data/photos/"+id); + } } catch(RuntimeException e) { log.print(2, "Failed to write image."); throw new RuntimeException("Failed to write image."); diff --git a/src/main/java/org/jmeifert/fsuvius/util/Log.java b/src/main/java/org/jmeifert/fsuvius/util/Log.java index 1761989..14d3622 100644 --- a/src/main/java/org/jmeifert/fsuvius/util/Log.java +++ b/src/main/java/org/jmeifert/fsuvius/util/Log.java @@ -49,16 +49,10 @@ public void print(int level, String entry) { StringBuilder sb = new StringBuilder(); sb.append(PREFIX); sb.append(new Date()); - switch(level) { - case 1: - sb.append(LOG_WARN); - break; - case 2: - sb.append(LOG_ERROR); - break; - default: - sb.append(LOG_OK); - break; + switch (level) { + case 1 -> sb.append(LOG_WARN); + case 2 -> sb.append(LOG_ERROR); + default -> sb.append(LOG_OK); } sb.append(className); sb.append(": "); diff --git a/src/main/resources/static/editor.html b/src/main/resources/static/editor.html new file mode 100644 index 0000000..9a357ed --- /dev/null +++ b/src/main/resources/static/editor.html @@ -0,0 +1,42 @@ + + + + Mount Fsuvius + + + + + + + + +
+

Edit User

+

Photo

+ +

Upload an image (Max size: 1MB)

+ + +
+ +
Name
+
Balance

+ + + + +
+ + + + +
+ +
+ + + + \ No newline at end of file diff --git a/src/main/resources/static/editor.js b/src/main/resources/static/editor.js new file mode 100644 index 0000000..5f6b7ab --- /dev/null +++ b/src/main/resources/static/editor.js @@ -0,0 +1,129 @@ +/* Get URL-encoded parameters */ +const params = new URLSearchParams(decodeURI(window.location.search)); + +/* URL for performing operations on this user */ +const USER_URL = "api/users/" + params.get("id"); + +/* URL for performing operations on this user's photo */ +const PHOTO_URL = "api/photos/" + params.get("id"); + +/* This user's ID */ +const USER_ID = params.get("id"); + +/* Max photo upload size */ +const MAX_UPLOAD_SIZE = 1024 * 1024; + +/* Toast messages */ +var toast_timeout; + +function show_toast(message) { + clearTimeout(toast_timeout); + var td = document.getElementById("TOAST_MESSAGE"); + td.innerHTML = message; + td.className = "show"; + toast_timeout = setTimeout(hide_toast, 3000); +} + +function hide_toast() { + var td = document.getElementById("TOAST_MESSAGE"); + td.className = td.className.replace("show", "hide"); +} + +/* Update fields with this user's data (including photo) */ +function handle_display() { + console.log(`Handling DISPLAY user ${USER_ID}`) + fetch(USER_URL, { + method: "GET", + headers: { + "Accept": "application/json", + }, + }).then(async response => { + if(!response.ok) { throw new Error("GET request failed!"); } + const data = await response.json(); + console.log("[DEBUG] Response:"); + console.log(data); + document.getElementById("USER_NAME").value = data.name; + document.getElementById("USER_BALANCE").value = data.balance; + document.getElementById("USER_PHOTO").src = PHOTO_URL; + }).catch(error => { + console.log(error); + show_toast("Couldn't fetch parameters. See console for error details."); + }); +} + +/* Save changes to this user */ +function handle_save() { + console.log(`Saving changes to user "${USER_URL}"`); + show_toast("Saving your changes..."); + let new_name = document.getElementById("USER_NAME").value; + let new_balance = document.getElementById("USER_BALANCE").value; + let new_user = { + "id": `${USER_ID}`, + "name": `${new_name}`, + "balance": `${new_balance}` + } + fetch((USER_URL), { + method: "PUT", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(new_user), + }).then(async response => { + if(!response.ok) { throw new Error("PUT request failed!"); } + window.location.href="index.html"; + }).catch(error => { + console.log(error); + show_toast("Couldn't save changes. See console for error details."); + }); +} + +/* Handle deleting this user */ +function handle_delete() { + console.log(`Handling deletion of user ${USER_ID}`); + show_toast("Deleting user..."); + if(window.confirm("Are you sure you want to delete this user?")) { + fetch((USER_URL), { + method: "DELETE", + }).then(async response => { + if(!response.ok) { throw new Error("DELETE request failed!"); } + window.location.href="index.html"; + }).catch(error => { + console.log(error); + show_toast("Couldn't delete user. See console for error details."); + }); + } +} + +/* Handle uploading a new photo */ +function handle_upload_photo(input) { + console.log("Handling upload of user photo..."); + if(input.files[0].size < MAX_UPLOAD_SIZE) { + show_toast("Uploading photo..."); + const fr = new FileReader(); + fr.addEventListener("load", function(event) { + fetch(PHOTO_URL, { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: event.target.result, + }).then(async response => { + if(!response.ok) { + throw new Error("POST request failed!"); + } + show_toast("Photo uploaded."); + document.getElementById("USER_PHOTO").src = event.target.result; + }).catch(error => { + console.log(error); + show_toast("Something went wrong uploading your photo."); + }); + }); + fr.readAsDataURL(input.files[0]); + } else { + show_toast("Your photo is too large!"); + } +} + +/* ===== On page load ===== */ +handle_display() \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 3919b4e..c34ea71 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -6,6 +6,8 @@ + + + +
diff --git a/src/main/resources/static/index.js b/src/main/resources/static/index.js index df790ae..3107c8b 100644 --- a/src/main/resources/static/index.js +++ b/src/main/resources/static/index.js @@ -1,17 +1,36 @@ -/* The URL for getting a list of users */ +/* URL for getting a list of users */ const USERS_URL = "api/users" -/* The URL prefix for performing operations on single users */ +/* URL prefix for performing operations on single users */ const USER_URL = "api/users/" -/* The URL prefix for performing operations on single photos */ +/* URL prefix for performing operations on single photos */ const PHOTO_URL = "api/photos/" +/* Toast messages */ +var toast_timeout; + +function show_toast(message) { + clearTimeout(toast_timeout); + var td = document.getElementById("TOAST_MESSAGE"); + td.innerHTML = message; + td.className = "show"; + toast_timeout = setTimeout(hide_toast, 3000); +} + +function hide_toast() { + var td = document.getElementById("TOAST_MESSAGE"); + td.className = td.className.replace("show", "hide"); +} + /* Handle creating a user */ function handle_create() { let new_name = document.getElementById("CREATE_FIELD").value; - if(new_name.length < 1) { return; } - console.log("Handling CREATE new user..."); + if(new_name.length < 1) { + show_toast("Please type a username to create."); + return; + } + console.log("Handling creation of new user..."); fetch(USERS_URL, { method: "POST", headers: { @@ -25,75 +44,67 @@ function handle_create() { display_list(); }).catch(error => { console.log(error); - window.alert("Failed to create user."); + show_toast("Failed to create user."); }); } -/* Handle updating a user */ -function handle_update(id) { - console.log(`Handling UPDATE for ID [${id}]`); - let name_field = document.getElementById(`FIELD_NAME_${id}`); - let balance_field = document.getElementById(`FIELD_BALANCE_${id}`); - let new_name = name_field.value; - let new_balance = balance_field.value; - let new_user = { - "id": `${id}`, - "name": `${new_name}`, - "balance": `${new_balance}` - } - console.log("[DEBUG] New user:"); - console.log(new_user); +/* Handle +1 for a single user */ +function handle_plus(id) { + handle_balance_change(id, +1.0); +} + +/* Handle -1 for a single user */ +function handle_minus(id) { + handle_balance_change(id, -1.0); +} + +/* Change a user's balance */ +function handle_balance_change(id, offset) { + console.log(`Handling balance update for user ${id}`); + show_toast("Saving your changes..."); - fetch((USER_URL + id), { - method: "PUT", + /* Get current user parameters */ + fetch(USER_URL + id, { + method: "GET", headers: { "Accept": "application/json", - "Content-Type": "application/json", }, - body: JSON.stringify(new_user), }).then(async response => { - if(!response.ok) { throw new Error("PUT request failed!"); } + if(!response.ok) { throw new Error("GET request failed!"); } const data = await response.json(); - console.log("[DEBUG] Response:") - console.log(data); - name_field.value = data.name; - balance_field.value = data.balance; - }).catch(error => { - console.log(error); - window.alert(`Failed to update user ${id}.`); - }); -} + //console.log("[DEBUG] Response:"); + //console.log(data); + var new_balance = data.balance + offset; + let new_user = { + "id": `${id}`, + "name": `${data.name}`, + "balance": `${new_balance}` + } -/* Handle deleting a user */ -function handle_remove(id) { - console.log(`Handling REMOVE for ID [${id}]`); - if(window.confirm("Are you sure you want to remove this user?")) { - fetch((USER_URL + id), { - method: "DELETE", + /* Put user with new parameters */ + fetch(USER_URL + id, { + method: "PUT", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(new_user), }).then(async response => { - if(!response.ok) { throw new Error("DELETE request failed!"); } - display_list(); + if(!response.ok) { throw new Error("PUT request failed!"); } + const data = await response.json(); + //console.log("[DEBUG] Response:"); + //console.log(data); + document.getElementById(`USER_BALANCE_${id}`).innerHTML = `${data.balance} FSU`; + show_toast("Changes saved."); }).catch(error => { console.log(error); - window.alert(`Failed to delete user ${id}.`); + show_toast("Couldn't save changes. See console for error details."); }); - } -} -/* Handle +1 for a single user */ -function handle_plus(id) { - console.log(`Handling PLUS for ID [${id}]`); - let balance_field = document.getElementById(`FIELD_BALANCE_${id}`); - balance_field.value = Number(balance_field.value) + 1.0; - handle_update(id); -} - -/* Handle -1 for a single user */ -function handle_minus(id) { - console.log(`Handling MINUS for ID [${id}]`); - let balance_field = document.getElementById(`FIELD_BALANCE_${id}`); - balance_field.value = Number(balance_field.value) - 1.0; - handle_update(id); + }).catch(error => { + console.log(error); + show_toast("Couldn't save changes. See console for error details."); + }); } /* Display list of all users */ @@ -107,27 +118,30 @@ function display_list() { }).then(async response => { if(!response.ok) { throw new Error("GET request failed!"); } const data = await response.json(); - console.log(data); + //console.log("[DEBUG] Response:"); + //console.log(data); formatted_result = ""; for(let i in data) { let user = data[i]; console.log(user); var user_HTML = ` -
- -

${user.balance}

- - - +
+ +
+

${user.name}

+

${user.balance} FSU

+ + + +
` formatted_result += user_HTML; } document.getElementById("USER_LIST").innerHTML = formatted_result; }).catch(error => { - document.getElementById("USER_LIST").innerHTML = "

Sorry, something went wrong with displaying the list of users. Check the console for more info.

"; console.log(error); + show_toast("Couldn't display list of users. See console for error details."); }); } diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css index 0dec507..c5a034b 100644 --- a/src/main/resources/static/style.css +++ b/src/main/resources/static/style.css @@ -35,19 +35,7 @@ button { cursor: pointer; font: inherit; margin: 0px 0px; - padding: 6px 10px; - border: 1px solid black; - color: black; - background: none; - transition-duration: 200ms; -} - -.inline_button { - cursor: pointer; - font: inherit; - display: inline-block; - margin: 8px 8px 0px 0px; - padding: 6px 10px; + padding: 4px 8px; border: 1px solid black; color: black; background: none; @@ -59,17 +47,12 @@ button:hover { color: white; } -.inline_button:hover { - background: black; - color: white; -} - .name_field { font: inherit; width: 250px; margin: 8px 0; border: 1px solid black; - padding: 6px; + padding: 4px; overflow-x: scroll; overflow-y: hidden; background: none; @@ -80,7 +63,7 @@ button:hover { width: 100px; margin: 8px 0; border: 1px solid black; - padding: 6px; + padding: 4px; overflow-x: scroll; overflow-y: hidden; background: none; @@ -94,7 +77,7 @@ button:hover { box-shadow: 3px 3px 5px rgba(0,0,0,0.5); } -/* ===== NAVBAR ===== */ +/* ===== Navbar ===== */ .navbar { padding: 8px; @@ -124,8 +107,61 @@ button:hover { border: 1px solid white; } +/* ===== User preview cards ===== */ + +.userpreview_container { + width: 100%; + margin: 0 0 24px 0; + padding: 0; + box-sizing: border-box; + display: grid; + grid-template-columns: 100px auto; + word-wrap: break-word; + overflow-wrap: anywhere; + overflow-y: hidden; + overflow-x: hidden; +} + +.userpreview_content { + height: 100px; + box-sizing: border-box; + padding: 0px 12px; +} + +.userpreview_photo { + width: 100px; + height: 100px; + object-fit: cover; + border-radius: 5px; +} + +/* ===== Toast messages ===== */ + +#TOAST_MESSAGE { + font: inherit; + color: white; + visibility: hidden; + min-width: 200px; + position: fixed; + bottom: 50px; + left: 50px; + z-index: 999; + background: rgba(0, 0, 0, 0.7); + padding: 8px 12px; + box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.5); +} + +#TOAST_MESSAGE.show { visibility: visible; } + +.user_balance { + display: inline-block; + padding-right: 10px; + font-weight: bold; + font-style: italic; +} + +/* ===== Labels ===== */ .name_label { - font-size: 20px; display: inline-block; padding: 0px 12px 0px 0px; margin: 0; @@ -133,17 +169,8 @@ button:hover { } .balance_label { - font-size: 20px; display: inline-block; padding: 0px 12px 0px 0px; margin: 0; width: 100px; -} - -.controls_label { - font-size: 20px; - display: inline-block; - padding: 0px 12px 0px 0px; - margin: 0; - width: 200px; } \ No newline at end of file