Compare commits

...

2 Commits

  1. 14
      pages/base.html
  2. 29
      pages/index.html
  3. 21
      scripts/api.js
  4. 134
      src/server/api.go
  5. 2
      src/server/server.go
  6. 0
      static/images/android-chrome-192x192.png
  7. 0
      static/images/android-chrome-512x512.png
  8. 0
      static/images/apple-touch-icon.png
  9. 3
      static/images/check.svg
  10. 3
      static/images/door-open-fill.svg
  11. 0
      static/images/favicon-16x16.png
  12. 0
      static/images/favicon-32x32.png
  13. BIN
      static/images/favicon.ico
  14. 4
      static/images/person-dash-fill.svg
  15. 4
      static/images/person-fill-add.svg
  16. 3
      static/images/trash3-fill.svg
  17. 3
      static/images/x.svg

14
pages/base.html

@ -7,22 +7,22 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dela</title>
<link rel="shortcut icon" href="/static/images/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<script src="/static/bootstrap/js/bootstrap.min.js"></script>
<link rel="shortcut icon" href="/static/favicon.ico" type="image/x-icon">
</head>
<body class="d-flex flex-column h-100">
<header class="d-flex flex-wrap align-items-center justify-content-center justify-content-md-between py-3 mb-4 border-bottom">
<header class="d-flex flex-wrap align-items-center justify-content-center py-3 mb-4 border-bottom">
<div class="col-md-3 mb-2 mb-md-0">
<a href="/" class="d-inline-flex link-body-emphasis text-decoration-none">
<img width="32" height="32" src="/static/android-chrome-192x192.png" alt="Dela">
<img width="64" height="64" src="/static/android-chrome-192x192.png" alt="Dela">
</a>
</div>
<div class="col-md-3 text-end" id="bar-auth">
<a href="/login" class="btn btn-outline-primary me-2">Log in</a>
<a href="/register" class="btn btn-primary">Register</a>
<a href="/login" class="btn btn-outline-primary"><img src="/static/images/door-open-fill.svg" alt="Log in"></a>
<a href="/register" class="btn btn-outline-primary"><img src="/static/images/person-fill-add.svg" alt="Register"></a>
</div>
</header>
@ -51,11 +51,11 @@
}
// Check if auth info is indeed valid
let response = await get_user(username, password);
let response = await getUser(username, password);
if (response.ok) {
let barAuth = document.getElementById("bar-auth");
barAuth.innerHTML = "<b>" + username + "</b>" + " | ";
barAuth.innerHTML += '<button id="log-out-btn" class="btn btn-primary" type="button">Log out</button>';
barAuth.innerHTML += '<button id="log-out-btn" class="btn btn-outline-primary"><img src="/static/images/person-dash-fill.svg"></button>';
document.getElementById("log-out-btn").addEventListener("click", (event) => {
// Log out
forgetAuthInfo();

29
pages/index.html

@ -17,7 +17,7 @@
</form>
<div class="container text-center" style="margin-top: 40px;">
<div class="container text-center" style="margin-top: 4ch;">
<table id="todos" class="table table-hover" style="word-wrap: break-word;"></table>
</div>
@ -42,7 +42,7 @@ document.addEventListener('DOMContentLoaded', async function() {
newTodoTextInput.value = "";
// Make a request
let response = await post_new_todo(username, password, {text: newTodoText});
let response = await postNewTodo(username, password, {text: newTodoText});
if (response.ok) {
location.reload();
}
@ -50,10 +50,12 @@ document.addEventListener('DOMContentLoaded', async function() {
// Fetch and display TODOs
response = await get_todos(username, password);
response = await getTodos(username, password);
let todosJson = await response.json();
let todos = [];
let completeButtonIDs = [];
let deleteButtonIDs = [];
if (response.ok && todosJson != null) {
let todosDiv = document.getElementById("todos");
todosJson.forEach((item) => {
@ -64,21 +66,38 @@ document.addEventListener('DOMContentLoaded', async function() {
todos.push(item);
let todoCompleteBtnID = "btn-complete-" + String(item.id);
let todoDeleteBtnID = "btn-delete-" + String(item.id);
let todoEditBtnID = "btn-edit-" + String(item.id);
// Display
let timeCreated = new Date(item.timeCreatedUnix * 1000);
todosDiv.innerHTML += "<tr><td>" + item.text + "</td>" +
"<td>" + " " + timeCreated.getDate() + "/" + timeCreated.getMonth() + "/" + timeCreated.getFullYear() + "</td>" +
"<td><button class='btn btn-success' id='" + todoCompleteBtnID + "'>" +
"Done</button></td></tr>";
"<img src='/static/images/check.svg'></button><button class='btn btn-danger' id='" +
todoDeleteBtnID + "'><img src='/static/images/trash3-fill.svg'></button></td></tr>";
completeButtonIDs.push(todoCompleteBtnID);
deleteButtonIDs.push(todoDeleteBtnID);
});
}
// Loop over all buttons (doesn't matter which ones because the amounts are equal)
for (let i = 0; i < completeButtonIDs.length; i++) {
// Done button
document.getElementById(completeButtonIDs[i]).addEventListener("click", async (event) => {
response = await delete_todo(username, password, todos[i].id);
// Mark as done
todos[i].isDone = true;
// Update
response = await updateTodo(username, password, todos[i].id, todos[i]);
if (response.ok) {
location.reload();
}
});
// Delete button
document.getElementById(deleteButtonIDs[i]).addEventListener("click", async (event) => {
response = await deleteTodo(username, password, todos[i].id);
if (response.ok) {
location.reload();
}

21
scripts/api.js

@ -3,7 +3,7 @@
*/
async function post_new_todo(username, password, new_todo) {
async function postNewTodo(username, password, new_todo) {
return fetch("/api/todo", {
method: "POST",
headers: {
@ -16,7 +16,7 @@ async function post_new_todo(username, password, new_todo) {
}
async function get_todos(username, password) {
async function getTodos(username, password) {
return fetch("/api/todo", {
method: "GET",
headers: {
@ -27,7 +27,7 @@ async function get_todos(username, password) {
}
async function get_todo_groups(username, password) {
async function getTodoGroups(username, password) {
return fetch("/api/group", {
method: "GET",
headers: {
@ -37,7 +37,7 @@ async function get_todo_groups(username, password) {
});
}
async function delete_todo(username, password, id) {
async function deleteTodo(username, password, id) {
return fetch("/api/todo/"+String(id), {
method: "DELETE",
headers: {
@ -47,7 +47,18 @@ async function delete_todo(username, password, id) {
});
}
async function get_user(username, password) {
async function updateTodo(username, password, id, updatedTodo) {
return fetch("/api/todo/"+String(id), {
method: "POST",
headers: {
"EncryptedBase64": "false",
"Auth": username + "<-->" + password,
},
body: JSON.stringify(updatedTodo),
});
}
async function getUser(username, password) {
return fetch("/api/user", {
method: "GET",
headers: {

134
src/server/api.go

@ -124,7 +124,7 @@ func (s *Server) UserEndpoint(w http.ResponseWriter, req *http.Request) {
}
}
func (s *Server) TodoEndpoint(w http.ResponseWriter, req *http.Request) {
func (s *Server) SpecificTodoEndpoint(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodDelete:
// Delete an existing TODO
@ -150,35 +150,92 @@ func (s *Server) TodoEndpoint(w http.ResponseWriter, req *http.Request) {
return
}
// Mark TODO as done and assign a completion time
updatedTodo, err := s.db.GetTodo(todoID)
// // Mark TODO as done and assign a completion time
// updatedTodo, err := s.db.GetTodo(todoID)
// if err != nil {
// logger.Error("[Server] Failed to get todo with id %d for marking completion: %s", todoID, err)
// http.Error(w, "TODO retrieval error", http.StatusInternalServerError)
// return
// }
// updatedTodo.IsDone = true
// updatedTodo.CompletionTimeUnix = uint64(time.Now().Unix())
// err = s.db.UpdateTodo(todoID, *updatedTodo)
// if err != nil {
// logger.Error("[Server] Failed to update TODO with id %d: %s", todoID, err)
// http.Error(w, "Failed to update TODO information", http.StatusInternalServerError)
// return
// }
// Now delete
err = s.db.DeleteTodo(todoID)
if err != nil {
logger.Error("[Server] Failed to get todo with id %d for marking completion: %s", todoID, err)
http.Error(w, "TODO retrieval error", http.StatusInternalServerError)
logger.Error("[Server] Failed to delete %s's TODO: %s", GetUsernameFromAuth(req), err)
http.Error(w, "Failed to delete TODO", http.StatusInternalServerError)
return
}
updatedTodo.IsDone = true
updatedTodo.CompletionTimeUnix = uint64(time.Now().Unix())
err = s.db.UpdateTodo(todoID, *updatedTodo)
// Success!
logger.Info("[Server] Deleted TODO with ID %d", todoID)
w.WriteHeader(http.StatusOK)
case http.MethodPost:
// Change TODO information
// Check authentication information
if !IsRequestAuthValid(req, s.db) {
http.Error(w, "Invalid user auth data", http.StatusForbidden)
return
}
// Obtain TODO ID
todoIDStr := path.Base(req.URL.Path)
todoID, err := strconv.ParseUint(todoIDStr, 10, 64)
if err != nil {
logger.Error("[Server] Failed to update TODO with id %d: %s", todoID, err)
http.Error(w, "Failed to update TODO information", http.StatusInternalServerError)
http.Error(w, "Invalid TODO ID", http.StatusBadRequest)
return
}
// Now delete
// err = s.db.DeleteTodo(todoID)
// if err != nil {
// logger.Error("[Server] Failed to delete %s's TODO: %s", GetUsernameFromAuth(req), err)
// http.Error(w, "Failed to delete TODO", http.StatusInternalServerError)
// return
// }
// Check if the user owns this TODO
if !DoesUserOwnTodo(GetUsernameFromAuth(req), todoID, s.db) {
http.Error(w, "You don't own this TODO", http.StatusForbidden)
return
}
// Success!
logger.Info("[Server] updated (marked as done) TODO with ID %d", todoID)
// Read body
body, err := io.ReadAll(req.Body)
if err != nil {
logger.Warning("[Server] Failed to read request body to possibly update a TODO: %s", err)
http.Error(w, "Failed to read body", http.StatusInternalServerError)
return
}
// Unmarshal JSON
var updatedTodo db.Todo
err = json.Unmarshal(body, &updatedTodo)
if err != nil {
logger.Warning("[Server] Received invalid TODO JSON in order to update: %s", err)
http.Error(w, "Invalid TODO JSON", http.StatusBadRequest)
return
}
// Update. (Creation date, owner username and an ID do not change)
err = s.db.UpdateTodo(todoID, updatedTodo)
if err != nil {
logger.Warning("[Server] Failed to update TODO: %s", err)
http.Error(w, "Failed to update", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
logger.Info("[Server] Updated TODO with ID %d", todoID)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) TodoEndpoint(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodPost:
// Create a new TODO
defer req.Body.Close()
@ -218,6 +275,7 @@ func (s *Server) TodoEndpoint(w http.ResponseWriter, req *http.Request) {
// Success!
w.WriteHeader(http.StatusOK)
logger.Info("[Server] Created a new TODO for %s", newTodo.OwnerUsername)
case http.MethodGet:
// Retrieve TODO information
// Check authentication information
@ -226,7 +284,7 @@ func (s *Server) TodoEndpoint(w http.ResponseWriter, req *http.Request) {
return
}
// Get TODO
// Get all user TODOs
todos, err := s.db.GetAllUserTodos(GetUsernameFromAuth(req))
if err != nil {
http.Error(w, "Failed to get TODOs", http.StatusInternalServerError)
@ -243,42 +301,6 @@ func (s *Server) TodoEndpoint(w http.ResponseWriter, req *http.Request) {
// Send out
w.Header().Add("Content-Type", "application/json")
w.Write(todosBytes)
// case http.MethodPatch:
// // Change TODO due date and text
// // Check authentication information
// if !IsRequestAuthValid(req, s.db) {
// http.Error(w, "Invalid user auth data", http.StatusForbidden)
// return
// }
// // Read body
// body, err := io.ReadAll(req.Body)
// if err != nil {
// logger.Warning("[Server] Failed to read request body to possibly update a TODO: %s", err)
// http.Error(w, "Failed to read body", http.StatusInternalServerError)
// return
// }
// // Unmarshal JSON
// var todo db.Todo
// err = json.Unmarshal(body, &todo)
// if err != nil {
// logger.Warning("[Server] Received invalid TODO JSON in order to update: %s", err)
// http.Error(w, "Invalid TODO JSON", http.StatusBadRequest)
// return
// }
// // TODO
// err = s.db.UpdateTodo(todo.ID, todo)
// if err != nil {
// logger.Warning("[Server] Failed to update TODO: %s", err)
// http.Error(w, "Failed to update", http.StatusBadRequest)
// return
// }
// w.WriteHeader(http.StatusOK)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}

2
src/server/server.go

@ -133,7 +133,7 @@ func New(config conf.Conf) (*Server, error) {
})
mux.HandleFunc("/api/user", server.UserEndpoint)
mux.HandleFunc("/api/todo", server.TodoEndpoint)
mux.HandleFunc("/api/todo/", server.TodoEndpoint)
mux.HandleFunc("/api/todo/", server.SpecificTodoEndpoint)
// mux.HandleFunc("/api/group", server.TodoGroupEndpoint)

0
static/android-chrome-192x192.png → static/images/android-chrome-192x192.png

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

0
static/android-chrome-512x512.png → static/images/android-chrome-512x512.png

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

0
static/apple-touch-icon.png → static/images/apple-touch-icon.png

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

3
static/images/check.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>

After

Width:  |  Height:  |  Size: 295 B

3
static/images/door-open-fill.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-door-open-fill" viewBox="0 0 16 16">
<path d="M1.5 15a.5.5 0 0 0 0 1h13a.5.5 0 0 0 0-1H13V2.5A1.5 1.5 0 0 0 11.5 1H11V.5a.5.5 0 0 0-.57-.495l-7 1A.5.5 0 0 0 3 1.5V15H1.5zM11 2h.5a.5.5 0 0 1 .5.5V15h-1V2zm-2.5 8c-.276 0-.5-.448-.5-1s.224-1 .5-1 .5.448.5 1-.224 1-.5 1z"/>
</svg>

After

Width:  |  Height:  |  Size: 375 B

0
static/favicon-16x16.png → static/images/favicon-16x16.png

Before

Width:  |  Height:  |  Size: 775 B

After

Width:  |  Height:  |  Size: 775 B

0
static/favicon-32x32.png → static/images/favicon-32x32.png

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
static/images/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

4
static/images/person-dash-fill.svg

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-dash-fill" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11 7.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5z"/>
<path d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
</svg>

After

Width:  |  Height:  |  Size: 327 B

4
static/images/person-fill-add.svg

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-fill-add" viewBox="0 0 16 16">
<path d="M12.5 16a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Zm.5-5v1h1a.5.5 0 0 1 0 1h-1v1a.5.5 0 0 1-1 0v-1h-1a.5.5 0 0 1 0-1h1v-1a.5.5 0 0 1 1 0Zm-2-6a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/>
<path d="M2 13c0 1 1 1 1 1h5.256A4.493 4.493 0 0 1 8 12.5a4.49 4.49 0 0 1 1.544-3.393C9.077 9.038 8.564 9 8 9c-5 0-6 3-6 4Z"/>
</svg>

After

Width:  |  Height:  |  Size: 449 B

3
static/images/trash3-fill.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash3-fill" viewBox="0 0 16 16">
<path d="M11 1.5v1h3.5a.5.5 0 0 1 0 1h-.538l-.853 10.66A2 2 0 0 1 11.115 16h-6.23a2 2 0 0 1-1.994-1.84L2.038 3.5H1.5a.5.5 0 0 1 0-1H5v-1A1.5 1.5 0 0 1 6.5 0h3A1.5 1.5 0 0 1 11 1.5Zm-5 0v1h4v-1a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5ZM4.5 5.029l.5 8.5a.5.5 0 1 0 .998-.06l-.5-8.5a.5.5 0 1 0-.998.06Zm6.53-.528a.5.5 0 0 0-.528.47l-.5 8.5a.5.5 0 0 0 .998.058l.5-8.5a.5.5 0 0 0-.47-.528ZM8 4.5a.5.5 0 0 0-.5.5v8.5a.5.5 0 0 0 1 0V5a.5.5 0 0 0-.5-.5Z"/>
</svg>

After

Width:  |  Height:  |  Size: 582 B

3
static/images/x.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>

After

Width:  |  Height:  |  Size: 332 B

Loading…
Cancel
Save