Browse Source

Fixed a disastrous misconfiguration in the API; progress towards displaying TODOs

master
parent
commit
6414cd4233
  1. 15
      pages/about.html
  2. 13
      pages/base.html
  3. 234
      pages/index.html
  4. 9
      pages/list.html
  5. 62
      src/db/todo.go
  6. 6
      src/db/user.go
  7. 61
      src/server/api.go
  8. 2
      src/server/server.go

15
pages/about.html

@ -0,0 +1,15 @@
{{ template "base" . }}
{{ define "content" }}
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<main class="px-3">
<h1>Dela.</h1>
<p class="lead">A free and open-source web TODO list</p>
<p class="lead">
<a href="/login" class="btn btn-lg btn-primary">Login</a>
<a href="/register" class="btn btn-lg btn-primary">Register</a>
</p>
</main>
</div>
{{ end }}

13
pages/base.html

@ -45,11 +45,14 @@
<script src="/scripts/auth.js"></script> <script src="/scripts/auth.js"></script>
<script> <script>
window.onload = async () => { document.addEventListener('DOMContentLoaded', async function() {
let username = getUsername(); let username = getUsername();
let password = getUserPassword(); let password = getUserPassword();
if (username == null | username == "") { if (username == null | username == "" | password == null | password == "") {
if (window.location.pathname != "/about" && window.location.pathname != "/login" && window.location.pathname != "/register") {
window.location.replace("/about");
}
return; return;
} }
@ -69,10 +72,12 @@
document.getElementById("log-out-btn").addEventListener("click", (event) => { document.getElementById("log-out-btn").addEventListener("click", (event) => {
// Log out // Log out
forgetAuthInfo(); forgetAuthInfo();
window.location.replace("/"); window.location.replace("/about");
}); });
} else {
window.location.replace("/about");
} }
} }, false)
</script> </script>
{{ end }} {{ end }}

234
pages/index.html

@ -1,15 +1,229 @@
{{ template "base" . }} {{ template "base" . }}
{{ define "content" }} {{ define "content" }}
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<main class="px-3"> <!-- <div class="album py-5 bg-body-tertiary">
<h1>Dela.</h1> <div class="container">
<p class="lead">A free and open-source web TODO list</p> <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
<p class="lead"> <div class="col">
<a href="/login" class="btn btn-lg btn-primary">Login</a> <div class="card shadow-sm">
<a href="/register" class="btn btn-lg btn-primary">Register</a> <svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
</p> <div class="card-body">
</main> <p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
</div> <div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-body-secondary">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-body-secondary">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-body-secondary">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-body-secondary">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-body-secondary">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-body-secondary">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-body-secondary">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-body-secondary">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"></rect><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-body-secondary">9 mins</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div> -->
<!-- <div>
<form action="return false;">
<input type="text" name="" id="newTodoText">
<input type="date" name="" id="newTodoDue">
<button>Add</button>
</form>
</div> -->
<form>
<div class="mb-3">
<label for="new-todo-text" class="form-label">TODO</label>
<input type="text" class="form-control" id="new-todo-text">
</div>
<div class="mb-3">
<label for="new-todo-due" class="form-label">Due</label>
<input type="datetime-local" class="form-control" id="new-todo-due">
</div>
<button id="new-todo-submit" class="btn btn-primary">Add</button>
</form>
<div id="todos">
</div>
<div id="groups">
</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
document.getElementById("new-todo-submit").addEventListener("click", (event) => {
});
let groups = [];
let todos = [];
let username = getUsername();
let password = getUserPassword();
// TODO groups
let response = await fetch("/api/group", {
method: "GET",
headers: {
"EncryptedBase64": "false",
"Auth": username + "<-->" + password
},
});
let groupsJson = await response.json();
if (response.ok) {
let groupsDiv = document.getElementById("groups");
groupsJson.forEach((item) => {
groupsDiv.innerHTML += "<p>" + item.Name + "</p>";
});
}
// TODOs
response = await fetch("/api/todo", {
method: "GET",
headers: {
"EncryptedBase64": "false",
"Auth": username + "<-->" + password
},
});
let todosJson = await response.json();
if (response.ok) {
let todosDiv = document.getElementById("todos");
todosJson.forEach((item) => {
todosDiv.innerHTML += "<p>" + item.Text + "</p>";
});
// for (let i = 0; i < todosJson.length; i++) {
// console.log(todosJson[i]);
// todosDiv.innerHTML += "<p>" + todosJson[i].text + "</p>";
// }
}
}, false)
</script>
{{ end }} {{ end }}

9
pages/list.html

@ -1,9 +0,0 @@
{{ template "base" . }}
{{ define "content" }}
<script>
</script>
{{ end }}

62
src/db/todo.go

@ -4,20 +4,20 @@ import "database/sql"
// Todo group structure // Todo group structure
type TodoGroup struct { type TodoGroup struct {
ID uint64 ID uint64 `json: "id"`
Name string Name string `json: "name"`
TimeCreatedUnix uint64 TimeCreatedUnix uint64 `json: "timeCreatedUnix`
OwnerUsername string OwnerUsername string `json: "ownerUsername`
} }
// Todo structure // Todo structure
type Todo struct { type Todo struct {
ID uint64 ID uint64 `json: "id"`
GroupID uint64 GroupID uint64 `json: "groupId"`
Text string Text string `json: "text"`
TimeCreatedUnix uint64 TimeCreatedUnix uint64 `json: "timeCreatedUnix"`
DueUnix uint64 DueUnix uint64 `json: "dueUnix"`
OwnerUsername string OwnerUsername string `json: "ownerUsername"`
} }
// Creates a new TODO group in the database // Creates a new TODO group in the database
@ -67,6 +67,27 @@ func (db *DB) GetTodoGroup(id uint64) (*TodoGroup, error) {
return todoGroup, nil return todoGroup, nil
} }
// Retrieves information on ALL TODO groups
func (db *DB) GetTodoGroups() ([]*TodoGroup, error) {
var groups []*TodoGroup
rows, err := db.Query("SELECT * FROM todo_groups")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
todoGroup, err := scanTodoGroup(rows)
if err != nil {
return groups, err
}
groups = append(groups, todoGroup)
}
return groups, nil
}
// Deletes information about a TODO group of given ID from the database // Deletes information about a TODO group of given ID from the database
func (db *DB) DeleteTodoGroup(id uint64) error { func (db *DB) DeleteTodoGroup(id uint64) error {
_, err := db.Exec( _, err := db.Exec(
@ -124,6 +145,27 @@ func (db *DB) GetTodo(id uint64) (*Todo, error) {
return todo, nil return todo, nil
} }
// Retrieves information on ALL TODOs
func (db *DB) GetTodos() ([]*Todo, error) {
var todos []*Todo
rows, err := db.Query("SELECT * FROM todos")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
todo, err := scanTodo(rows)
if err != nil {
return todos, err
}
todos = append(todos, todo)
}
return todos, nil
}
// Creates a new TODO in the database // Creates a new TODO in the database
func (db *DB) CreateTodo(todo Todo) error { func (db *DB) CreateTodo(todo Todo) error {
_, err := db.Exec( _, err := db.Exec(

6
src/db/user.go

@ -4,9 +4,9 @@ import "database/sql"
// User structure // User structure
type User struct { type User struct {
Username string Username string `json: "username"`
Password string Password string `json: "password"`
TimeCreatedUnix uint64 TimeCreatedUnix uint64 `json: "timeCreatedUnix"`
} }
func scanUser(rows *sql.Rows) (*User, error) { func scanUser(rows *sql.Rows) (*User, error) {

61
src/server/api.go

@ -89,6 +89,22 @@ func (s *Server) UserEndpoint(w http.ResponseWriter, req *http.Request) {
return return
} }
// Create an initial TODO group
err = s.db.CreateTodoGroup(
db.TodoGroup{
Name: "Todos",
TimeCreatedUnix: uint64(time.Now().Unix()),
OwnerUsername: newUser.Username,
},
)
if err != nil {
// Oops, that's VERY bad. Delete newly created user
s.db.DeleteUser(newUser.Username)
logger.Error("[SERVER] Failed to create an initial TODO group for a newly created \"%s\": %s. Deleted.", newUser.Username, err)
http.Error(w, "Failed to create initial TODO group", http.StatusInternalServerError)
return
}
// Success! // Success!
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
logger.Info("[Server] Created a new user \"%s\"", newUser.Username) logger.Info("[Server] Created a new user \"%s\"", newUser.Username)
@ -190,41 +206,29 @@ func (s *Server) TodoEndpoint(w http.ResponseWriter, req *http.Request) {
logger.Info("[Server] Created a new TODO for %s", newTodo.OwnerUsername) logger.Info("[Server] Created a new TODO for %s", newTodo.OwnerUsername)
case http.MethodGet: case http.MethodGet:
// Retrieve TODO information // Retrieve TODO information
// Check authentication information // Check authentication information
if !IsRequestAuthValid(req, s.db) { if !IsRequestAuthValid(req, s.db) {
http.Error(w, "Invalid user auth data", http.StatusForbidden) http.Error(w, "Invalid user auth data", http.StatusForbidden)
return return
} }
todoID, err := GetTodoIDFromReq(req)
if err != nil {
http.Error(w, "Invalid TODO ID", http.StatusBadRequest)
return
}
if !DoesUserOwnTodo(GetUsernameFromAuth(req), todoID, s.db) {
http.Error(w, "You don't own this TODO", http.StatusForbidden)
return
}
// Get TODO // Get TODO
todo, err := s.db.GetTodo(todoID) todos, err := s.db.GetAllUserTodos(GetUsernameFromAuth(req))
if err != nil { if err != nil {
http.Error(w, "Failed to get TODO", http.StatusInternalServerError) http.Error(w, "Failed to get TODOs", http.StatusInternalServerError)
return return
} }
// Marshal to JSON // Marshal to JSON
todoBytes, err := json.Marshal(&todo) todosBytes, err := json.Marshal(&todos)
if err != nil { if err != nil {
http.Error(w, "Failed to marhsal TODO JSON", http.StatusInternalServerError) http.Error(w, "Failed to marhsal TODOs JSON", http.StatusInternalServerError)
return return
} }
// Send out // Send out
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.Write(todoBytes) w.Write(todosBytes)
case http.MethodPatch: case http.MethodPatch:
// Change TODO due date and text // Change TODO due date and text
@ -350,7 +354,7 @@ func (s *Server) TodoGroupEndpoint(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
logger.Info("[Server] Created a new TODO group for %s", newGroup.OwnerUsername) logger.Info("[Server] Created a new TODO group for %s", newGroup.OwnerUsername)
case http.MethodGet: case http.MethodGet:
// Retrieve todo group // Retrieve all todo groups
// Check authentication information // Check authentication information
if !IsRequestAuthValid(req, s.db) { if !IsRequestAuthValid(req, s.db) {
@ -358,28 +362,17 @@ func (s *Server) TodoGroupEndpoint(w http.ResponseWriter, req *http.Request) {
return return
} }
groupID, err := GetTodoIDFromReq(req) // Get groups
if err != nil { groups, err := s.db.GetAllUserTodoGroups(GetUsernameFromAuth(req))
http.Error(w, "Invalid group ID", http.StatusBadRequest)
return
}
if !DoesUserOwnTodoGroup(GetUsernameFromAuth(req), groupID, s.db) {
http.Error(w, "You don't own this group", http.StatusForbidden)
return
}
// Get group
group, err := s.db.GetTodoGroup(groupID)
if err != nil { if err != nil {
http.Error(w, "Failed to get TODO group", http.StatusInternalServerError) http.Error(w, "Failed to get TODO groups", http.StatusInternalServerError)
return return
} }
// Marshal to JSON // Marshal to JSON
groupBytes, err := json.Marshal(&group) groupBytes, err := json.Marshal(&groups)
if err != nil { if err != nil {
http.Error(w, "Failed to marhsal TODO group JSON", http.StatusInternalServerError) http.Error(w, "Failed to marhsal TODO groups JSON", http.StatusInternalServerError)
return return
} }

2
src/server/server.go

@ -125,7 +125,7 @@ func New(config conf.Conf) (*Server, error) {
}) })
mux.HandleFunc("/api/user", server.UserEndpoint) mux.HandleFunc("/api/user", server.UserEndpoint)
mux.HandleFunc("/api/todo", server.TodoEndpoint) mux.HandleFunc("/api/todo", server.TodoEndpoint)
mux.HandleFunc("/api/groups", server.TodoGroupEndpoint) mux.HandleFunc("/api/group", server.TodoGroupEndpoint)
server.http.Handler = mux server.http.Handler = mux
logger.Info("[Server] Created an HTTP server instance") logger.Info("[Server] Created an HTTP server instance")

Loading…
Cancel
Save