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. 232
      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>
window.onload = async () => {
document.addEventListener('DOMContentLoaded', async function() {
let username = getUsername();
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;
}
@ -69,10 +72,12 @@
document.getElementById("log-out-btn").addEventListener("click", (event) => {
// Log out
forgetAuthInfo();
window.location.replace("/");
window.location.replace("/about");
});
} else {
window.location.replace("/about");
}
}
}, false)
</script>
{{ end }}

232
pages/index.html

@ -1,15 +1,229 @@
{{ 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 class="album py-5 bg-body-tertiary">
<div class="container">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
<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 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 }}

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
type TodoGroup struct {
ID uint64
Name string
TimeCreatedUnix uint64
OwnerUsername string
ID uint64 `json: "id"`
Name string `json: "name"`
TimeCreatedUnix uint64 `json: "timeCreatedUnix`
OwnerUsername string `json: "ownerUsername`
}
// Todo structure
type Todo struct {
ID uint64
GroupID uint64
Text string
TimeCreatedUnix uint64
DueUnix uint64
OwnerUsername string
ID uint64 `json: "id"`
GroupID uint64 `json: "groupId"`
Text string `json: "text"`
TimeCreatedUnix uint64 `json: "timeCreatedUnix"`
DueUnix uint64 `json: "dueUnix"`
OwnerUsername string `json: "ownerUsername"`
}
// Creates a new TODO group in the database
@ -67,6 +67,27 @@ func (db *DB) GetTodoGroup(id uint64) (*TodoGroup, error) {
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
func (db *DB) DeleteTodoGroup(id uint64) error {
_, err := db.Exec(
@ -124,6 +145,27 @@ func (db *DB) GetTodo(id uint64) (*Todo, error) {
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
func (db *DB) CreateTodo(todo Todo) error {
_, err := db.Exec(

6
src/db/user.go

@ -4,9 +4,9 @@ import "database/sql"
// User structure
type User struct {
Username string
Password string
TimeCreatedUnix uint64
Username string `json: "username"`
Password string `json: "password"`
TimeCreatedUnix uint64 `json: "timeCreatedUnix"`
}
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
}
// 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!
w.WriteHeader(http.StatusOK)
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)
case http.MethodGet:
// Retrieve TODO information
// Check authentication information
if !IsRequestAuthValid(req, s.db) {
http.Error(w, "Invalid user auth data", http.StatusForbidden)
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
todo, err := s.db.GetTodo(todoID)
todos, err := s.db.GetAllUserTodos(GetUsernameFromAuth(req))
if err != nil {
http.Error(w, "Failed to get TODO", http.StatusInternalServerError)
http.Error(w, "Failed to get TODOs", http.StatusInternalServerError)
return
}
// Marshal to JSON
todoBytes, err := json.Marshal(&todo)
todosBytes, err := json.Marshal(&todos)
if err != nil {
http.Error(w, "Failed to marhsal TODO JSON", http.StatusInternalServerError)
http.Error(w, "Failed to marhsal TODOs JSON", http.StatusInternalServerError)
return
}
// Send out
w.Header().Add("Content-Type", "application/json")
w.Write(todoBytes)
w.Write(todosBytes)
case http.MethodPatch:
// Change TODO due date and text
@ -350,7 +354,7 @@ func (s *Server) TodoGroupEndpoint(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
logger.Info("[Server] Created a new TODO group for %s", newGroup.OwnerUsername)
case http.MethodGet:
// Retrieve todo group
// Retrieve all todo groups
// Check authentication information
if !IsRequestAuthValid(req, s.db) {
@ -358,28 +362,17 @@ func (s *Server) TodoGroupEndpoint(w http.ResponseWriter, req *http.Request) {
return
}
groupID, err := GetTodoIDFromReq(req)
if err != nil {
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)
// Get groups
groups, err := s.db.GetAllUserTodoGroups(GetUsernameFromAuth(req))
if err != nil {
http.Error(w, "Failed to get TODO group", http.StatusInternalServerError)
http.Error(w, "Failed to get TODO groups", http.StatusInternalServerError)
return
}
// Marshal to JSON
groupBytes, err := json.Marshal(&group)
groupBytes, err := json.Marshal(&groups)
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
}

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/todo", server.TodoEndpoint)
mux.HandleFunc("/api/groups", server.TodoGroupEndpoint)
mux.HandleFunc("/api/group", server.TodoGroupEndpoint)
server.http.Handler = mux
logger.Info("[Server] Created an HTTP server instance")

Loading…
Cancel
Save