Browse Source

Page: Added profile page

master
parent
commit
72f7fbbe4b
  1. 3
      pages/base.html
  2. 74
      pages/profile.html
  3. 4
      scripts/api.js
  4. 4
      src/db/user.go
  5. 4
      src/i18n/i18n.go
  6. 9
      src/i18n/page.go
  7. 32
      src/server/endpoints.go
  8. 2
      src/server/page.go
  9. 48
      src/server/server.go
  10. 10
      src/server/validation.go
  11. 4
      static/images/person-vcard.svg
  12. 0
      translations/ENG/about.json
  13. 0
      translations/ENG/base.json
  14. 0
      translations/ENG/category.json
  15. 0
      translations/ENG/error.json
  16. 0
      translations/ENG/index.json
  17. 0
      translations/ENG/login.json
  18. 3
      translations/ENG/paint.json
  19. 40
      translations/ENG/profile.json
  20. 0
      translations/ENG/register.json
  21. 2
      translations/RU/about.json
  22. 2
      translations/RU/base.json
  23. 2
      translations/RU/category.json
  24. 2
      translations/RU/error.json
  25. 2
      translations/RU/index.json
  26. 2
      translations/RU/login.json
  27. 3
      translations/RU/paint.json
  28. 40
      translations/RU/profile.json
  29. 2
      translations/RU/register.json
  30. 1
      translations/eng/paint.json
  31. 1
      translations/ru/paint.json

3
pages/base.html

@ -43,6 +43,9 @@
<small id="locale">ENG</small>
</button>
</div>
<div class="text-end">
<a class="btn btn-secondary" href="/profile"><img src="/static/images/person-vcard.svg"></a>
</div>
<div class="text-end" id="bar-auth">
<a href="/login" class="btn btn-outline-light me-2">{{index .Translation "base link log in"}}</a>
<a href="/register" class="btn btn-warning">{{index .Translation "base link sign up"}}</a>

74
pages/profile.html

@ -0,0 +1,74 @@
{{ template "base" . }}
{{ define "content" }}
<main class="container my-5">
<div class="row d-flex justify-content-center">
<div class="col col-md-9 col-lg-7 col-xl-6">
<div class="card" style="border-radius: 15px;">
<div class="card-body p-4">
<div class="d-flex">
<div class="flex-shrink-0">
<img src="/static/images/person-vcard.svg" class="img-fluid" style="width: 128px; border-radius: 10px;">
</div>
<div class="flex-grow-1 ms-3">
<h5 class="mb-1">{{ .Data.Email }}</h5>
<p class="mb-2 pb-1">{{index .Translation "profile created"}}: {{ .Data.TimeCreated }}</p>
<div class="d-flex pt-1">
<button type="button" class="btn btn-primary flex-grow-1" onclick="logOut();">
{{index .Translation "profile log out"}}
</button>
<button type="button" class="btn btn-outline-danger flex-grow-1" onclick="openDeleteModal();">
{{index .Translation "profile delete account"}}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">{{index .Translation "profile modal confirm deletion"}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{index .Translation "profile modal deletion confirmation"}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" id="confirmDeleteButton" onclick="handleAccountDelete();">
{{index .Translation "profile modal delete button"}}
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{{index .Translation "profile modal cancel button"}}
</button>
</div>
</div>
</div>
</div>
<!-- End delete confirmation modal -->
<script>
function logOut() {
forgetAuthInfo();
window.location.replace("/about");
}
function openDeleteModal() {
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'), {});
deleteModal.show();
}
async function handleAccountDelete() {
await deleteAccount();
window.location.replace("/about");
}
</script>
{{ end }}

4
scripts/api.js

@ -70,6 +70,10 @@ async function deleteTodo(id) {
return del("/api/todo/delete/"+id);
}
async function deleteAccount() {
return del("/api/user/delete");
}
async function deleteCategory(id) {
return del("/api/group/delete/"+id);
}

4
src/db/user.go

@ -25,6 +25,7 @@ type User struct {
Email string `json:"email"`
Password string `json:"password"`
TimeCreatedUnix uint64 `json:"timeCreatedUnix"`
TimeCreated string `json:"timeCreated"`
ConfirmedEmail bool `json:"confirmedEmail"`
}
@ -36,6 +37,9 @@ func scanUser(rows *sql.Rows) (*User, error) {
return nil, err
}
// Convert to Basic time string
user.TimeCreated = unixToTimeStr(user.TimeCreatedUnix)
return &user, nil
}

4
src/i18n/i18n.go

@ -13,8 +13,8 @@ func (l *Language) String() string {
}
const (
Ru Language = "ru"
Eng Language = "eng"
RU Language = "RU"
ENG Language = "ENG"
)
type Translations []*Translation

9
src/i18n/page.go

@ -1,6 +1,7 @@
package i18n
import (
"fmt"
"path/filepath"
)
@ -12,5 +13,13 @@ func GetPageTranslation(pageName string, language Language, translationsDirPath
return nil, err
}
if translation.Language != language {
return translation, fmt.Errorf(
"translation language (%s) differs from what was requested (%s)",
translation.Language,
language,
)
}
return translation, nil
}

32
src/server/endpoints.go

@ -295,7 +295,7 @@ func (s *Server) EndpointUserUpdate(w http.ResponseWriter, req *http.Request) {
}
// Check whether the user in request is the user specified in JSON
email := GetLoginFromReq(req)
email := GetEmailFromReq(req)
if email != user.Email {
// Gotcha!
logger.Warning("[Server][EndpointUserUpdate] %s tried to update user information of %s!", email, user.Email)
@ -330,8 +330,8 @@ func (s *Server) EndpointUserDelete(w http.ResponseWriter, req *http.Request) {
}
// Delete
email := GetLoginFromReq(req)
err := s.db.DeleteUser(email)
email := GetEmailFromReq(req)
err := s.db.DeleteUserClean(email)
if err != nil {
http.Error(w, "Failed to delete user", http.StatusInternalServerError)
logger.Error("[Server][EndpointUserDelete] Failed to delete \"%s\": %s", email, err)
@ -357,7 +357,7 @@ func (s *Server) EndpointUserGet(w http.ResponseWriter, req *http.Request) {
}
// Get information from the database
email := GetLoginFromReq(req)
email := GetEmailFromReq(req)
userDB, err := s.db.GetUser(email)
if err != nil {
logger.Error("[Server][EndpointUserGet] Failed to retrieve information on \"%s\": %s", email, err)
@ -399,7 +399,7 @@ func (s *Server) EndpointTodoUpdate(w http.ResponseWriter, req *http.Request) {
}
// Check if the user owns this TODO
if !s.db.DoesUserOwnTodo(todoID, GetLoginFromReq(req)) {
if !s.db.DoesUserOwnTodo(todoID, GetEmailFromReq(req)) {
http.Error(w, "You don't own this TODO", http.StatusForbidden)
return
}
@ -455,7 +455,7 @@ func (s *Server) EndpointTodoMarkDone(w http.ResponseWriter, req *http.Request)
}
// Check if the user owns this TODO
if !s.db.DoesUserOwnTodo(todoID, GetLoginFromReq(req)) {
if !s.db.DoesUserOwnTodo(todoID, GetEmailFromReq(req)) {
http.Error(w, "You don't own this TODO", http.StatusForbidden)
return
}
@ -504,7 +504,7 @@ func (s *Server) EndpointTodoDelete(w http.ResponseWriter, req *http.Request) {
}
// Check if the user owns this TODO
if !s.db.DoesUserOwnTodo(todoID, GetLoginFromReq(req)) {
if !s.db.DoesUserOwnTodo(todoID, GetEmailFromReq(req)) {
http.Error(w, "You don't own this TODO", http.StatusForbidden)
return
}
@ -512,7 +512,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", GetLoginFromReq(req), err)
logger.Error("[Server] Failed to delete %s's TODO: %s", GetEmailFromReq(req), err)
http.Error(w, "Failed to delete TODO", http.StatusInternalServerError)
return
}
@ -559,12 +559,12 @@ func (s *Server) EndpointTodoCreate(w http.ResponseWriter, req *http.Request) {
return
}
if !s.db.DoesUserOwnGroup(newTodo.GroupID, GetLoginFromReq(req)) {
if !s.db.DoesUserOwnGroup(newTodo.GroupID, GetEmailFromReq(req)) {
http.Error(w, "You do not own this group", http.StatusForbidden)
return
}
newTodo.OwnerEmail = GetLoginFromReq(req)
newTodo.OwnerEmail = GetEmailFromReq(req)
newTodo.TimeCreatedUnix = uint64(time.Now().Unix())
err = s.db.CreateTodo(newTodo)
if err != nil {
@ -596,7 +596,7 @@ func (s *Server) EndpointUserTodosGet(w http.ResponseWriter, req *http.Request)
}
// Get all user TODOs
todos, err := s.db.GetAllUserTodos(GetLoginFromReq(req))
todos, err := s.db.GetAllUserTodos(GetEmailFromReq(req))
if err != nil {
http.Error(w, "Failed to get TODOs", http.StatusInternalServerError)
return
@ -636,7 +636,7 @@ func (s *Server) EndpointTodoGroupDelete(w http.ResponseWriter, req *http.Reques
return
}
if !s.db.DoesUserOwnGroup(groupId, GetLoginFromReq(req)) {
if !s.db.DoesUserOwnGroup(groupId, GetEmailFromReq(req)) {
http.Error(w, "You don't own this group", http.StatusForbidden)
return
}
@ -657,13 +657,13 @@ func (s *Server) EndpointTodoGroupDelete(w http.ResponseWriter, req *http.Reques
// Delete all ToDos associated with this group and then delete the group itself
err = s.db.DeleteTodoGroupClean(groupId)
if err != nil {
logger.Error("[Server][EndpointGroupDelete] Failed to delete %s's TODO group: %s", GetLoginFromReq(req), err)
logger.Error("[Server][EndpointGroupDelete] Failed to delete %s's TODO group: %s", GetEmailFromReq(req), err)
http.Error(w, "Failed to delete TODO group", http.StatusInternalServerError)
return
}
// Success!
logger.Info("[Server][EndpointGroupDelete] Cleanly deleted group ID: %d for %s", groupId, GetLoginFromReq(req))
logger.Info("[Server][EndpointGroupDelete] Cleanly deleted group ID: %d for %s", groupId, GetEmailFromReq(req))
w.WriteHeader(http.StatusOK)
}
@ -700,7 +700,7 @@ func (s *Server) EndpointTodoGroupCreate(w http.ResponseWriter, req *http.Reques
}
// Add group to the database
newGroup.OwnerEmail = GetLoginFromReq(req)
newGroup.OwnerEmail = GetEmailFromReq(req)
newGroup.TimeCreatedUnix = uint64(time.Now().Unix())
newGroup.Removable = true
err = s.db.CreateTodoGroup(newGroup)
@ -725,7 +725,7 @@ func (s *Server) EndpointTodoGroupGet(w http.ResponseWriter, req *http.Request)
}
// Get groups
groups, err := s.db.GetAllUserTodoGroups(GetLoginFromReq(req))
groups, err := s.db.GetAllUserTodoGroups(GetEmailFromReq(req))
if err != nil {
http.Error(w, "Failed to get TODO groups", http.StatusInternalServerError)
return

2
src/server/page.go

@ -37,7 +37,7 @@ func (s *Server) GetPageData(templateNames []string, language i18n.Language) (*P
pageTranslation, err := i18n.GetPageTranslation(page, language, translationsDirPath)
if err != nil {
// Try ENG
pageTranslation, err = i18n.GetPageTranslation(page, i18n.Eng, translationsDirPath)
pageTranslation, err = i18n.GetPageTranslation(page, i18n.ENG, translationsDirPath)
if err != nil {
return nil, err
}

48
src/server/server.go

@ -146,7 +146,7 @@ func New(config conf.Conf) (*Server, error) {
return
}
indexPageData, err := GetIndexPageData(server.db, GetLoginFromReq(req))
indexPageData, err := GetIndexPageData(server.db, GetEmailFromReq(req))
if err != nil {
http.Redirect(w, req, "/error", http.StatusTemporaryRedirect)
logger.Error("[Server][/] Failed to get index page data: %s", err)
@ -205,7 +205,7 @@ func New(config conf.Conf) (*Server, error) {
return
}
categoriesData, err := GetCategoryPageData(server.db, GetLoginFromReq(req), groupId)
categoriesData, err := GetCategoryPageData(server.db, GetEmailFromReq(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)
@ -220,6 +220,50 @@ func New(config conf.Conf) (*Server, error) {
return
}
} else if req.URL.Path == "/profile" {
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
}
email := GetEmailFromReq(req)
user, err := server.db.GetUser(email)
if err != nil {
// TODO
return
}
user.Password = "" // No passwords sent
pageData, err := server.GetPageData([]string{"profile", "base"}, LanguageFromReq(req))
if err != nil {
// TODO
return
}
pageData.Data = user
requestedPage, err := template.ParseFiles(
filepath.Join(pagesDirPath, "base.html"),
filepath.Join(pagesDirPath, "profile.html"),
)
if err != nil {
http.Redirect(w, req, "/error", http.StatusTemporaryRedirect)
logger.Error("[Server][/profile] Failed to get a page: %s", err)
return
}
err = requestedPage.ExecuteTemplate(w, "profile.html", &pageData)
if err != nil {
http.Redirect(w, req, "/error", http.StatusTemporaryRedirect)
logger.Error("[Server][/profile] Template error: %s", err)
return
}
} else {
// default
requestedPage, err := template.ParseFiles(

10
src/server/validation.go

@ -112,7 +112,7 @@ func IsUserAuthorizedReq(req *http.Request, dbase *db.DB) bool {
}
// Returns email value from basic auth or from cookie if the former does not exist
func GetLoginFromReq(req *http.Request) string {
func GetEmailFromReq(req *http.Request) string {
email, _, ok := req.BasicAuth()
if !ok || email == "" {
cookie, err := req.Cookie("auth")
@ -154,14 +154,14 @@ func LocaleFromReq(req *http.Request) string {
}
func LanguageFromReq(req *http.Request) i18n.Language {
switch LocaleFromReq(req) {
switch strings.ToUpper(LocaleFromReq(req)) {
case "ENG":
return i18n.Eng
return i18n.ENG
case "RU":
return i18n.Ru
return i18n.RU
default:
return i18n.Eng
return i18n.ENG
}
}

4
static/images/person-vcard.svg

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-vcard" viewBox="0 0 16 16">
<path d="M5 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4m4-2.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5M9 8a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4A.5.5 0 0 1 9 8m1 2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5"/>
<path d="M2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2zM1 4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H8.96q.04-.245.04-.5C9 10.567 7.21 9 5 9c-2.086 0-3.8 1.398-3.984 3.181A1 1 0 0 1 1 12z"/>
</svg>

After

Width:  |  Height:  |  Size: 581 B

0
translations/eng/about.json → translations/ENG/about.json

0
translations/eng/base.json → translations/ENG/base.json

0
translations/eng/category.json → translations/ENG/category.json

0
translations/eng/error.json → translations/ENG/error.json

0
translations/eng/index.json → translations/ENG/index.json

0
translations/eng/login.json → translations/ENG/login.json

3
translations/ENG/paint.json

@ -0,0 +1,3 @@
{
"language": "ENG"
}

40
translations/ENG/profile.json

@ -0,0 +1,40 @@
{
"language": "ENG",
"messages": [
{
"id": "profile log out",
"message": "Log Out",
"translation": "Log Out"
},
{
"id": "profile created",
"message": "Created",
"translation": "Created"
},
{
"id": "profile delete account",
"message": "Delete Account",
"translation": "Delete Account"
},
{
"id": "profile modal confirm deletion",
"message": "Confirm Deletion",
"translation": "Confirm Deletion"
},
{
"id": "profile modal deletion confirmation",
"message": "Are you sure you want to delete your account?",
"translation": "Are you sure you want to delete your account?"
},
{
"id": "profile modal delete button",
"message": "Delete",
"translation": "Delete"
},
{
"id": "profile modal cancel button",
"message": "Cancel",
"translation": "Cancel"
}
]
}

0
translations/eng/register.json → translations/ENG/register.json

2
translations/ru/about.json → translations/RU/about.json

@ -1,5 +1,5 @@
{
"language": "ENG",
"language": "RU",
"messages": [
{
"id": "about info",

2
translations/ru/base.json → translations/RU/base.json

@ -1,5 +1,5 @@
{
"language": "ENG",
"language": "RU",
"messages": [
{
"id": "base link main",

2
translations/ru/category.json → translations/RU/category.json

@ -1,5 +1,5 @@
{
"language": "ENG",
"language": "RU",
"messages": [
{
"id": "category modal confirm deletion",

2
translations/ru/error.json → translations/RU/error.json

@ -1,5 +1,5 @@
{
"language": "ENG",
"language": "RU",
"messages": [
{
"id": "error error big",

2
translations/ru/index.json → translations/RU/index.json

@ -1,5 +1,5 @@
{
"language": "ENG",
"language": "RU",
"messages": [
{
"id": "index categories",

2
translations/ru/login.json → translations/RU/login.json

@ -1,5 +1,5 @@
{
"language": "ENG",
"language": "RU",
"messages": [
{
"id": "login main",

3
translations/RU/paint.json

@ -0,0 +1,3 @@
{
"language": "RU"
}

40
translations/RU/profile.json

@ -0,0 +1,40 @@
{
"language": "RU",
"messages": [
{
"id": "profile log out",
"message": "Log Out",
"translation": "Выйти"
},
{
"id": "profile delete account",
"message": "Delete Account",
"translation": "Удалить Аккаунт"
},
{
"id": "profile created",
"message": "Created",
"translation": "Создан"
},
{
"id": "profile modal confirm deletion",
"message": "Confirm Deletion",
"translation": "Подтверждение удаления"
},
{
"id": "profile modal deletion confirmation",
"message": "Are you sure you want to delete your account?",
"translation": "Вы действительно хотите удалить свой аккаунт?"
},
{
"id": "profile modal delete button",
"message": "Delete",
"translation": "Удалить"
},
{
"id": "profile modal cancel button",
"message": "Cancel",
"translation": "Отменить"
}
]
}

2
translations/ru/register.json → translations/RU/register.json

@ -1,5 +1,5 @@
{
"language": "ENG",
"language": "RU",
"messages": [
{
"id": "register main",

1
translations/eng/paint.json

@ -1 +0,0 @@
{}

1
translations/ru/paint.json

@ -1 +0,0 @@
{}
Loading…
Cancel
Save