diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9a4dd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dela.zip +bin/ diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..e69de29 diff --git a/pages/base.html b/pages/base.html index 6de1211..7424324 100644 --- a/pages/base.html +++ b/pages/base.html @@ -12,47 +12,31 @@ - - - -
-
-
- - - Dela - - - - - -
- Login - Sign-up -
-
-
-
- - - - - {{ template "content" . }} + + {{ template "content" . }} @@ -61,31 +45,24 @@ diff --git a/pages/category.html b/pages/category.html new file mode 100644 index 0000000..9df0c2e --- /dev/null +++ b/pages/category.html @@ -0,0 +1,111 @@ +{{ template "base" . }} + +{{ define "content" }} + +

{{.CurrentGroupId}}

+ + +
+ + + + + +
+
+
+
+
+ +
+
+ +
+
+ + +
+
+
+
+ +
+ + + + + + + + + {{ range .Todos }} + + + + + + + {{ end }} + +
ToDoCreatedDueGroup Id
{{ .Text }}{{ .TimeCreatedUnix }}{{ .DueUnix }}{{ .GroupID }}
+
+
+
+ + + +{{ end }} \ No newline at end of file diff --git a/pages/error.html b/pages/error.html new file mode 100644 index 0000000..7e9eb97 --- /dev/null +++ b/pages/error.html @@ -0,0 +1,12 @@ +{{ template "base" . }} + +{{ define "content" }} +
+
+

Error!

+

You have encountered an error

+

Try to reload the page or try again later

+
+
+ +{{ end }} \ No newline at end of file diff --git a/pages/index.html b/pages/index.html index c22011f..fde659a 100644 --- a/pages/index.html +++ b/pages/index.html @@ -9,262 +9,124 @@ - -
-
-
-
-
- -
-
- - -
-
+
+ {{ end }} \ No newline at end of file diff --git a/pages/login.html b/pages/login.html index 56d2ccd..d500c0a 100644 --- a/pages/login.html +++ b/pages/login.html @@ -44,10 +44,8 @@ async function logIn() { password = sha256(password); // Check if auth info is indeed valid - let response = await getUser(login, password); - + let response = await getUser(); if (response.ok) { - rememberAuthInfo(login, password); window.location.replace("/"); } else { document.getElementById("error_message").innerText = await response.text(); diff --git a/pages/register.html b/pages/register.html index 4ef6f02..b48056a 100644 --- a/pages/register.html +++ b/pages/register.html @@ -59,7 +59,6 @@ async function register() { let response = await postNewUser(postData); if (response.ok) { - rememberAuthInfo(postData.login, postData.password); window.location.replace("/"); } else { document.getElementById("error_message").innerText = await response.text(); diff --git a/pages/todos.html b/pages/todos.html deleted file mode 100644 index 9a5d7c5..0000000 --- a/pages/todos.html +++ /dev/null @@ -1,270 +0,0 @@ -{{ template "base" . }} - -{{ define "content" }} - - - - - -
-
-
-
- -
-
- - -
-
-
-
- - -
- - {{ for .Todos }} - - {{ .Name }} - - {{ end }} -
-
- - - - -{{ end }} \ No newline at end of file diff --git a/scripts/api.js b/scripts/api.js index a2e1ffa..7e93a60 100644 --- a/scripts/api.js +++ b/scripts/api.js @@ -1,83 +1,83 @@ /* - 2023 Kasyanov Nikolay Alexeyevich (Unbewohnte) + 2024 Kasyanov Nikolay Alexeyevich (Unbewohnte) */ - -async function post(url, login, password, json) { +async function post(url, json) { return fetch(url, { method: "POST", + credentials: "include", headers: { - "Authorization": "Basic " + btoa(login + ":" + password), "Content-Type": "application/json", }, body: JSON.stringify(json) }) } -async function postNewTodo(login, password, newTodo) { - return post("/api/todo/create", login, password, newTodo) + +async function postNewTodo(newTodo) { + return post("/api/todo/create", newTodo) } -async function postNewGroup(login, password, newGroup) { - return post("/api/group/create", login, password, newGroup) +async function postNewGroup(newGroup) { + return post("/api/group/create", newGroup) } async function postNewUser(newUser) { - return post("/api/user/create", "", "", newUser) + return post("/api/user/create", newUser) } -async function get(url, login, password) { +async function get(url) { return fetch(url, { method: "GET", + credentials: "include", headers: { - "Authorization": "Basic " + btoa(login + ":" + password), "Content-Type": "application/json", }, }) } -async function getUser(login, password) { - return get("/api/user/get", login, password); +async function getUser() { + return get("/api/user/get"); } -async function getTodos(login, password) { - return get("/api/todo/get", login, password); +async function getTodos() { + return get("/api/todo/get"); } -async function getGroup(login, password) { - return get("/api/group/get", login, password); +async function getGroup() { + return get("/api/group/get"); } -async function getAllGroups(login, password) { - return get("/api/user/get", login, password); +async function getAllGroups() { + return get("/api/user/get"); } -async function del(url, login, password) { +async function del(url) { return fetch(url, { method: "DELETE", + credentials: "include", headers: { - "Authorization": "Basic " + btoa(login + ":" + password), "Content-Type": "application/json", }, }) } -async function deleteTodo(login, password, id) { - return del("/api/todo/delete/"+id, login, password); +async function deleteTodo(id) { + return del("/api/todo/delete/"+id); } -async function update(url, login, password, json) { - return post(url, login, password, json); +async function update(url, json) { + return post(url, json); } -async function updateTodo(login, password, id, updatedTodo) { - return update("/api/todo/update/"+id, login, password, updatedTodo); +async function updateTodo(id, updatedTodo) { + return update("/api/todo/update/"+id, updatedTodo); } -async function updateGroup(login, password, id, updatedGroup) { - return update("/api/group/update/"+id, login, password, updateGroup); +async function updateGroup(id, updatedGroup) { + return update("/api/group/update/"+id, updatedGroup); } -async function updateUser(login, password, updatedUser) { - return update("/api/group/update/"+login, login, password, updatedUser); +async function updateUser(updatedUser) { + return update("/api/user/update", updatedUser); } \ No newline at end of file diff --git a/scripts/auth.js b/scripts/auth.js index aa5f5e1..d35fadf 100644 --- a/scripts/auth.js +++ b/scripts/auth.js @@ -1,30 +1,7 @@ /* - 2023 Kasyanov Nikolay Alexeyevich (Unbewohnte) + 2024 Kasyanov Nikolay Alexeyevich (Unbewohnte) */ - -// Saves auth information to local storage -function rememberAuthInfo(login, password) { - localStorage.setItem("login", login); - localStorage.setItem("password", password); -} - -// Retrieves user's password from local storage -function getUserPassword() { - return localStorage.getItem("password"); -} - -// Retrieves user's login from local storage -function getLogin() { -return localStorage.getItem("login"); -} - -// Removes all auth information from local storage -function forgetAuthInfo() { -localStorage.removeItem("login"); -localStorage.removeItem("password"); -} - /** * [js-sha256]{@link https://github.com/emn178/js-sha256} * diff --git a/src/db/db.go b/src/db/db.go index fbdeae3..130f337 100644 --- a/src/db/db.go +++ b/src/db/db.go @@ -1,6 +1,6 @@ /* dela - web TODO list - Copyright (C) 2023 Kasyanov Nikolay Alexeyevich (Unbewohnte) + Copyright (C) 2023, 2024 Kasyanov Nikolay Alexeyevich (Unbewohnte) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -33,10 +33,10 @@ type DB struct { func setUpTables(db *DB) error { // Users _, err := db.Exec(`CREATE TABLE IF NOT EXISTS users( - login TEXT PRIMARY KEY UNIQUE, - email TEXT NOT NULL UNIQUE, - password TEXT NOT NULL, - time_created_unix INTEGER)`, + login TEXT PRIMARY KEY UNIQUE, + email TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + time_created_unix INTEGER)`, ) if err != nil { return err @@ -48,6 +48,7 @@ func setUpTables(db *DB) error { name TEXT, time_created_unix INTEGER, owner_login TEXT NOT NULL, + removable INTEGER, FOREIGN KEY(owner_login) REFERENCES users(login))`, ) if err != nil { diff --git a/src/db/group.go b/src/db/group.go index f8699b9..c42e8a5 100644 --- a/src/db/group.go +++ b/src/db/group.go @@ -1,3 +1,21 @@ +/* + dela - web TODO list + Copyright (C) 2023, 2024 Kasyanov Nikolay Alexeyevich (Unbewohnte) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package db import "database/sql" @@ -8,15 +26,26 @@ type TodoGroup struct { Name string `json:"name"` TimeCreatedUnix uint64 `json:"timeCreatedUnix"` OwnerLogin string `json:"ownerLogin"` + Removable bool `json:"removable"` +} + +func NewTodoGroup(name string, timeCreatedUnix uint64, ownerLogin string, removable bool) TodoGroup { + return TodoGroup{ + Name: name, + TimeCreatedUnix: timeCreatedUnix, + OwnerLogin: ownerLogin, + Removable: removable, + } } // Creates a new TODO group in the database func (db *DB) CreateTodoGroup(group TodoGroup) error { _, err := db.Exec( - "INSERT INTO todo_groups(name, time_created_unix, owner_username) VALUES(?, ?, ?)", + "INSERT INTO todo_groups(name, time_created_unix, owner_login, removable) VALUES(?, ?, ?, ?)", group.Name, group.TimeCreatedUnix, group.OwnerLogin, + group.Removable, ) return err @@ -29,6 +58,7 @@ func scanTodoGroup(rows *sql.Rows) (*TodoGroup, error) { &newTodoGroup.Name, &newTodoGroup.TimeCreatedUnix, &newTodoGroup.OwnerLogin, + &newTodoGroup.Removable, ) if err != nil { return nil, err @@ -78,6 +108,26 @@ func (db *DB) GetTodoGroups() ([]*TodoGroup, error) { return groups, nil } +func (db *DB) GetGroupTodos(groupId uint64) ([]*Todo, error) { + rows, err := db.Query("SELECT * FROM todos WHERE group_id=?", groupId) + if err != nil { + return nil, err + } + defer rows.Close() + + var todos []*Todo + + for rows.Next() { + todoGroup, err := scanTodo(rows) + if err != nil { + return todos, err + } + todos = append(todos, todoGroup) + } + + return todos, nil +} + // Deletes information about a TODO group of given ID from the database func (db *DB) DeleteTodoGroup(id uint64) error { _, err := db.Exec( diff --git a/src/db/todo.go b/src/db/todo.go index b035ac4..3451a98 100644 --- a/src/db/todo.go +++ b/src/db/todo.go @@ -1,6 +1,6 @@ /* dela - web TODO list - Copyright (C) 2023 Kasyanov Nikolay Alexeyevich (Unbewohnte) + Copyright (C) 2023, 2024 Kasyanov Nikolay Alexeyevich (Unbewohnte) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -95,7 +95,7 @@ func (db *DB) GetTodos() ([]*Todo, error) { // Creates a new TODO in the database func (db *DB) CreateTodo(todo Todo) error { _, err := db.Exec( - "INSERT INTO todos(group_id, text, time_created_unix, due_unix, owner_username, is_done, completion_time_unix) VALUES(?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO todos(group_id, text, time_created_unix, due_unix, owner_login, is_done, completion_time_unix) VALUES(?, ?, ?, ?, ?, ?, ?)", todo.GroupID, todo.Text, todo.TimeCreatedUnix, @@ -134,12 +134,12 @@ func (db *DB) UpdateTodo(todoID uint64, updatedTodo Todo) error { } // Searches and retrieves TODO groups created by the user -func (db *DB) GetAllUserTodoGroups(username string) ([]*TodoGroup, error) { +func (db *DB) GetAllUserTodoGroups(login string) ([]*TodoGroup, error) { var todoGroups []*TodoGroup rows, err := db.Query( - "SELECT * FROM todo_groups WHERE owner_username=?", - username, + "SELECT * FROM todo_groups WHERE owner_login=?", + login, ) if err != nil { return nil, err @@ -158,12 +158,12 @@ func (db *DB) GetAllUserTodoGroups(username string) ([]*TodoGroup, error) { } // Searches and retrieves TODOs created by the user -func (db *DB) GetAllUserTodos(username string) ([]*Todo, error) { +func (db *DB) GetAllUserTodos(login string) ([]*Todo, error) { var todos []*Todo rows, err := db.Query( - "SELECT * FROM todos WHERE owner_username=?", - username, + "SELECT * FROM todos WHERE owner_login=?", + login, ) if err != nil { return nil, err @@ -183,20 +183,20 @@ func (db *DB) GetAllUserTodos(username string) ([]*Todo, error) { } // Deletes all information regarding TODOs of specified user -func (db *DB) DeleteAllUserTodos(username string) error { +func (db *DB) DeleteAllUserTodos(login string) error { _, err := db.Exec( - "DELETE FROM todos WHERE owner_username=?", - username, + "DELETE FROM todos WHERE owner_login=?", + login, ) return err } // Deletes all information regarding TODO groups of specified user -func (db *DB) DeleteAllUserTodoGroups(username string) error { +func (db *DB) DeleteAllUserTodoGroups(login string) error { _, err := db.Exec( - "DELETE FROM todo_groups WHERE owner_username=?", - username, + "DELETE FROM todo_groups WHERE owner_login=?", + login, ) return err diff --git a/src/server/endpoints.go b/src/server/endpoints.go index d778c49..c19372b 100644 --- a/src/server/endpoints.go +++ b/src/server/endpoints.go @@ -1,6 +1,6 @@ /* dela - web TODO list - Copyright (C) 2023 Kasyanov Nikolay Alexeyevich (Unbewohnte) + Copyright (C) 2023, 2024 Kasyanov Nikolay Alexeyevich (Unbewohnte) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -22,6 +22,7 @@ import ( "Unbewohnte/dela/db" "Unbewohnte/dela/logger" "encoding/json" + "fmt" "io" "net/http" "path" @@ -70,12 +71,36 @@ func (s *Server) EndpointUserCreate(w http.ResponseWriter, req *http.Request) { } logger.Info("[Server][EndpointUserCreate] Created a new user with login \"%s\"", user.Login) + + // Create a non-removable default category + err = s.db.CreateTodoGroup(db.NewTodoGroup( + "Notes", + uint64(time.Now().Unix()), + user.Login, + false, + )) + if err != nil { + http.Error(w, "Failed to create default group", http.StatusInternalServerError) + logger.Error("[Server][EndpojntUserCreate] Failed to create a default group for %s: %s", user.Login, err) + return + } + + // Send cookie + http.SetCookie(w, &http.Cookie{ + Name: "auth", + Value: fmt.Sprintf("%s:%s", user.Login, user.Password), + SameSite: http.SameSiteStrictMode, + HttpOnly: false, + Path: "/", + Secure: true, + }) w.WriteHeader(http.StatusOK) } func (s *Server) EndpointUserUpdate(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return } // Retrieve user data @@ -103,7 +128,7 @@ func (s *Server) EndpointUserUpdate(w http.ResponseWriter, req *http.Request) { } // Check whether the user in request is the user specified in JSON - login, _, _ := req.BasicAuth() + login := GetLoginFromReq(req) if login != user.Login { // Gotcha! logger.Warning("[Server][EndpointUserUpdate] %s tried to update user information of %s!", login, user.Login) @@ -138,7 +163,7 @@ func (s *Server) EndpointUserDelete(w http.ResponseWriter, req *http.Request) { } // Delete - login, _, _ := req.BasicAuth() + login := GetLoginFromReq(req) err := s.db.DeleteUser(login) if err != nil { http.Error(w, "Failed to delete user", http.StatusInternalServerError) @@ -165,7 +190,7 @@ func (s *Server) EndpointUserGet(w http.ResponseWriter, req *http.Request) { } // Get information from the database - login, _, _ := req.BasicAuth() + login := GetLoginFromReq(req) userDB, err := s.db.GetUser(login) if err != nil { logger.Error("[Server][EndpointUserGet] Failed to retrieve information on \"%s\": %s", login, err) @@ -207,7 +232,7 @@ func (s *Server) EndpointTodoUpdate(w http.ResponseWriter, req *http.Request) { } // Check if the user owns this TODO - if !s.db.DoesUserOwnTodo(todoID, GetLoginFromAuth(req)) { + if !s.db.DoesUserOwnTodo(todoID, GetLoginFromReq(req)) { http.Error(w, "You don't own this TODO", http.StatusForbidden) return } @@ -265,7 +290,7 @@ func (s *Server) EndpointTodoDelete(w http.ResponseWriter, req *http.Request) { } // Check if the user owns this TODO - if !s.db.DoesUserOwnTodo(todoID, GetLoginFromAuth(req)) { + if !s.db.DoesUserOwnTodo(todoID, GetLoginFromReq(req)) { http.Error(w, "You don't own this TODO", http.StatusForbidden) return } @@ -273,7 +298,7 @@ func (s *Server) EndpointTodoDelete(w http.ResponseWriter, req *http.Request) { // Now delete err = s.db.DeleteTodo(todoID) if err != nil { - logger.Error("[Server] Failed to delete %s's TODO: %s", GetLoginFromAuth(req), err) + logger.Error("[Server] Failed to delete %s's TODO: %s", GetLoginFromReq(req), err) http.Error(w, "Failed to delete TODO", http.StatusInternalServerError) return } @@ -310,7 +335,7 @@ func (s *Server) EndpointTodoCreate(w http.ResponseWriter, req *http.Request) { } // Add TODO to the database - newTodo.OwnerLogin = GetLoginFromAuth(req) + newTodo.OwnerLogin = GetLoginFromReq(req) newTodo.TimeCreatedUnix = uint64(time.Now().Unix()) err = s.db.CreateTodo(newTodo) if err != nil { @@ -342,7 +367,7 @@ func (s *Server) EndpointUserTodosGet(w http.ResponseWriter, req *http.Request) } // Get all user TODOs - todos, err := s.db.GetAllUserTodos(GetLoginFromAuth(req)) + todos, err := s.db.GetAllUserTodos(GetLoginFromReq(req)) if err != nil { http.Error(w, "Failed to get TODOs", http.StatusInternalServerError) return @@ -387,7 +412,7 @@ func (s *Server) EndpointTodoGroupDelete(w http.ResponseWriter, req *http.Reques return } - if !s.db.DoesUserOwnGroup(group.ID, GetLoginFromAuth(req)) { + if !s.db.DoesUserOwnGroup(group.ID, GetLoginFromReq(req)) { http.Error(w, "You don't own this group", http.StatusForbidden) return } @@ -395,7 +420,7 @@ func (s *Server) EndpointTodoGroupDelete(w http.ResponseWriter, req *http.Reques // Now delete err = s.db.DeleteTodoGroup(group.ID) if err != nil { - logger.Error("[Server] Failed to delete %s's TODO group: %s", GetLoginFromAuth(req), err) + logger.Error("[Server] Failed to delete %s's TODO group: %s", GetLoginFromReq(req), err) http.Error(w, "Failed to delete TODO group", http.StatusInternalServerError) return } @@ -431,7 +456,7 @@ func (s *Server) EndpointTodoGroupCreate(w http.ResponseWriter, req *http.Reques } // Add group to the database - newGroup.OwnerLogin = GetLoginFromAuth(req) + newGroup.OwnerLogin = GetLoginFromReq(req) newGroup.TimeCreatedUnix = uint64(time.Now().Unix()) err = s.db.CreateTodoGroup(newGroup) if err != nil { @@ -456,7 +481,7 @@ func (s *Server) EndpointTodoGroupGet(w http.ResponseWriter, req *http.Request) } // Get groups - groups, err := s.db.GetAllUserTodoGroups(GetLoginFromAuth(req)) + groups, err := s.db.GetAllUserTodoGroups(GetLoginFromReq(req)) if err != nil { http.Error(w, "Failed to get TODO groups", http.StatusInternalServerError) return diff --git a/src/server/page.go b/src/server/page.go index 6bbeebf..081c371 100644 --- a/src/server/page.go +++ b/src/server/page.go @@ -1,6 +1,6 @@ /* dela - web TODO list - Copyright (C) 2023 Kasyanov Nikolay Alexeyevich (Unbewohnte) + Copyright (C) 2023, 2024 Kasyanov Nikolay Alexeyevich (Unbewohnte) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -19,6 +19,7 @@ package server import ( + "Unbewohnte/dela/db" "html/template" "path/filepath" ) @@ -35,3 +36,42 @@ func getPage(pagesDir string, basePageName string, pageName string) (*template.T return page, nil } + +type IndexPageData struct { + Groups []*db.TodoGroup `json:"groups"` +} + +func GetIndexPageData(db *db.DB, login string) (*IndexPageData, error) { + groups, err := db.GetAllUserTodoGroups(login) + if err != nil { + return nil, err + } + + return &IndexPageData{ + Groups: groups, + }, nil +} + +type CategoryPageData struct { + Groups []*db.TodoGroup `json:"groups"` + CurrentGroupId uint64 `json:"currentGroupId"` + Todos []*db.Todo `json:"todos"` +} + +func GetCategoryPageData(db *db.DB, login string, groupId uint64) (*CategoryPageData, error) { + groups, err := db.GetAllUserTodoGroups(login) + if err != nil { + return nil, err + } + + todos, err := db.GetGroupTodos(groupId) + if err != nil { + return nil, err + } + + return &CategoryPageData{ + Groups: groups, + CurrentGroupId: groupId, + Todos: todos, + }, nil +} diff --git a/src/server/server.go b/src/server/server.go index 18c6aee..9994ec4 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -1,6 +1,6 @@ /* dela - web TODO list - Copyright (C) 2023 Kasyanov Nikolay Alexeyevich (Unbewohnte) + Copyright (C) 2023, 2024 Kasyanov Nikolay Alexeyevich (Unbewohnte) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -25,8 +25,11 @@ import ( "context" "fmt" "net/http" + "net/http/cookiejar" "os" + "path" "path/filepath" + "strconv" "time" ) @@ -37,9 +40,10 @@ const ( ) type Server struct { - config conf.Conf - db *db.DB - http http.Server + config conf.Conf + db *db.DB + http http.Server + cookieJar *cookiejar.Jar } // Creates a new server instance with provided config @@ -102,19 +106,75 @@ func New(config conf.Conf) (*Server, error) { // handle page requests mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { - switch req.URL.Path { - case "/": + if req.Method != "GET" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + if req.URL.Path == "/" { + // Auth first + if !IsUserAuthorizedReq(req, server.db) { + http.Redirect(w, req, "/about", http.StatusTemporaryRedirect) + return + } + requestedPage, err := getPage( filepath.Join(server.config.BaseContentDir, PagesDirName), "base.html", "index.html", ) if err != nil { - http.Redirect(w, req, "/about", http.StatusTemporaryRedirect) + http.Redirect(w, req, "/error", http.StatusTemporaryRedirect) logger.Error("[Server][/] Failed to get a page: %s", err) return } - requestedPage.ExecuteTemplate(w, "index.html", nil) - default: + pageData, err := GetIndexPageData(server.db, GetLoginFromReq(req)) + if err != nil { + http.Redirect(w, req, "/error", http.StatusTemporaryRedirect) + logger.Error("[Server][/] Failed to get index page data: %s", err) + return + } + + requestedPage.ExecuteTemplate(w, "index.html", &pageData) + } else if path.Dir(req.URL.Path) == "/group" { + if req.Method != "GET" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Auth first + if !IsUserAuthorizedReq(req, server.db) { + http.Redirect(w, req, "/about", http.StatusTemporaryRedirect) + return + } + + // Get group ID + groupId, err := strconv.ParseUint(path.Base(req.URL.Path), 10, 64) + if err != nil { + http.Redirect(w, req, "/error", http.StatusTemporaryRedirect) + return + } + + requestedPage, err := getPage( + filepath.Join(server.config.BaseContentDir, PagesDirName), "base.html", "category.html", + ) + if err != nil { + http.Redirect(w, req, "/error", http.StatusTemporaryRedirect) + logger.Error("[Server][/category/] Failed to get a page: %s", err) + return + } + + // Get page data + pageData, err := GetCategoryPageData(server.db, GetLoginFromReq(req), groupId) + if err != nil { + http.Redirect(w, req, "/error", http.StatusTemporaryRedirect) + logger.Error("[Server][/category/] Failed to get category (%d) page data: %s", groupId, err) + return + } + + requestedPage.ExecuteTemplate(w, "category.html", &pageData) + + } else { + // default requestedPage, err := getPage( filepath.Join(server.config.BaseContentDir, PagesDirName), "base.html", @@ -123,7 +183,8 @@ func New(config conf.Conf) (*Server, error) { if err == nil { requestedPage.ExecuteTemplate(w, req.URL.Path[1:]+".html", nil) } else { - http.Error(w, "Page processing error", http.StatusInternalServerError) + // http.Error(w, "Page processing error", http.StatusInternalServerError) + http.Redirect(w, req, "/error", http.StatusTemporaryRedirect) } } }) @@ -131,6 +192,7 @@ func New(config conf.Conf) (*Server, error) { mux.HandleFunc("/api/user/delete", server.EndpointUserDelete) // Non specific mux.HandleFunc("/api/user/update", server.EndpointUserUpdate) // Non specific mux.HandleFunc("/api/user/create", server.EndpointUserCreate) // Non specific + mux.HandleFunc("/api/todo/create", server.EndpointTodoCreate) // Non specific mux.HandleFunc("/api/todo/get", server.EndpointUserTodosGet) // Non specific mux.HandleFunc("/api/todo/delete/", server.EndpointTodoDelete) // Specific mux.HandleFunc("/api/todo/update/", server.EndpointTodoUpdate) // Specific @@ -140,6 +202,9 @@ func New(config conf.Conf) (*Server, error) { mux.HandleFunc("/api/group/delete/", server.EndpointTodoGroupDelete) // Specific server.http.Handler = mux + jar, _ := cookiejar.New(nil) + server.cookieJar = jar + logger.Info("[Server] Created an HTTP server instance") return &server, nil diff --git a/src/server/validation.go b/src/server/validation.go index 9cc19b3..cb423e1 100644 --- a/src/server/validation.go +++ b/src/server/validation.go @@ -1,6 +1,6 @@ /* dela - web TODO list - Copyright (C) 2023 Kasyanov Nikolay Alexeyevich (Unbewohnte) + Copyright (C) 2023, 2024 Kasyanov Nikolay Alexeyevich (Unbewohnte) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -21,6 +21,7 @@ package server import ( "Unbewohnte/dela/db" "net/http" + "strings" ) const ( @@ -34,11 +35,16 @@ func IsUserValid(user db.User) (bool, string) { if uint(len(user.Login)) < MinimalLoginLength { return false, "Login is too small" } + for _, char := range user.Login { + if char < 0x21 || char > 0x7E { + // Not printable ASCII char! + return false, "Login has a non printable ASCII character" + } + } if uint(len(user.Password)) < MinimalPasswordLength { return false, "Password is too small" } - for _, char := range user.Password { if char < 0x21 || char > 0x7E { // Not printable ASCII char! @@ -63,15 +69,36 @@ func IsUserAuthorized(db *db.DB, user db.User) bool { return true } +// Returns login and password from a cookie. If an error is encountered, returns empty strings +func AuthFromCookie(cookie *http.Cookie) (string, string) { + if cookie == nil { + return "", "" + } + + parts := strings.Split(cookie.Value, ":") + if len(parts) != 2 { + return "", "" + } + + return parts[0], parts[1] +} + /* Gets auth information from a request and checks if such a user exists and compares passwords. Returns true if such user exists and passwords do match */ func IsUserAuthorizedReq(req *http.Request, dbase *db.DB) bool { - login, password, ok := req.BasicAuth() - if !ok { - return false + var login, password string + var ok bool + login, password, ok = req.BasicAuth() + if !ok || login == "" || password == "" { + cookie, err := req.Cookie("auth") + if err != nil { + return false + } + + login, password = AuthFromCookie(cookie) } return IsUserAuthorized(dbase, db.User{ @@ -80,7 +107,17 @@ func IsUserAuthorizedReq(req *http.Request, dbase *db.DB) bool { }) } -func GetLoginFromAuth(req *http.Request) string { - login, _, _ := req.BasicAuth() +// Returns login value from basic auth or from cookie if the former does not exist +func GetLoginFromReq(req *http.Request) string { + login, _, ok := req.BasicAuth() + if !ok || login == "" { + cookie, err := req.Cookie("auth") + if err != nil { + return "" + } + + login, _ = AuthFromCookie(cookie) + } + return login } diff --git a/static/images/box-arrow-up-right.svg b/static/images/box-arrow-up-right.svg new file mode 100644 index 0000000..03f68d5 --- /dev/null +++ b/static/images/box-arrow-up-right.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file