@ -0,0 +1,47 @@ |
|||||||
|
all: clean |
||||||
|
mkdir -p bin && \
|
||||||
|
cd src && CGO_ENABLED=0 go build && mv dela ../bin && \
|
||||||
|
cd .. && \
|
||||||
|
cp -r pages bin && \
|
||||||
|
cp -r scripts bin && \
|
||||||
|
cp -r static bin
|
||||||
|
|
||||||
|
portable: clean all |
||||||
|
cd bin/ && cp ../COPYING . && cp ../README.md . && zip -r dela.zip * && mv dela.zip ..
|
||||||
|
|
||||||
|
cross: clean |
||||||
|
mkdir -p bin
|
||||||
|
mkdir -p bin/dela_linux_x64
|
||||||
|
mkdir -p bin/dela_linux_x32
|
||||||
|
mkdir -p bin/dela_windows_x64
|
||||||
|
mkdir -p bin/dela_windows_arm64
|
||||||
|
mkdir -p bin/dela_darwin_x64
|
||||||
|
mkdir -p bin/dela_darwin_arm64
|
||||||
|
mkdir -p bin/dela_freebsd_x64
|
||||||
|
mkdir -p bin/dela_freebsd_arm64
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cd src && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build && mv dela ../bin/dela_linux_x64
|
||||||
|
cd src && CGO_ENABLED=0 GOOS=linux GOARCH=386 go build && mv dela ../bin/dela_linux_x32
|
||||||
|
cd src && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build && mv dela.exe ../bin/dela_windows_x64
|
||||||
|
cd src && CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build && mv dela.exe ../bin/dela_windows_arm64
|
||||||
|
cd src && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build && mv dela ../bin/dela_darwin_x64
|
||||||
|
cd src && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build && mv dela ../bin/dela_darwin_arm64
|
||||||
|
cd src && CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build && mv dela ../bin/dela_freebsd_x64
|
||||||
|
cd src && CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 go build && mv dela ../bin/dela_freebsd_arm64
|
||||||
|
|
||||||
|
|
||||||
|
mkdir -p bin/includes
|
||||||
|
|
||||||
|
cp -r pages bin/includes
|
||||||
|
cp -r scripts bin/includes
|
||||||
|
cp -r static bin/includes
|
||||||
|
cp COPYING bin/includes
|
||||||
|
cp LICENSE* bin/includes
|
||||||
|
cp README.md bin/includes
|
||||||
|
|
||||||
|
|
||||||
|
clean: |
||||||
|
rm -rf bin
|
||||||
|
|
@ -0,0 +1,27 @@ |
|||||||
|
# Dela dead simple web TODO list |
||||||
|
|
||||||
|
## About |
||||||
|
|
||||||
|
Dela is a web TODO list application which can be hosted on your server and accessed via browser. Current capabilities include: |
||||||
|
|
||||||
|
- Password protected account system |
||||||
|
- TODO creation |
||||||
|
- TODO completion |
||||||
|
|
||||||
|
## Build |
||||||
|
|
||||||
|
``` |
||||||
|
make |
||||||
|
``` |
||||||
|
|
||||||
|
or manually compile with `go build` inside the `src` directory, move the binary and put it alongside `pages`, `scripts` and `static` (although the path to the contents can be specified via configuration file). |
||||||
|
|
||||||
|
``` |
||||||
|
make portable |
||||||
|
``` |
||||||
|
|
||||||
|
will create a `dela.zip` archive with all the content directories and the binary which are to be freely transported to other machines (of the same OS and architecture, obviously) |
||||||
|
|
||||||
|
## Use |
||||||
|
|
||||||
|
After the first run a configuration file will be put alongside the executable, upon the second launch server will start and the service will be accessible on the specified port. |
@ -0,0 +1,9 @@ |
|||||||
|
1) Профиль |
||||||
|
2) Теги заметок/Каталоги заметок |
||||||
|
3) Темная/Светлая тема |
||||||
|
4) Drag and Drop |
||||||
|
5) Подтверждение почты |
||||||
|
|
||||||
|
Не SQLite3 |
||||||
|
|
||||||
|
Figma |
@ -0,0 +1,15 @@ |
|||||||
|
{{ template "base" . }} |
||||||
|
|
||||||
|
{{ define "content" }} |
||||||
|
<main class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column"> |
||||||
|
<div class="p-2 flex-fill text-wrap text-center"> |
||||||
|
<h1>Dela.</h1> |
||||||
|
<p class="lead">Simple and elegant 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> |
||||||
|
</div> |
||||||
|
</main> |
||||||
|
|
||||||
|
{{ end }} |
@ -0,0 +1,92 @@ |
|||||||
|
{{ define "base" }} |
||||||
|
|
||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
|
||||||
|
<head> |
||||||
|
<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> |
||||||
|
</head> |
||||||
|
|
||||||
|
<!-- <body class="d-flex flex-column h-100" style="width: 100%;"> --> |
||||||
|
<body class="w-100 h-100"> |
||||||
|
<!-- Header --> |
||||||
|
<!-- <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="64" height="64" src="/static/images/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"><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> --> |
||||||
|
<header class="p-3 text-bg-primary"> |
||||||
|
<div class="container"> |
||||||
|
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start"> |
||||||
|
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none"> |
||||||
|
<a href="/" class="d-inline-flex link-body-emphasis text-decoration-none"> |
||||||
|
<img width="64" height="64" src="/static/images/android-chrome-192x192.png" alt="Dela"> |
||||||
|
</a> |
||||||
|
</a> |
||||||
|
|
||||||
|
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0"> |
||||||
|
<li><a href="/register" class="nav-link px-2 text-white">Register</a></li> |
||||||
|
<li><a href="/about" class="nav-link px-2 text-white">About</a></li> |
||||||
|
</ul> |
||||||
|
|
||||||
|
<div class="text-end" id="bar-auth"> |
||||||
|
<a href="/login" class="btn btn-outline-light me-2">Login</a> |
||||||
|
<a href="/register" class="btn btn-warning">Sign-up</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</header> |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Content --> |
||||||
|
{{ template "content" . }} |
||||||
|
|
||||||
|
</body> |
||||||
|
</html> |
||||||
|
|
||||||
|
<script src="/scripts/auth.js"></script> |
||||||
|
<script src="/scripts/api.js"></script> |
||||||
|
<script> |
||||||
|
document.addEventListener('DOMContentLoaded', async function() { |
||||||
|
let login = getLogin(); |
||||||
|
let password = getUserPassword(); |
||||||
|
|
||||||
|
if (login == null | login == "" | password == null | password == "") { |
||||||
|
if (window.location.pathname != "/about" && window.location.pathname != "/login" && window.location.pathname != "/register") { |
||||||
|
window.location.replace("/about"); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Check if auth info is indeed valid |
||||||
|
let response = await getUser(login, password); |
||||||
|
if (response.ok) { |
||||||
|
let barAuth = document.getElementById("bar-auth"); |
||||||
|
barAuth.innerHTML = "<b>" + login + "</b>" + " | "; |
||||||
|
barAuth.innerHTML += '<button id="log-out-btn" class="btn btn-outline-light me-2"><img src="/static/images/person-dash-fill.svg"></button>'; |
||||||
|
document.getElementById("log-out-btn").addEventListener("click", (event) => { |
||||||
|
// Log out |
||||||
|
forgetAuthInfo(); |
||||||
|
window.location.replace("/about"); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
forgetAuthInfo(); |
||||||
|
window.location.replace("/about"); |
||||||
|
} |
||||||
|
}, false) |
||||||
|
</script> |
||||||
|
|
||||||
|
{{ end }} |
@ -0,0 +1,270 @@ |
|||||||
|
{{ template "base" . }} |
||||||
|
|
||||||
|
{{ define "content" }} |
||||||
|
|
||||||
|
<!-- Main --> |
||||||
|
<main class="d-flex flex-wrap"> |
||||||
|
|
||||||
|
<!-- Sidebar --> |
||||||
|
<div id="sidebar" class="flex-shrink-1 p-2 d-flex flex-column align-items-stretch bg-body-tertiary" style="width: 380px;"> |
||||||
|
<a href="/" class="d-flex align-items-center flex-shrink-0 p-3 link-body-emphasis text-decoration-none border-bottom"> |
||||||
|
<svg class="bi pe-none me-2" width="30" height="24"><use xlink:href="#bootstrap"></use></svg> |
||||||
|
<span class="fs-5 fw-semibold">List group</span> |
||||||
|
</a> |
||||||
|
<div class="list-group list-group-flush border-bottom scrollarea"> |
||||||
|
<a href="#" class="list-group-item list-group-item-action active py-3 lh-sm" aria-current="true"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small>Wed</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Tues</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Mon</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
|
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm" aria-current="true"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Wed</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Tues</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Mon</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm" aria-current="true"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Wed</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Tues</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Mon</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm" aria-current="true"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Wed</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Tues</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Mon</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
|
||||||
|
<div class="p-2 flex-grow-1"> |
||||||
|
<form action="javascript:void(0);"> |
||||||
|
<div class="container"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col"> |
||||||
|
<input type="text" class="form-control" id="new-todo-text" placeholder="TODO text"> |
||||||
|
</div> |
||||||
|
<div class="col"> |
||||||
|
<button id="new-todo-submit" class="btn btn-primary">Add</button> |
||||||
|
<button class="btn btn-secondary" id="show-done">Show Done</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
|
||||||
|
<div class="container text-center"> |
||||||
|
<table id="todos" class="table table-hover" style="word-wrap: break-word;"> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
</main> |
||||||
|
|
||||||
|
<script> |
||||||
|
async function displayTodos(showDone) { |
||||||
|
let username = getUsername(); |
||||||
|
let password = getUserPassword(); |
||||||
|
|
||||||
|
// Fetch and display TODOs |
||||||
|
let response = await getTodos(username, password); |
||||||
|
let todosJson = await response.json(); |
||||||
|
let todosDisplayed = []; |
||||||
|
|
||||||
|
if (response.ok && todosJson != null) { |
||||||
|
let todosDiv = document.getElementById("todos"); |
||||||
|
// Clear what we've had before |
||||||
|
todosDiv.innerHTML = ""; |
||||||
|
|
||||||
|
todosJson.forEach((item) => { |
||||||
|
if (showDone === true && item.isDone == true) { |
||||||
|
// An already done Todo |
||||||
|
|
||||||
|
let todoDeleteBtnID = "btn-delete-" + String(item.id); |
||||||
|
|
||||||
|
// Display |
||||||
|
let timeCreated = new Date(item.timeCreatedUnix * 1000); |
||||||
|
let timeDone = new Date(item.completionTimeUnix * 1000); |
||||||
|
todosDiv.innerHTML += "<tr><td>" + item.text + "</td>" + |
||||||
|
"<td>" + " " + timeCreated.getDate() + "/" + (timeCreated.getMonth() + 1) + "/" + timeCreated.getFullYear() + " | " + |
||||||
|
timeDone.getDate() + "/" + (timeDone.getMonth() + 1) + "/" + timeDone.getFullYear() + "</td>" + |
||||||
|
"<td>" + "<button class='btn btn-danger' id='" + |
||||||
|
todoDeleteBtnID + "'><img src='/static/images/trash3-fill.svg'></button></td></tr>"; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
todosDisplayed.push({item: item, buttonDel: todoDeleteBtnID}); |
||||||
|
|
||||||
|
|
||||||
|
} else if (showDone === false && item.isDone == false) { |
||||||
|
// A yet to be done Todo |
||||||
|
|
||||||
|
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() + 1) + "/" + timeCreated.getFullYear() + "</td>" + |
||||||
|
"<td><button class='btn btn-success' id='" + todoCompleteBtnID + "'>" + |
||||||
|
"<img src='/static/images/check.svg'></button><button class='btn btn-danger' id='" + |
||||||
|
todoDeleteBtnID + "'><img src='/static/images/trash3-fill.svg'></button></td></tr>"; |
||||||
|
|
||||||
|
todosDisplayed.push({item: item, buttonDel: todoDeleteBtnID, buttonComplete: todoCompleteBtnID}); |
||||||
|
|
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// Loop over all buttons (doesn't matter which ones because the amounts are equal) |
||||||
|
for (let i = 0; i < todosDisplayed.length; i++) { |
||||||
|
let elem = todosDisplayed[i]; |
||||||
|
|
||||||
|
if (showDone === false && elem.item.isDone === false) { |
||||||
|
// Done button |
||||||
|
document.getElementById(elem.buttonComplete).addEventListener("click", async (event) => { |
||||||
|
// Mark as done |
||||||
|
elem.item.isDone = true; |
||||||
|
// Set completion time |
||||||
|
elem.item.completionTimeUnix = Math.floor(Date.now() / 1000); |
||||||
|
|
||||||
|
// Update |
||||||
|
response = await updateTodo(username, password, elem.item.id, elem.item); |
||||||
|
if (response.ok) { |
||||||
|
location.reload(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Delete button |
||||||
|
document.getElementById(elem.buttonDel).addEventListener("click", async (event) => { |
||||||
|
response = await deleteTodo(username, password, elem.item.id); |
||||||
|
if (response.ok) { |
||||||
|
location.reload(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} else { |
||||||
|
// Delete button |
||||||
|
document.getElementById(elem.buttonDel).addEventListener("click", async (event) => { |
||||||
|
response = await deleteTodo(username, password, elem.item.id); |
||||||
|
if (response.ok) { |
||||||
|
location.reload(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async function() { |
||||||
|
let username = getUsername(); |
||||||
|
let password = getUserPassword(); |
||||||
|
|
||||||
|
document.getElementById("new-todo-text").focus(); |
||||||
|
|
||||||
|
|
||||||
|
let showDoneButton = document.getElementById("show-done"); |
||||||
|
showDoneButton.addEventListener("click", (event) => { |
||||||
|
displayTodos(true); // Re-display without reloading |
||||||
|
|
||||||
|
// Rename the button |
||||||
|
showDoneButton.innerText = "Show To Do"; |
||||||
|
showDoneButton.className = "btn btn-success"; |
||||||
|
|
||||||
|
// Make it "reset to default" |
||||||
|
showDoneButton.addEventListener("click", (event) => { |
||||||
|
location.reload(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
|
||||||
|
// "Add" button |
||||||
|
document.getElementById("new-todo-submit").addEventListener("click", async (event) => { |
||||||
|
let newTodoTextInput = document.getElementById("new-todo-text"); |
||||||
|
let newTodoText = newTodoTextInput.value; |
||||||
|
if (newTodoText.length < 1) { |
||||||
|
newTodoTextInput.setCustomValidity("At least one character is needed!"); |
||||||
|
return; |
||||||
|
} else { |
||||||
|
newTodoTextInput.setCustomValidity(""); |
||||||
|
} |
||||||
|
newTodoTextInput.value = ""; |
||||||
|
|
||||||
|
// Make a request |
||||||
|
let response = await postNewTodo(username, password, {text: newTodoText}); |
||||||
|
if (response.ok) { |
||||||
|
location.reload(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
|
||||||
|
// Fetch and display TODOs |
||||||
|
await displayTodos(false); |
||||||
|
}, false) |
||||||
|
</script> |
||||||
|
|
||||||
|
{{ end }} |
@ -0,0 +1,58 @@ |
|||||||
|
{{ template "base" . }} |
||||||
|
|
||||||
|
{{ define "content" }} |
||||||
|
|
||||||
|
|
||||||
|
<main class="d-flex flex-wrap align-content-center align-items-center"> |
||||||
|
<div class="p-2 flex-fill text-wrap text-center"> |
||||||
|
<!-- text-center --> |
||||||
|
<h3>Log in</h3> |
||||||
|
<form name="loginForm" onsubmit="return false;"> |
||||||
|
<p> |
||||||
|
<label for="login" class="form-label">Login</label> <br> |
||||||
|
<input type="text" name="login" minlength="3" placeholder="Login" required> |
||||||
|
</p> |
||||||
|
|
||||||
|
<p> |
||||||
|
<label for="password" class="form-label">Password</label> <br> |
||||||
|
<input type="password" name="password" minlength="3" placeholder="Password" required> |
||||||
|
</p> |
||||||
|
|
||||||
|
<p><span id="error_message" class="text-danger"></span></p> |
||||||
|
|
||||||
|
<p> |
||||||
|
<input type="submit" value="Log in" class="btn btn-primary" onclick="logIn()"> |
||||||
|
</p> |
||||||
|
</form> |
||||||
|
|
||||||
|
</div> |
||||||
|
</main> |
||||||
|
|
||||||
|
<script> |
||||||
|
async function logIn() { |
||||||
|
let loginForm = document.forms["loginForm"]; |
||||||
|
|
||||||
|
let login = String(loginForm.elements["login"].value).trim(); |
||||||
|
if (login.length < 3) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let password = String(loginForm.elements["password"].value); |
||||||
|
if (password.length < 3) { |
||||||
|
return; |
||||||
|
} |
||||||
|
password = sha256(password); |
||||||
|
|
||||||
|
// Check if auth info is indeed valid |
||||||
|
let response = await getUser(login, password); |
||||||
|
|
||||||
|
if (response.ok) { |
||||||
|
rememberAuthInfo(login, password); |
||||||
|
window.location.replace("/"); |
||||||
|
} else { |
||||||
|
document.getElementById("error_message").innerText = await response.text(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
{{ end }} |
@ -0,0 +1,69 @@ |
|||||||
|
{{ template "base" . }} |
||||||
|
|
||||||
|
{{ define "content" }} |
||||||
|
|
||||||
|
<main class="d-flex flex-wrap align-content-center"> |
||||||
|
<div class="p-2 flex-fill text-wrap text-center"> |
||||||
|
<h3>Register</h3> |
||||||
|
<form name="registerForm" onsubmit="return false;"> |
||||||
|
<p> |
||||||
|
<label for="login" class="form-label">Login</label> <br> |
||||||
|
<input type="text" name="login" minlength="3" placeholder="Login" required> |
||||||
|
</p> |
||||||
|
|
||||||
|
<p> |
||||||
|
<label for="email" class="form-label">Email</label> <br> |
||||||
|
<input type="email" name="email" minlength="3" placeholder="login@example.com" required> |
||||||
|
</p> |
||||||
|
|
||||||
|
<p> |
||||||
|
<label for="password" class="form-label">Password</label> <br> |
||||||
|
<input type="password" name="password" minlength="3" placeholder="Password" required> |
||||||
|
</p> |
||||||
|
|
||||||
|
<p><span id="error_message" class="text-danger"></span></p> |
||||||
|
|
||||||
|
<p> |
||||||
|
<input type="submit" value="Register" class="btn btn-primary" onclick="register();"> |
||||||
|
</p> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</main> |
||||||
|
|
||||||
|
<script> |
||||||
|
async function register() { |
||||||
|
let registerForm = document.forms["registerForm"]; |
||||||
|
|
||||||
|
let login = String(registerForm.elements["login"].value).trim(); |
||||||
|
if (login.length < 3) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let email = String(registerForm.elements["email"].value).trim(); |
||||||
|
if (email.length < 3) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let password = String(registerForm.elements["password"].value); |
||||||
|
if (password.length < 3) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let passwordSHA256 = sha256(password); |
||||||
|
let postData = { |
||||||
|
login: login, |
||||||
|
email: email, |
||||||
|
password: passwordSHA256, |
||||||
|
}; |
||||||
|
|
||||||
|
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(); |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
{{ end }} |
@ -0,0 +1,270 @@ |
|||||||
|
{{ template "base" . }} |
||||||
|
|
||||||
|
{{ define "content" }} |
||||||
|
|
||||||
|
<!-- Sidebar --> |
||||||
|
<div id="sidebar" class="flex-shrink-1 p-2 d-flex flex-column align-items-stretch bg-body-tertiary" style="width: 380px;"> |
||||||
|
<a href="/" class="d-flex align-items-center flex-shrink-0 p-3 link-body-emphasis text-decoration-none border-bottom"> |
||||||
|
<svg class="bi pe-none me-2" width="30" height="24"><use xlink:href="#bootstrap"></use></svg> |
||||||
|
<span class="fs-5 fw-semibold">List group</span> |
||||||
|
</a> |
||||||
|
<div class="list-group list-group-flush border-bottom scrollarea"> |
||||||
|
<a href="#" class="list-group-item list-group-item-action active py-3 lh-sm" aria-current="true"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small>Wed</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Tues</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Mon</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
|
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm" aria-current="true"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Wed</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Tues</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Mon</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm" aria-current="true"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Wed</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Tues</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Mon</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm" aria-current="true"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Wed</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Tues</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
<a href="#" class="list-group-item list-group-item-action py-3 lh-sm"> |
||||||
|
<div class="d-flex w-100 align-items-center justify-content-between"> |
||||||
|
<strong class="mb-1">List group item heading</strong> |
||||||
|
<small class="text-body-secondary">Mon</small> |
||||||
|
</div> |
||||||
|
<div class="col-10 mb-1 small">Some placeholder content in a paragraph below the heading and date.</div> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
|
||||||
|
<form action="javascript:void(0);"> |
||||||
|
<div class="container text-center"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col"> |
||||||
|
<input type="text" class="form-control" id="new-todo-text" placeholder="TODO text"> |
||||||
|
</div> |
||||||
|
<div class="col"> |
||||||
|
<button id="new-todo-submit" class="btn btn-primary">Add</button> |
||||||
|
<button class="btn btn-secondary" id="show-done">Show Done</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
|
||||||
|
|
||||||
|
<div class="container text-center" style="margin-top: 4ch;"> |
||||||
|
<table id="todos" class="table table-hover" style="word-wrap: break-word;"> |
||||||
|
{{ for .Todos }} |
||||||
|
<tr> |
||||||
|
{{ .Name }} |
||||||
|
</tr> |
||||||
|
{{ end }} |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
|
||||||
|
|
||||||
|
<script> |
||||||
|
async function displayTodos(showDone) { |
||||||
|
let username = getUsername(); |
||||||
|
let password = getUserPassword(); |
||||||
|
|
||||||
|
// Fetch and display TODOs |
||||||
|
let response = await getTodos(username, password); |
||||||
|
let todosJson = await response.json(); |
||||||
|
let todosDisplayed = []; |
||||||
|
|
||||||
|
if (response.ok && todosJson != null) { |
||||||
|
let todosDiv = document.getElementById("todos"); |
||||||
|
// Clear what we've had before |
||||||
|
todosDiv.innerHTML = ""; |
||||||
|
|
||||||
|
todosJson.forEach((item) => { |
||||||
|
if (showDone === true && item.isDone == true) { |
||||||
|
// An already done Todo |
||||||
|
|
||||||
|
let todoDeleteBtnID = "btn-delete-" + String(item.id); |
||||||
|
|
||||||
|
// Display |
||||||
|
let timeCreated = new Date(item.timeCreatedUnix * 1000); |
||||||
|
let timeDone = new Date(item.completionTimeUnix * 1000); |
||||||
|
todosDiv.innerHTML += "<tr><td>" + item.text + "</td>" + |
||||||
|
"<td>" + " " + timeCreated.getDate() + "/" + (timeCreated.getMonth() + 1) + "/" + timeCreated.getFullYear() + " | " + |
||||||
|
timeDone.getDate() + "/" + (timeDone.getMonth() + 1) + "/" + timeDone.getFullYear() + "</td>" + |
||||||
|
"<td>" + "<button class='btn btn-danger' id='" + |
||||||
|
todoDeleteBtnID + "'><img src='/static/images/trash3-fill.svg'></button></td></tr>"; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
todosDisplayed.push({item: item, buttonDel: todoDeleteBtnID}); |
||||||
|
|
||||||
|
|
||||||
|
} else if (showDone === false && item.isDone == false) { |
||||||
|
// A yet to be done Todo |
||||||
|
|
||||||
|
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() + 1) + "/" + timeCreated.getFullYear() + "</td>" + |
||||||
|
"<td><button class='btn btn-success' id='" + todoCompleteBtnID + "'>" + |
||||||
|
"<img src='/static/images/check.svg'></button><button class='btn btn-danger' id='" + |
||||||
|
todoDeleteBtnID + "'><img src='/static/images/trash3-fill.svg'></button></td></tr>"; |
||||||
|
|
||||||
|
todosDisplayed.push({item: item, buttonDel: todoDeleteBtnID, buttonComplete: todoCompleteBtnID}); |
||||||
|
|
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// Loop over all buttons (doesn't matter which ones because the amounts are equal) |
||||||
|
for (let i = 0; i < todosDisplayed.length; i++) { |
||||||
|
let elem = todosDisplayed[i]; |
||||||
|
|
||||||
|
if (showDone === false && elem.item.isDone === false) { |
||||||
|
// Done button |
||||||
|
document.getElementById(elem.buttonComplete).addEventListener("click", async (event) => { |
||||||
|
// Mark as done |
||||||
|
elem.item.isDone = true; |
||||||
|
// Set completion time |
||||||
|
elem.item.completionTimeUnix = Math.floor(Date.now() / 1000); |
||||||
|
|
||||||
|
// Update |
||||||
|
response = await updateTodo(username, password, elem.item.id, elem.item); |
||||||
|
if (response.ok) { |
||||||
|
location.reload(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Delete button |
||||||
|
document.getElementById(elem.buttonDel).addEventListener("click", async (event) => { |
||||||
|
response = await deleteTodo(username, password, elem.item.id); |
||||||
|
if (response.ok) { |
||||||
|
location.reload(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} else { |
||||||
|
// Delete button |
||||||
|
document.getElementById(elem.buttonDel).addEventListener("click", async (event) => { |
||||||
|
response = await deleteTodo(username, password, elem.item.id); |
||||||
|
if (response.ok) { |
||||||
|
location.reload(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async function() { |
||||||
|
let username = getUsername(); |
||||||
|
let password = getUserPassword(); |
||||||
|
|
||||||
|
document.getElementById("new-todo-text").focus(); |
||||||
|
|
||||||
|
|
||||||
|
let showDoneButton = document.getElementById("show-done"); |
||||||
|
showDoneButton.addEventListener("click", (event) => { |
||||||
|
displayTodos(true); // Re-display without reloading |
||||||
|
|
||||||
|
// Rename the button |
||||||
|
showDoneButton.innerText = "Show To Do"; |
||||||
|
showDoneButton.className = "btn btn-success"; |
||||||
|
|
||||||
|
// Make it "reset to default" |
||||||
|
showDoneButton.addEventListener("click", (event) => { |
||||||
|
location.reload(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
|
||||||
|
// "Add" button |
||||||
|
document.getElementById("new-todo-submit").addEventListener("click", async (event) => { |
||||||
|
let newTodoTextInput = document.getElementById("new-todo-text"); |
||||||
|
let newTodoText = newTodoTextInput.value; |
||||||
|
if (newTodoText.length < 1) { |
||||||
|
newTodoTextInput.setCustomValidity("At least one character is needed!"); |
||||||
|
return; |
||||||
|
} else { |
||||||
|
newTodoTextInput.setCustomValidity(""); |
||||||
|
} |
||||||
|
newTodoTextInput.value = ""; |
||||||
|
|
||||||
|
// Make a request |
||||||
|
let response = await postNewTodo(username, password, {text: newTodoText}); |
||||||
|
if (response.ok) { |
||||||
|
location.reload(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
|
||||||
|
// Fetch and display TODOs |
||||||
|
await displayTodos(false); |
||||||
|
}, false) |
||||||
|
</script> |
||||||
|
|
||||||
|
{{ end }} |
@ -0,0 +1,83 @@ |
|||||||
|
/* |
||||||
|
2023 Kasyanov Nikolay Alexeyevich (Unbewohnte) |
||||||
|
*/ |
||||||
|
|
||||||
|
|
||||||
|
async function post(url, login, password, json) { |
||||||
|
return fetch(url, { |
||||||
|
method: "POST", |
||||||
|
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 postNewGroup(login, password, newGroup) { |
||||||
|
return post("/api/group/create", login, password, newGroup) |
||||||
|
} |
||||||
|
|
||||||
|
async function postNewUser(newUser) { |
||||||
|
return post("/api/user/create", "", "", newUser) |
||||||
|
} |
||||||
|
|
||||||
|
async function get(url, login, password) { |
||||||
|
return fetch(url, { |
||||||
|
method: "GET", |
||||||
|
headers: { |
||||||
|
"Authorization": "Basic " + btoa(login + ":" + password), |
||||||
|
"Content-Type": "application/json", |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
async function getUser(login, password) { |
||||||
|
return get("/api/user/get", login, password); |
||||||
|
} |
||||||
|
|
||||||
|
async function getTodos(login, password) { |
||||||
|
return get("/api/todo/get", login, password); |
||||||
|
} |
||||||
|
|
||||||
|
async function getGroup(login, password) { |
||||||
|
return get("/api/group/get", login, password); |
||||||
|
} |
||||||
|
|
||||||
|
async function getAllGroups(login, password) { |
||||||
|
return get("/api/user/get", login, password); |
||||||
|
} |
||||||
|
|
||||||
|
async function del(url, login, password) { |
||||||
|
return fetch(url, { |
||||||
|
method: "DELETE", |
||||||
|
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 update(url, login, password, json) { |
||||||
|
return post(url, login, password, json); |
||||||
|
} |
||||||
|
|
||||||
|
async function updateTodo(login, password, id, updatedTodo) { |
||||||
|
return update("/api/todo/update/"+id, login, password, updatedTodo); |
||||||
|
} |
||||||
|
|
||||||
|
async function updateGroup(login, password, id, updatedGroup) { |
||||||
|
return update("/api/group/update/"+id, login, password, updateGroup); |
||||||
|
} |
||||||
|
|
||||||
|
async function updateUser(login, password, updatedUser) { |
||||||
|
return update("/api/group/update/"+login, login, password, updatedUser); |
||||||
|
} |
@ -0,0 +1,545 @@ |
|||||||
|
/* |
||||||
|
2023 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}
|
||||||
|
* |
||||||
|
* @version 0.9.0 |
||||||
|
* @author Chen, Yi-Cyuan [emn178@gmail.com] |
||||||
|
* @copyright Chen, Yi-Cyuan 2014-2017 |
||||||
|
* @license MIT |
||||||
|
*/ |
||||||
|
/*jslint bitwise: true */ |
||||||
|
(function () { |
||||||
|
'use strict'; |
||||||
|
|
||||||
|
var ERROR = 'input is invalid type'; |
||||||
|
var WINDOW = typeof window === 'object'; |
||||||
|
var root = WINDOW ? window : {}; |
||||||
|
if (root.JS_SHA256_NO_WINDOW) { |
||||||
|
WINDOW = false; |
||||||
|
} |
||||||
|
var WEB_WORKER = !WINDOW && typeof self === 'object'; |
||||||
|
var NODE_JS = !root.JS_SHA256_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node; |
||||||
|
if (NODE_JS) { |
||||||
|
root = global; |
||||||
|
} else if (WEB_WORKER) { |
||||||
|
root = self; |
||||||
|
} |
||||||
|
var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && typeof module === 'object' && module.exports; |
||||||
|
var AMD = typeof define === 'function' && define.amd; |
||||||
|
var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined'; |
||||||
|
var HEX_CHARS = '0123456789abcdef'.split(''); |
||||||
|
var EXTRA = [-2147483648, 8388608, 32768, 128]; |
||||||
|
var SHIFT = [24, 16, 8, 0]; |
||||||
|
var K = [ |
||||||
|
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, |
||||||
|
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, |
||||||
|
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, |
||||||
|
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, |
||||||
|
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, |
||||||
|
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, |
||||||
|
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, |
||||||
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 |
||||||
|
]; |
||||||
|
var OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer']; |
||||||
|
|
||||||
|
var blocks = []; |
||||||
|
|
||||||
|
if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) { |
||||||
|
Array.isArray = function (obj) { |
||||||
|
return Object.prototype.toString.call(obj) === '[object Array]'; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) { |
||||||
|
ArrayBuffer.isView = function (obj) { |
||||||
|
return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
var createOutputMethod = function (outputType, is224) { |
||||||
|
return function (message) { |
||||||
|
return new Sha256(is224, true).update(message)[outputType](); |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
var createMethod = function (is224) { |
||||||
|
var method = createOutputMethod('hex', is224); |
||||||
|
if (NODE_JS) { |
||||||
|
method = nodeWrap(method, is224); |
||||||
|
} |
||||||
|
method.create = function () { |
||||||
|
return new Sha256(is224); |
||||||
|
}; |
||||||
|
method.update = function (message) { |
||||||
|
return method.create().update(message); |
||||||
|
}; |
||||||
|
for (var i = 0; i < OUTPUT_TYPES.length; ++i) { |
||||||
|
var type = OUTPUT_TYPES[i]; |
||||||
|
method[type] = createOutputMethod(type, is224); |
||||||
|
} |
||||||
|
return method; |
||||||
|
}; |
||||||
|
|
||||||
|
var nodeWrap = function (method, is224) { |
||||||
|
var crypto = eval("require('crypto')"); |
||||||
|
var Buffer = eval("require('buffer').Buffer"); |
||||||
|
var algorithm = is224 ? 'sha224' : 'sha256'; |
||||||
|
var nodeMethod = function (message) { |
||||||
|
if (typeof message === 'string') { |
||||||
|
return crypto.createHash(algorithm).update(message, 'utf8').digest('hex'); |
||||||
|
} else { |
||||||
|
if (message === null || message === undefined) { |
||||||
|
throw new Error(ERROR); |
||||||
|
} else if (message.constructor === ArrayBuffer) { |
||||||
|
message = new Uint8Array(message); |
||||||
|
} |
||||||
|
} |
||||||
|
if (Array.isArray(message) || ArrayBuffer.isView(message) || |
||||||
|
message.constructor === Buffer) { |
||||||
|
return crypto.createHash(algorithm).update(new Buffer(message)).digest('hex'); |
||||||
|
} else { |
||||||
|
return method(message); |
||||||
|
} |
||||||
|
}; |
||||||
|
return nodeMethod; |
||||||
|
}; |
||||||
|
|
||||||
|
var createHmacOutputMethod = function (outputType, is224) { |
||||||
|
return function (key, message) { |
||||||
|
return new HmacSha256(key, is224, true).update(message)[outputType](); |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
var createHmacMethod = function (is224) { |
||||||
|
var method = createHmacOutputMethod('hex', is224); |
||||||
|
method.create = function (key) { |
||||||
|
return new HmacSha256(key, is224); |
||||||
|
}; |
||||||
|
method.update = function (key, message) { |
||||||
|
return method.create(key).update(message); |
||||||
|
}; |
||||||
|
for (var i = 0; i < OUTPUT_TYPES.length; ++i) { |
||||||
|
var type = OUTPUT_TYPES[i]; |
||||||
|
method[type] = createHmacOutputMethod(type, is224); |
||||||
|
} |
||||||
|
return method; |
||||||
|
}; |
||||||
|
|
||||||
|
function Sha256(is224, sharedMemory) { |
||||||
|
if (sharedMemory) { |
||||||
|
blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = |
||||||
|
blocks[4] = blocks[5] = blocks[6] = blocks[7] = |
||||||
|
blocks[8] = blocks[9] = blocks[10] = blocks[11] = |
||||||
|
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; |
||||||
|
this.blocks = blocks; |
||||||
|
} else { |
||||||
|
this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; |
||||||
|
} |
||||||
|
|
||||||
|
if (is224) { |
||||||
|
this.h0 = 0xc1059ed8; |
||||||
|
this.h1 = 0x367cd507; |
||||||
|
this.h2 = 0x3070dd17; |
||||||
|
this.h3 = 0xf70e5939; |
||||||
|
this.h4 = 0xffc00b31; |
||||||
|
this.h5 = 0x68581511; |
||||||
|
this.h6 = 0x64f98fa7; |
||||||
|
this.h7 = 0xbefa4fa4; |
||||||
|
} else { // 256
|
||||||
|
this.h0 = 0x6a09e667; |
||||||
|
this.h1 = 0xbb67ae85; |
||||||
|
this.h2 = 0x3c6ef372; |
||||||
|
this.h3 = 0xa54ff53a; |
||||||
|
this.h4 = 0x510e527f; |
||||||
|
this.h5 = 0x9b05688c; |
||||||
|
this.h6 = 0x1f83d9ab; |
||||||
|
this.h7 = 0x5be0cd19; |
||||||
|
} |
||||||
|
|
||||||
|
this.block = this.start = this.bytes = this.hBytes = 0; |
||||||
|
this.finalized = this.hashed = false; |
||||||
|
this.first = true; |
||||||
|
this.is224 = is224; |
||||||
|
} |
||||||
|
|
||||||
|
Sha256.prototype.update = function (message) { |
||||||
|
if (this.finalized) { |
||||||
|
return; |
||||||
|
} |
||||||
|
var notString, type = typeof message; |
||||||
|
if (type !== 'string') { |
||||||
|
if (type === 'object') { |
||||||
|
if (message === null) { |
||||||
|
throw new Error(ERROR); |
||||||
|
} else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) { |
||||||
|
message = new Uint8Array(message); |
||||||
|
} else if (!Array.isArray(message)) { |
||||||
|
if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) { |
||||||
|
throw new Error(ERROR); |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
throw new Error(ERROR); |
||||||
|
} |
||||||
|
notString = true; |
||||||
|
} |
||||||
|
var code, index = 0, i, length = message.length, blocks = this.blocks; |
||||||
|
|
||||||
|
while (index < length) { |
||||||
|
if (this.hashed) { |
||||||
|
this.hashed = false; |
||||||
|
blocks[0] = this.block; |
||||||
|
blocks[16] = blocks[1] = blocks[2] = blocks[3] = |
||||||
|
blocks[4] = blocks[5] = blocks[6] = blocks[7] = |
||||||
|
blocks[8] = blocks[9] = blocks[10] = blocks[11] = |
||||||
|
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; |
||||||
|
} |
||||||
|
|
||||||
|
if (notString) { |
||||||
|
for (i = this.start; index < length && i < 64; ++index) { |
||||||
|
blocks[i >> 2] |= message[index] << SHIFT[i++ & 3]; |
||||||
|
} |
||||||
|
} else { |
||||||
|
for (i = this.start; index < length && i < 64; ++index) { |
||||||
|
code = message.charCodeAt(index); |
||||||
|
if (code < 0x80) { |
||||||
|
blocks[i >> 2] |= code << SHIFT[i++ & 3]; |
||||||
|
} else if (code < 0x800) { |
||||||
|
blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; |
||||||
|
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; |
||||||
|
} else if (code < 0xd800 || code >= 0xe000) { |
||||||
|
blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; |
||||||
|
blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; |
||||||
|
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; |
||||||
|
} else { |
||||||
|
code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); |
||||||
|
blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; |
||||||
|
blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; |
||||||
|
blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; |
||||||
|
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this.lastByteIndex = i; |
||||||
|
this.bytes += i - this.start; |
||||||
|
if (i >= 64) { |
||||||
|
this.block = blocks[16]; |
||||||
|
this.start = i - 64; |
||||||
|
this.hash(); |
||||||
|
this.hashed = true; |
||||||
|
} else { |
||||||
|
this.start = i; |
||||||
|
} |
||||||
|
} |
||||||
|
if (this.bytes > 4294967295) { |
||||||
|
this.hBytes += this.bytes / 4294967296 << 0; |
||||||
|
this.bytes = this.bytes % 4294967296; |
||||||
|
} |
||||||
|
return this; |
||||||
|
}; |
||||||
|
|
||||||
|
Sha256.prototype.finalize = function () { |
||||||
|
if (this.finalized) { |
||||||
|
return; |
||||||
|
} |
||||||
|
this.finalized = true; |
||||||
|
var blocks = this.blocks, i = this.lastByteIndex; |
||||||
|
blocks[16] = this.block; |
||||||
|
blocks[i >> 2] |= EXTRA[i & 3]; |
||||||
|
this.block = blocks[16]; |
||||||
|
if (i >= 56) { |
||||||
|
if (!this.hashed) { |
||||||
|
this.hash(); |
||||||
|
} |
||||||
|
blocks[0] = this.block; |
||||||
|
blocks[16] = blocks[1] = blocks[2] = blocks[3] = |
||||||
|
blocks[4] = blocks[5] = blocks[6] = blocks[7] = |
||||||
|
blocks[8] = blocks[9] = blocks[10] = blocks[11] = |
||||||
|
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; |
||||||
|
} |
||||||
|
blocks[14] = this.hBytes << 3 | this.bytes >>> 29; |
||||||
|
blocks[15] = this.bytes << 3; |
||||||
|
this.hash(); |
||||||
|
}; |
||||||
|
|
||||||
|
Sha256.prototype.hash = function () { |
||||||
|
var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6, |
||||||
|
h = this.h7, blocks = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc; |
||||||
|
|
||||||
|
for (j = 16; j < 64; ++j) { |
||||||
|
// rightrotate
|
||||||
|
t1 = blocks[j - 15]; |
||||||
|
s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3); |
||||||
|
t1 = blocks[j - 2]; |
||||||
|
s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10); |
||||||
|
blocks[j] = blocks[j - 16] + s0 + blocks[j - 7] + s1 << 0; |
||||||
|
} |
||||||
|
|
||||||
|
bc = b & c; |
||||||
|
for (j = 0; j < 64; j += 4) { |
||||||
|
if (this.first) { |
||||||
|
if (this.is224) { |
||||||
|
ab = 300032; |
||||||
|
t1 = blocks[0] - 1413257819; |
||||||
|
h = t1 - 150054599 << 0; |
||||||
|
d = t1 + 24177077 << 0; |
||||||
|
} else { |
||||||
|
ab = 704751109; |
||||||
|
t1 = blocks[0] - 210244248; |
||||||
|
h = t1 - 1521486534 << 0; |
||||||
|
d = t1 + 143694565 << 0; |
||||||
|
} |
||||||
|
this.first = false; |
||||||
|
} else { |
||||||
|
s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10)); |
||||||
|
s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7)); |
||||||
|
ab = a & b; |
||||||
|
maj = ab ^ (a & c) ^ bc; |
||||||
|
ch = (e & f) ^ (~e & g); |
||||||
|
t1 = h + s1 + ch + K[j] + blocks[j]; |
||||||
|
t2 = s0 + maj; |
||||||
|
h = d + t1 << 0; |
||||||
|
d = t1 + t2 << 0; |
||||||
|
} |
||||||
|
s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10)); |
||||||
|
s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7)); |
||||||
|
da = d & a; |
||||||
|
maj = da ^ (d & b) ^ ab; |
||||||
|
ch = (h & e) ^ (~h & f); |
||||||
|
t1 = g + s1 + ch + K[j + 1] + blocks[j + 1]; |
||||||
|
t2 = s0 + maj; |
||||||
|
g = c + t1 << 0; |
||||||
|
c = t1 + t2 << 0; |
||||||
|
s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10)); |
||||||
|
s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7)); |
||||||
|
cd = c & d; |
||||||
|
maj = cd ^ (c & a) ^ da; |
||||||
|
ch = (g & h) ^ (~g & e); |
||||||
|
t1 = f + s1 + ch + K[j + 2] + blocks[j + 2]; |
||||||
|
t2 = s0 + maj; |
||||||
|
f = b + t1 << 0; |
||||||
|
b = t1 + t2 << 0; |
||||||
|
s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10)); |
||||||
|
s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7)); |
||||||
|
bc = b & c; |
||||||
|
maj = bc ^ (b & d) ^ cd; |
||||||
|
ch = (f & g) ^ (~f & h); |
||||||
|
t1 = e + s1 + ch + K[j + 3] + blocks[j + 3]; |
||||||
|
t2 = s0 + maj; |
||||||
|
e = a + t1 << 0; |
||||||
|
a = t1 + t2 << 0; |
||||||
|
} |
||||||
|
|
||||||
|
this.h0 = this.h0 + a << 0; |
||||||
|
this.h1 = this.h1 + b << 0; |
||||||
|
this.h2 = this.h2 + c << 0; |
||||||
|
this.h3 = this.h3 + d << 0; |
||||||
|
this.h4 = this.h4 + e << 0; |
||||||
|
this.h5 = this.h5 + f << 0; |
||||||
|
this.h6 = this.h6 + g << 0; |
||||||
|
this.h7 = this.h7 + h << 0; |
||||||
|
}; |
||||||
|
|
||||||
|
Sha256.prototype.hex = function () { |
||||||
|
this.finalize(); |
||||||
|
|
||||||
|
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, |
||||||
|
h6 = this.h6, h7 = this.h7; |
||||||
|
|
||||||
|
var hex = HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] + |
||||||
|
HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] + |
||||||
|
HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] + |
||||||
|
HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] + |
||||||
|
HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] + |
||||||
|
HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] + |
||||||
|
HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] + |
||||||
|
HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] + |
||||||
|
HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] + |
||||||
|
HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] + |
||||||
|
HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] + |
||||||
|
HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] + |
||||||
|
HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F] + |
||||||
|
HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] + |
||||||
|
HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] + |
||||||
|
HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] + |
||||||
|
HEX_CHARS[(h4 >> 28) & 0x0F] + HEX_CHARS[(h4 >> 24) & 0x0F] + |
||||||
|
HEX_CHARS[(h4 >> 20) & 0x0F] + HEX_CHARS[(h4 >> 16) & 0x0F] + |
||||||
|
HEX_CHARS[(h4 >> 12) & 0x0F] + HEX_CHARS[(h4 >> 8) & 0x0F] + |
||||||
|
HEX_CHARS[(h4 >> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F] + |
||||||
|
HEX_CHARS[(h5 >> 28) & 0x0F] + HEX_CHARS[(h5 >> 24) & 0x0F] + |
||||||
|
HEX_CHARS[(h5 >> 20) & 0x0F] + HEX_CHARS[(h5 >> 16) & 0x0F] + |
||||||
|
HEX_CHARS[(h5 >> 12) & 0x0F] + HEX_CHARS[(h5 >> 8) & 0x0F] + |
||||||
|
HEX_CHARS[(h5 >> 4) & 0x0F] + HEX_CHARS[h5 & 0x0F] + |
||||||
|
HEX_CHARS[(h6 >> 28) & 0x0F] + HEX_CHARS[(h6 >> 24) & 0x0F] + |
||||||
|
HEX_CHARS[(h6 >> 20) & 0x0F] + HEX_CHARS[(h6 >> 16) & 0x0F] + |
||||||
|
HEX_CHARS[(h6 >> 12) & 0x0F] + HEX_CHARS[(h6 >> 8) & 0x0F] + |
||||||
|
HEX_CHARS[(h6 >> 4) & 0x0F] + HEX_CHARS[h6 & 0x0F]; |
||||||
|
if (!this.is224) { |
||||||
|
hex += HEX_CHARS[(h7 >> 28) & 0x0F] + HEX_CHARS[(h7 >> 24) & 0x0F] + |
||||||
|
HEX_CHARS[(h7 >> 20) & 0x0F] + HEX_CHARS[(h7 >> 16) & 0x0F] + |
||||||
|
HEX_CHARS[(h7 >> 12) & 0x0F] + HEX_CHARS[(h7 >> 8) & 0x0F] + |
||||||
|
HEX_CHARS[(h7 >> 4) & 0x0F] + HEX_CHARS[h7 & 0x0F]; |
||||||
|
} |
||||||
|
return hex; |
||||||
|
}; |
||||||
|
|
||||||
|
Sha256.prototype.toString = Sha256.prototype.hex; |
||||||
|
|
||||||
|
Sha256.prototype.digest = function () { |
||||||
|
this.finalize(); |
||||||
|
|
||||||
|
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, |
||||||
|
h6 = this.h6, h7 = this.h7; |
||||||
|
|
||||||
|
var arr = [ |
||||||
|
(h0 >> 24) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 8) & 0xFF, h0 & 0xFF, |
||||||
|
(h1 >> 24) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 8) & 0xFF, h1 & 0xFF, |
||||||
|
(h2 >> 24) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 8) & 0xFF, h2 & 0xFF, |
||||||
|
(h3 >> 24) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 8) & 0xFF, h3 & 0xFF, |
||||||
|
(h4 >> 24) & 0xFF, (h4 >> 16) & 0xFF, (h4 >> 8) & 0xFF, h4 & 0xFF, |
||||||
|
(h5 >> 24) & 0xFF, (h5 >> 16) & 0xFF, (h5 >> 8) & 0xFF, h5 & 0xFF, |
||||||
|
(h6 >> 24) & 0xFF, (h6 >> 16) & 0xFF, (h6 >> 8) & 0xFF, h6 & 0xFF |
||||||
|
]; |
||||||
|
if (!this.is224) { |
||||||
|
arr.push((h7 >> 24) & 0xFF, (h7 >> 16) & 0xFF, (h7 >> 8) & 0xFF, h7 & 0xFF); |
||||||
|
} |
||||||
|
return arr; |
||||||
|
}; |
||||||
|
|
||||||
|
Sha256.prototype.array = Sha256.prototype.digest; |
||||||
|
|
||||||
|
Sha256.prototype.arrayBuffer = function () { |
||||||
|
this.finalize(); |
||||||
|
|
||||||
|
var buffer = new ArrayBuffer(this.is224 ? 28 : 32); |
||||||
|
var dataView = new DataView(buffer); |
||||||
|
dataView.setUint32(0, this.h0); |
||||||
|
dataView.setUint32(4, this.h1); |
||||||
|
dataView.setUint32(8, this.h2); |
||||||
|
dataView.setUint32(12, this.h3); |
||||||
|
dataView.setUint32(16, this.h4); |
||||||
|
dataView.setUint32(20, this.h5); |
||||||
|
dataView.setUint32(24, this.h6); |
||||||
|
if (!this.is224) { |
||||||
|
dataView.setUint32(28, this.h7); |
||||||
|
} |
||||||
|
return buffer; |
||||||
|
}; |
||||||
|
|
||||||
|
function HmacSha256(key, is224, sharedMemory) { |
||||||
|
var i, type = typeof key; |
||||||
|
if (type === 'string') { |
||||||
|
var bytes = [], length = key.length, index = 0, code; |
||||||
|
for (i = 0; i < length; ++i) { |
||||||
|
code = key.charCodeAt(i); |
||||||
|
if (code < 0x80) { |
||||||
|
bytes[index++] = code; |
||||||
|
} else if (code < 0x800) { |
||||||
|
bytes[index++] = (0xc0 | (code >> 6)); |
||||||
|
bytes[index++] = (0x80 | (code & 0x3f)); |
||||||
|
} else if (code < 0xd800 || code >= 0xe000) { |
||||||
|
bytes[index++] = (0xe0 | (code >> 12)); |
||||||
|
bytes[index++] = (0x80 | ((code >> 6) & 0x3f)); |
||||||
|
bytes[index++] = (0x80 | (code & 0x3f)); |
||||||
|
} else { |
||||||
|
code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff)); |
||||||
|
bytes[index++] = (0xf0 | (code >> 18)); |
||||||
|
bytes[index++] = (0x80 | ((code >> 12) & 0x3f)); |
||||||
|
bytes[index++] = (0x80 | ((code >> 6) & 0x3f)); |
||||||
|
bytes[index++] = (0x80 | (code & 0x3f)); |
||||||
|
} |
||||||
|
} |
||||||
|
key = bytes; |
||||||
|
} else { |
||||||
|
if (type === 'object') { |
||||||
|
if (key === null) { |
||||||
|
throw new Error(ERROR); |
||||||
|
} else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) { |
||||||
|
key = new Uint8Array(key); |
||||||
|
} else if (!Array.isArray(key)) { |
||||||
|
if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) { |
||||||
|
throw new Error(ERROR); |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
throw new Error(ERROR); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (key.length > 64) { |
||||||
|
key = (new Sha256(is224, true)).update(key).array(); |
||||||
|
} |
||||||
|
|
||||||
|
var oKeyPad = [], iKeyPad = []; |
||||||
|
for (i = 0; i < 64; ++i) { |
||||||
|
var b = key[i] || 0; |
||||||
|
oKeyPad[i] = 0x5c ^ b; |
||||||
|
iKeyPad[i] = 0x36 ^ b; |
||||||
|
} |
||||||
|
|
||||||
|
Sha256.call(this, is224, sharedMemory); |
||||||
|
|
||||||
|
this.update(iKeyPad); |
||||||
|
this.oKeyPad = oKeyPad; |
||||||
|
this.inner = true; |
||||||
|
this.sharedMemory = sharedMemory; |
||||||
|
} |
||||||
|
HmacSha256.prototype = new Sha256(); |
||||||
|
|
||||||
|
HmacSha256.prototype.finalize = function () { |
||||||
|
Sha256.prototype.finalize.call(this); |
||||||
|
if (this.inner) { |
||||||
|
this.inner = false; |
||||||
|
var innerHash = this.array(); |
||||||
|
Sha256.call(this, this.is224, this.sharedMemory); |
||||||
|
this.update(this.oKeyPad); |
||||||
|
this.update(innerHash); |
||||||
|
Sha256.prototype.finalize.call(this); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
var exports = createMethod(); |
||||||
|
exports.sha256 = exports; |
||||||
|
exports.sha224 = createMethod(true); |
||||||
|
exports.sha256.hmac = createHmacMethod(); |
||||||
|
exports.sha224.hmac = createHmacMethod(true); |
||||||
|
|
||||||
|
if (COMMON_JS) { |
||||||
|
module.exports = exports; |
||||||
|
} else { |
||||||
|
root.sha256 = exports.sha256; |
||||||
|
root.sha224 = exports.sha224; |
||||||
|
if (AMD) { |
||||||
|
define(function () { |
||||||
|
return exports; |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
})(); |
@ -0,0 +1,87 @@ |
|||||||
|
/* |
||||||
|
dela - web TODO list |
||||||
|
Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
|
||||||
|
package conf |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
) |
||||||
|
|
||||||
|
type Conf struct { |
||||||
|
Port uint16 `json:"port"` |
||||||
|
CertFilePath string `json:"cert_file_path"` |
||||||
|
KeyFilePath string `json:"key_file_path"` |
||||||
|
BaseContentDir string `json:"base_content_dir"` |
||||||
|
ProdDBName string `json:"production_db_name"` |
||||||
|
} |
||||||
|
|
||||||
|
// Creates a default server configuration
|
||||||
|
func Default() Conf { |
||||||
|
return Conf{ |
||||||
|
Port: 8080, |
||||||
|
CertFilePath: "", |
||||||
|
KeyFilePath: "", |
||||||
|
BaseContentDir: ".", |
||||||
|
ProdDBName: "dela.db", |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Tries to retrieve configuration from given json file
|
||||||
|
func FromFile(path string) (Conf, error) { |
||||||
|
configFile, err := os.Open(path) |
||||||
|
if err != nil { |
||||||
|
return Default(), err |
||||||
|
} |
||||||
|
defer configFile.Close() |
||||||
|
|
||||||
|
confBytes, err := io.ReadAll(configFile) |
||||||
|
if err != nil { |
||||||
|
return Default(), err |
||||||
|
} |
||||||
|
|
||||||
|
var config Conf |
||||||
|
err = json.Unmarshal(confBytes, &config) |
||||||
|
if err != nil { |
||||||
|
return Default(), err |
||||||
|
} |
||||||
|
|
||||||
|
return config, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Create empty configuration file
|
||||||
|
func Create(path string, conf Conf) (Conf, error) { |
||||||
|
configFile, err := os.Create(path) |
||||||
|
if err != nil { |
||||||
|
return Default(), err |
||||||
|
} |
||||||
|
defer configFile.Close() |
||||||
|
|
||||||
|
configJsonBytes, err := json.MarshalIndent(conf, "", " ") |
||||||
|
if err != nil { |
||||||
|
return conf, err |
||||||
|
} |
||||||
|
|
||||||
|
_, err = configFile.Write(configJsonBytes) |
||||||
|
if err != nil { |
||||||
|
return conf, nil |
||||||
|
} |
||||||
|
|
||||||
|
return conf, nil |
||||||
|
} |
@ -0,0 +1,113 @@ |
|||||||
|
/* |
||||||
|
dela - web TODO list |
||||||
|
Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
|
||||||
|
package db |
||||||
|
|
||||||
|
import ( |
||||||
|
"database/sql" |
||||||
|
"os" |
||||||
|
|
||||||
|
_ "modernc.org/sqlite" |
||||||
|
) |
||||||
|
|
||||||
|
// Database wrapper
|
||||||
|
type DB struct { |
||||||
|
*sql.DB |
||||||
|
} |
||||||
|
|
||||||
|
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)`, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Todo groups
|
||||||
|
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS todo_groups( |
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, |
||||||
|
name TEXT, |
||||||
|
time_created_unix INTEGER, |
||||||
|
owner_login TEXT NOT NULL, |
||||||
|
FOREIGN KEY(owner_login) REFERENCES users(login))`, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Todos
|
||||||
|
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS todos( |
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, |
||||||
|
group_id INTEGER NOT NULL, |
||||||
|
text TEXT NOT NULL, |
||||||
|
time_created_unix INTEGER, |
||||||
|
due_unix INTEGER, |
||||||
|
owner_login TEXT NOT NULL, |
||||||
|
is_done INTEGER, |
||||||
|
completion_time_unix INTEGER, |
||||||
|
FOREIGN KEY(group_id) REFERENCES todo_groups(id), |
||||||
|
FOREIGN KEY(owner_login) REFERENCES users(login))`, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Open database
|
||||||
|
func FromFile(path string) (*DB, error) { |
||||||
|
driver, err := sql.Open("sqlite", path) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
dbase := &DB{driver} |
||||||
|
|
||||||
|
err = setUpTables(dbase) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return dbase, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Create database file
|
||||||
|
func Create(path string) (*DB, error) { |
||||||
|
dbFile, err := os.Create(path) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
dbFile.Close() |
||||||
|
|
||||||
|
driver, err := sql.Open("sqlite", path) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
dbase := &DB{driver} |
||||||
|
|
||||||
|
err = setUpTables(dbase) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return dbase, nil |
||||||
|
} |
@ -0,0 +1,97 @@ |
|||||||
|
/* |
||||||
|
dela - web TODO list |
||||||
|
Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
|
||||||
|
package db |
||||||
|
|
||||||
|
import ( |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
func TestApi(t *testing.T) { |
||||||
|
db, err := Create(filepath.Join(os.TempDir(), "dela_test.db")) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("failed to create database: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
// User
|
||||||
|
user := User{ |
||||||
|
Login: "user1", |
||||||
|
Password: "ruohguoeruoger", |
||||||
|
TimeCreatedUnix: 12421467, |
||||||
|
} |
||||||
|
|
||||||
|
err = db.CreateUser(user) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("failed to create user: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
dbUser, err := db.GetUser(user.Login) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("failed to retrieve created user: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
if dbUser.Password != user.Password { |
||||||
|
t.Fatalf("user passwords don't match") |
||||||
|
} |
||||||
|
|
||||||
|
// Todos
|
||||||
|
group := TodoGroup{ |
||||||
|
Name: "group1", |
||||||
|
TimeCreatedUnix: 13524534, |
||||||
|
OwnerLogin: user.Login, |
||||||
|
} |
||||||
|
|
||||||
|
err = db.CreateTodoGroup(group) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("failed to create todo group: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
err = db.UpdateTodoGroup(1, TodoGroup{Name: "updated_name"}) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("failed to update todo group: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
dbGroup, err := db.GetTodoGroup(1) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("failed to get created TODO group: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
if dbGroup.Name == group.Name { |
||||||
|
t.Fatalf("name match changed value for a TODO group") |
||||||
|
} |
||||||
|
|
||||||
|
todo := Todo{ |
||||||
|
GroupID: dbGroup.ID, |
||||||
|
Text: "Do the dishes", |
||||||
|
TimeCreatedUnix: dbGroup.TimeCreatedUnix, |
||||||
|
DueUnix: 0, |
||||||
|
OwnerLogin: user.Login, |
||||||
|
} |
||||||
|
err = db.CreateTodo(todo) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("couldn't create a new TODO: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Now deletion
|
||||||
|
err = db.DeleteUserClean(user.Login) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("couldn't cleanly delete user with all TODOs: %s", err) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,113 @@ |
|||||||
|
package db |
||||||
|
|
||||||
|
import "database/sql" |
||||||
|
|
||||||
|
// Todo group structure
|
||||||
|
type TodoGroup struct { |
||||||
|
ID uint64 `json:"id"` |
||||||
|
Name string `json:"name"` |
||||||
|
TimeCreatedUnix uint64 `json:"timeCreatedUnix"` |
||||||
|
OwnerLogin string `json:"ownerLogin"` |
||||||
|
} |
||||||
|
|
||||||
|
// 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(?, ?, ?)", |
||||||
|
group.Name, |
||||||
|
group.TimeCreatedUnix, |
||||||
|
group.OwnerLogin, |
||||||
|
) |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func scanTodoGroup(rows *sql.Rows) (*TodoGroup, error) { |
||||||
|
var newTodoGroup TodoGroup |
||||||
|
err := rows.Scan( |
||||||
|
&newTodoGroup.ID, |
||||||
|
&newTodoGroup.Name, |
||||||
|
&newTodoGroup.TimeCreatedUnix, |
||||||
|
&newTodoGroup.OwnerLogin, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return &newTodoGroup, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Retrieves a TODO group with provided ID from the database
|
||||||
|
func (db *DB) GetTodoGroup(id uint64) (*TodoGroup, error) { |
||||||
|
rows, err := db.Query( |
||||||
|
"SELECT * FROM todo_groups WHERE id=?", |
||||||
|
id, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer rows.Close() |
||||||
|
|
||||||
|
rows.Next() |
||||||
|
todoGroup, err := scanTodoGroup(rows) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
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( |
||||||
|
"DELETE FROM todo_groups WHERE id=?", |
||||||
|
id, |
||||||
|
) |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Updates TODO group's name
|
||||||
|
func (db *DB) UpdateTodoGroup(groupID uint64, updatedGroup TodoGroup) error { |
||||||
|
_, err := db.Exec( |
||||||
|
"UPDATE todo_groups SET name=? WHERE id=?", |
||||||
|
updatedGroup.Name, |
||||||
|
groupID, |
||||||
|
) |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func (db *DB) DoesUserOwnGroup(groupId uint64, login string) bool { |
||||||
|
group, err := db.GetTodoGroup(groupId) |
||||||
|
if err != nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if group.OwnerLogin != login { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
@ -0,0 +1,216 @@ |
|||||||
|
/* |
||||||
|
dela - web TODO list |
||||||
|
Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
|
||||||
|
package db |
||||||
|
|
||||||
|
import "database/sql" |
||||||
|
|
||||||
|
// Todo structure
|
||||||
|
type Todo struct { |
||||||
|
ID uint64 `json:"id"` |
||||||
|
GroupID uint64 `json:"groupId"` |
||||||
|
Text string `json:"text"` |
||||||
|
TimeCreatedUnix uint64 `json:"timeCreatedUnix"` |
||||||
|
DueUnix uint64 `json:"dueUnix"` |
||||||
|
OwnerLogin string `json:"ownerLogin"` |
||||||
|
IsDone bool `json:"isDone"` |
||||||
|
CompletionTimeUnix uint64 `json:"completionTimeUnix"` |
||||||
|
} |
||||||
|
|
||||||
|
func scanTodo(rows *sql.Rows) (*Todo, error) { |
||||||
|
var newTodo Todo |
||||||
|
err := rows.Scan( |
||||||
|
&newTodo.ID, |
||||||
|
&newTodo.GroupID, |
||||||
|
&newTodo.Text, |
||||||
|
&newTodo.TimeCreatedUnix, |
||||||
|
&newTodo.DueUnix, |
||||||
|
&newTodo.OwnerLogin, |
||||||
|
&newTodo.IsDone, |
||||||
|
&newTodo.CompletionTimeUnix, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return &newTodo, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Retrieves a TODO with given Id from the database
|
||||||
|
func (db *DB) GetTodo(id uint64) (*Todo, error) { |
||||||
|
rows, err := db.Query( |
||||||
|
"SELECT * FROM todos WHERE id=?", |
||||||
|
id, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer rows.Close() |
||||||
|
|
||||||
|
rows.Next() |
||||||
|
todo, err := scanTodo(rows) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
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( |
||||||
|
"INSERT INTO todos(group_id, text, time_created_unix, due_unix, owner_username, is_done, completion_time_unix) VALUES(?, ?, ?, ?, ?, ?, ?)", |
||||||
|
todo.GroupID, |
||||||
|
todo.Text, |
||||||
|
todo.TimeCreatedUnix, |
||||||
|
todo.DueUnix, |
||||||
|
todo.OwnerLogin, |
||||||
|
todo.IsDone, |
||||||
|
todo.CompletionTimeUnix, |
||||||
|
) |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Deletes information about a TODO of certain ID from the database
|
||||||
|
func (db *DB) DeleteTodo(id uint64) error { |
||||||
|
_, err := db.Exec( |
||||||
|
"DELETE FROM todos WHERE id=?", |
||||||
|
id, |
||||||
|
) |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Updates TODO's due date, text, done state, completion time and group id
|
||||||
|
func (db *DB) UpdateTodo(todoID uint64, updatedTodo Todo) error { |
||||||
|
_, err := db.Exec( |
||||||
|
"UPDATE todos SET group_id=?, due_unix=?, text=?, is_done=?, completion_time_unix=? WHERE id=?", |
||||||
|
updatedTodo.GroupID, |
||||||
|
updatedTodo.DueUnix, |
||||||
|
updatedTodo.Text, |
||||||
|
updatedTodo.IsDone, |
||||||
|
updatedTodo.CompletionTimeUnix, |
||||||
|
todoID, |
||||||
|
) |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Searches and retrieves TODO groups created by the user
|
||||||
|
func (db *DB) GetAllUserTodoGroups(username string) ([]*TodoGroup, error) { |
||||||
|
var todoGroups []*TodoGroup |
||||||
|
|
||||||
|
rows, err := db.Query( |
||||||
|
"SELECT * FROM todo_groups WHERE owner_username=?", |
||||||
|
username, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer rows.Close() |
||||||
|
|
||||||
|
for rows.Next() { |
||||||
|
group, err := scanTodoGroup(rows) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
todoGroups = append(todoGroups, group) |
||||||
|
} |
||||||
|
|
||||||
|
return todoGroups, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Searches and retrieves TODOs created by the user
|
||||||
|
func (db *DB) GetAllUserTodos(username string) ([]*Todo, error) { |
||||||
|
var todos []*Todo |
||||||
|
|
||||||
|
rows, err := db.Query( |
||||||
|
"SELECT * FROM todos WHERE owner_username=?", |
||||||
|
username, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer rows.Close() |
||||||
|
|
||||||
|
for rows.Next() { |
||||||
|
todo, err := scanTodo(rows) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
todos = append(todos, todo) |
||||||
|
} |
||||||
|
|
||||||
|
return todos, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Deletes all information regarding TODOs of specified user
|
||||||
|
func (db *DB) DeleteAllUserTodos(username string) error { |
||||||
|
_, err := db.Exec( |
||||||
|
"DELETE FROM todos WHERE owner_username=?", |
||||||
|
username, |
||||||
|
) |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Deletes all information regarding TODO groups of specified user
|
||||||
|
func (db *DB) DeleteAllUserTodoGroups(username string) error { |
||||||
|
_, err := db.Exec( |
||||||
|
"DELETE FROM todo_groups WHERE owner_username=?", |
||||||
|
username, |
||||||
|
) |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func (db *DB) DoesUserOwnTodo(todoId uint64, login string) bool { |
||||||
|
todo, err := db.GetTodo(todoId) |
||||||
|
if err != nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if todo.OwnerLogin != login { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
@ -0,0 +1,110 @@ |
|||||||
|
/* |
||||||
|
dela - web TODO list |
||||||
|
Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
|
||||||
|
package db |
||||||
|
|
||||||
|
import "database/sql" |
||||||
|
|
||||||
|
// User structure
|
||||||
|
type User struct { |
||||||
|
Login string `json:"login"` |
||||||
|
Email string `json:"email"` |
||||||
|
Password string `json:"password"` |
||||||
|
TimeCreatedUnix uint64 `json:"timeCreatedUnix"` |
||||||
|
} |
||||||
|
|
||||||
|
func scanUser(rows *sql.Rows) (*User, error) { |
||||||
|
rows.Next() |
||||||
|
var user User |
||||||
|
err := rows.Scan(&user.Login, &user.Email, &user.Password, &user.TimeCreatedUnix) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return &user, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Searches for user with login and returns it
|
||||||
|
func (db *DB) GetUser(login string) (*User, error) { |
||||||
|
rows, err := db.Query("SELECT * FROM users WHERE login=?", login) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer rows.Close() |
||||||
|
|
||||||
|
user, err := scanUser(rows) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return user, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Creates a new user in the database
|
||||||
|
func (db *DB) CreateUser(newUser User) error { |
||||||
|
_, err := db.Exec( |
||||||
|
"INSERT INTO users(login, email, password, time_created_unix) VALUES(?, ?, ?, ?)", |
||||||
|
newUser.Login, |
||||||
|
newUser.Email, |
||||||
|
newUser.Password, |
||||||
|
newUser.TimeCreatedUnix, |
||||||
|
) |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Deletes user with given login
|
||||||
|
func (db *DB) DeleteUser(login string) error { |
||||||
|
_, err := db.Exec( |
||||||
|
"DELETE FROM users WHERE login=?", |
||||||
|
login, |
||||||
|
) |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func (db *DB) UserUpdate(newUser User) error { |
||||||
|
_, err := db.Exec( |
||||||
|
"UPDATE users SET email=? password=? WHERE login=?", |
||||||
|
newUser.Email, |
||||||
|
newUser.Password, |
||||||
|
newUser.Login, |
||||||
|
) |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Deletes a user and all his TODOs (with groups) as well
|
||||||
|
func (db *DB) DeleteUserClean(login string) error { |
||||||
|
err := db.DeleteAllUserTodoGroups(login) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
err = db.DeleteAllUserTodos(login) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
err = db.DeleteUser(login) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
/* |
||||||
|
dela - web TODO list |
||||||
|
Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
|
||||||
|
package encryption |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/sha256" |
||||||
|
"encoding/base64" |
||||||
|
"fmt" |
||||||
|
) |
||||||
|
|
||||||
|
// Encodes given string via Base64
|
||||||
|
func EncodeString(str string) string { |
||||||
|
return base64.StdEncoding.EncodeToString([]byte(str)) |
||||||
|
} |
||||||
|
|
||||||
|
// Decodes given string via Base64
|
||||||
|
func DecodeString(encodedStr string) string { |
||||||
|
decodedBytes, _ := base64.StdEncoding.DecodeString(encodedStr) |
||||||
|
return string(decodedBytes) |
||||||
|
} |
||||||
|
|
||||||
|
// Returns HEX string of SHA256'd data
|
||||||
|
func SHA256Hex(data []byte) string { |
||||||
|
hash := sha256.New() |
||||||
|
hash.Write(data) |
||||||
|
return fmt.Sprintf("%x", hash.Sum(nil)) |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
module Unbewohnte/dela |
||||||
|
|
||||||
|
go 1.20 |
||||||
|
|
||||||
|
require modernc.org/sqlite v1.22.0 |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect |
||||||
|
github.com/google/uuid v1.3.0 // indirect |
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect |
||||||
|
github.com/mattn/go-isatty v0.0.16 // indirect |
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect |
||||||
|
golang.org/x/mod v0.3.0 // indirect |
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect |
||||||
|
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect |
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect |
||||||
|
lukechampine.com/uint128 v1.2.0 // indirect |
||||||
|
modernc.org/cc/v3 v3.40.0 // indirect |
||||||
|
modernc.org/ccgo/v3 v3.16.13 // indirect |
||||||
|
modernc.org/libc v1.22.4 // indirect |
||||||
|
modernc.org/mathutil v1.5.0 // indirect |
||||||
|
modernc.org/memory v1.5.0 // indirect |
||||||
|
modernc.org/opt v0.1.3 // indirect |
||||||
|
modernc.org/strutil v1.1.3 // indirect |
||||||
|
modernc.org/token v1.0.1 // indirect |
||||||
|
) |
@ -0,0 +1,65 @@ |
|||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= |
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= |
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= |
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= |
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= |
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= |
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= |
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= |
||||||
|
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= |
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= |
||||||
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= |
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= |
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= |
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= |
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= |
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= |
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
||||||
|
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= |
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= |
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= |
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= |
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
||||||
|
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs= |
||||||
|
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= |
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= |
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||||
|
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= |
||||||
|
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= |
||||||
|
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= |
||||||
|
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= |
||||||
|
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= |
||||||
|
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= |
||||||
|
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= |
||||||
|
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= |
||||||
|
modernc.org/libc v1.22.4 h1:wymSbZb0AlrjdAVX3cjreCHTPCpPARbQXNz6BHPzdwQ= |
||||||
|
modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= |
||||||
|
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= |
||||||
|
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= |
||||||
|
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= |
||||||
|
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= |
||||||
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= |
||||||
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= |
||||||
|
modernc.org/sqlite v1.22.0 h1:Uo+wEWePCspy4SAu0w2VbzUHEftOs7yoaWX/cYjsq84= |
||||||
|
modernc.org/sqlite v1.22.0/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0= |
||||||
|
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= |
||||||
|
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= |
||||||
|
modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws= |
||||||
|
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= |
||||||
|
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= |
||||||
|
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE= |
@ -0,0 +1,60 @@ |
|||||||
|
/* |
||||||
|
dela - web TODO list |
||||||
|
Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
|
||||||
|
package logger |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
"log" |
||||||
|
"os" |
||||||
|
) |
||||||
|
|
||||||
|
// 3 basic loggers in global space
|
||||||
|
var ( |
||||||
|
// neutral information logger
|
||||||
|
infoLog *log.Logger |
||||||
|
// warning-level information logger
|
||||||
|
warningLog *log.Logger |
||||||
|
// errors information logger
|
||||||
|
errorLog *log.Logger |
||||||
|
) |
||||||
|
|
||||||
|
func init() { |
||||||
|
infoLog = log.New(os.Stdout, "[INFO] ", log.Ldate|log.Ltime) |
||||||
|
warningLog = log.New(os.Stdout, "[WARNING] ", log.Ldate|log.Ltime) |
||||||
|
errorLog = log.New(os.Stdout, "[ERROR] ", log.Ldate|log.Ltime) |
||||||
|
} |
||||||
|
|
||||||
|
// Set up loggers to write to the given writer
|
||||||
|
func SetOutput(writer io.Writer) { |
||||||
|
if writer == nil { |
||||||
|
writer = io.Discard |
||||||
|
} |
||||||
|
infoLog.SetOutput(writer) |
||||||
|
warningLog.SetOutput(writer) |
||||||
|
errorLog.SetOutput(writer) |
||||||
|
} |
||||||
|
func Info(format string, a ...interface{}) { |
||||||
|
infoLog.Printf(format, a...) |
||||||
|
} |
||||||
|
func Warning(format string, a ...interface{}) { |
||||||
|
warningLog.Printf(format, a...) |
||||||
|
} |
||||||
|
func Error(format string, a ...interface{}) { |
||||||
|
errorLog.Printf(format, a...) |
||||||
|
} |
@ -0,0 +1,96 @@ |
|||||||
|
/* |
||||||
|
dela - web TODO list |
||||||
|
Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
|
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"Unbewohnte/dela/conf" |
||||||
|
"Unbewohnte/dela/logger" |
||||||
|
"Unbewohnte/dela/server" |
||||||
|
"flag" |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
) |
||||||
|
|
||||||
|
const Version string = "0.1.2" |
||||||
|
|
||||||
|
var ( |
||||||
|
printVersion *bool = flag.Bool("version", false, "Print version information and exit") |
||||||
|
) |
||||||
|
|
||||||
|
const ConfName string = "conf.json" |
||||||
|
|
||||||
|
var ( |
||||||
|
WDir string |
||||||
|
Conf conf.Conf |
||||||
|
) |
||||||
|
|
||||||
|
func init() { |
||||||
|
// Parse flags
|
||||||
|
flag.Parse() |
||||||
|
if *printVersion { |
||||||
|
fmt.Printf("dela v%s - a web TODO list\n(c) 2023 Kasyanov Nikolay Alexeyevich (Unbewohnte)\n", Version) |
||||||
|
os.Exit(0) |
||||||
|
} |
||||||
|
|
||||||
|
// Initialize logging
|
||||||
|
logger.SetOutput(os.Stdout) |
||||||
|
|
||||||
|
// Work out the working directory
|
||||||
|
exePath, err := os.Executable() |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Init] Failed to retrieve executable's path: %s", err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
WDir = filepath.Dir(exePath) |
||||||
|
logger.Info("[Init] Working in \"%s\"", WDir) |
||||||
|
|
||||||
|
// Open configuration, create if does not exist
|
||||||
|
Conf, err = conf.FromFile(filepath.Join(WDir, ConfName)) |
||||||
|
if err != nil { |
||||||
|
_, err = conf.Create(filepath.Join(WDir, ConfName), conf.Default()) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Init] Failed to create a new configuration file: %s", err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
logger.Info("[Init] Created a new configuration file") |
||||||
|
os.Exit(0) |
||||||
|
} |
||||||
|
logger.Info("[Init] Opened existing configuration file") |
||||||
|
if Conf.BaseContentDir == "." { |
||||||
|
Conf.BaseContentDir = WDir |
||||||
|
} |
||||||
|
|
||||||
|
logger.Info("[Init] Successful initializaion!") |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
server, err := server.New(Conf) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Main] Failed to initialize a new server with conf (%+v): %s", Conf, err) |
||||||
|
return |
||||||
|
} |
||||||
|
logger.Info("[Main] Successfully initialized a new server instance with conf (%+v)", Conf) |
||||||
|
|
||||||
|
err = server.Start() |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Main] Fatal server failure: %s. Exiting...", err) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,146 @@ |
|||||||
|
/* |
||||||
|
dela - web TODO list |
||||||
|
Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
|
||||||
|
package server |
||||||
|
|
||||||
|
import ( |
||||||
|
"Unbewohnte/dela/conf" |
||||||
|
"Unbewohnte/dela/db" |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
func TestApi(t *testing.T) { |
||||||
|
// Create a new server
|
||||||
|
config := conf.Default() |
||||||
|
config.BaseContentDir = "../../" |
||||||
|
config.ProdDBName = filepath.Join(os.TempDir(), "dela_test_db.db") |
||||||
|
server, err := New(config) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("failed to create a new server: %s", err) |
||||||
|
} |
||||||
|
defer os.Remove(config.ProdDBName) |
||||||
|
|
||||||
|
go func() { |
||||||
|
time.Sleep(time.Second * 5) |
||||||
|
server.Stop() |
||||||
|
}() |
||||||
|
|
||||||
|
go func() { |
||||||
|
server.Start() |
||||||
|
}() |
||||||
|
|
||||||
|
// Create a new user
|
||||||
|
newUser := db.User{ |
||||||
|
Login: "user1", |
||||||
|
Password: "ruohguoeruoger", |
||||||
|
TimeCreatedUnix: 12421467, |
||||||
|
} |
||||||
|
newUserJsonBytes, err := json.Marshal(&newUser) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("could not marshal new user JSON: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
resp, err := http.Post(fmt.Sprintf("http://localhost:%d/api/user", config.Port), "application/json", bytes.NewBuffer(newUserJsonBytes)) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("failed to post a new user data: %s", err) |
||||||
|
} |
||||||
|
body, err := io.ReadAll(resp.Body) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("failed to read response body for user creation: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK { |
||||||
|
t.Fatalf("got non-OK status code for user creation: %s", string(body)) |
||||||
|
} |
||||||
|
resp.Body.Close() |
||||||
|
|
||||||
|
// Create a new TODO
|
||||||
|
// newGroup := db.TodoGroup{
|
||||||
|
// Name: "group1",
|
||||||
|
// TimeCreatedUnix: 13524534,
|
||||||
|
// OwnerUsername: newUser.Login,
|
||||||
|
// }
|
||||||
|
// newGroupBytes, err := json.Marshal(&newGroup)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatalf("could not marshal new user JSON: %s", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/api/groups", config.Port), bytes.NewBuffer(newGroupBytes))
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatalf("failed to create a new POST request to create a new TODO group: %s", err)
|
||||||
|
// }
|
||||||
|
// req.Header.Add(RequestHeaderAuthKey, fmt.Sprintf("%s%s%s", newUser.Login, RequestHeaderAuthSeparator, newUser.Password))
|
||||||
|
// req.Header.Add(RequestHeaderEncodedB64, "false")
|
||||||
|
|
||||||
|
// resp, err = http.DefaultClient.Do(req)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatalf("failed to post a new TODO group: %s", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
body, err = io.ReadAll(resp.Body) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("failed to read response body for TODO group creation: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK { |
||||||
|
t.Fatalf("got non-OK status code for TODO group creation: %s", string(body)) |
||||||
|
} |
||||||
|
resp.Body.Close() |
||||||
|
|
||||||
|
// TODO creation
|
||||||
|
var newTodo db.Todo = db.Todo{ |
||||||
|
GroupID: 0, |
||||||
|
Text: "Do the dishes", |
||||||
|
TimeCreatedUnix: uint64(time.Now().UnixMicro()), |
||||||
|
DueUnix: uint64(time.Now().Add(time.Hour * 5).UnixMicro()), |
||||||
|
OwnerLogin: newUser.Login, |
||||||
|
} |
||||||
|
|
||||||
|
newTodoBytes, err := json.Marshal(&newTodo) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("could not marshal new Todo: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/api/todo", config.Port), bytes.NewBuffer(newTodoBytes)) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("failed to create a new POST request to create a new TODO: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
resp, err = http.DefaultClient.Do(req) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("failed to post a new Todo: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
body, err = io.ReadAll(resp.Body) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("failed to read response body for Todo creation: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK { |
||||||
|
t.Fatalf("got non-OK status code for Todo creation: %s", string(body)) |
||||||
|
} |
||||||
|
resp.Body.Close() |
||||||
|
} |
@ -0,0 +1,510 @@ |
|||||||
|
/* |
||||||
|
dela - web TODO list |
||||||
|
Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
|
||||||
|
package server |
||||||
|
|
||||||
|
import ( |
||||||
|
"Unbewohnte/dela/db" |
||||||
|
"Unbewohnte/dela/logger" |
||||||
|
"encoding/json" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
"path" |
||||||
|
"strconv" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
func (s *Server) EndpointUserCreate(w http.ResponseWriter, req *http.Request) { |
||||||
|
if req.Method != http.MethodPost { |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Retrieve user data
|
||||||
|
defer req.Body.Close() |
||||||
|
|
||||||
|
contents, err := io.ReadAll(req.Body) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Server][EndpointUserCreate] Failed to read request body: %s", err) |
||||||
|
http.Error(w, "Failed to read request body", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
var user db.User |
||||||
|
err = json.Unmarshal(contents, &user) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Server][EndpointUserCreate] Failed to unmarshal user data: %s", err) |
||||||
|
http.Error(w, "User JSON unmarshal error", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Sanitize
|
||||||
|
valid, reason := IsUserValid(user) |
||||||
|
if !valid { |
||||||
|
http.Error(w, reason, http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
user.TimeCreatedUnix = uint64(time.Now().Unix()) |
||||||
|
|
||||||
|
// Insert into DB
|
||||||
|
err = s.db.CreateUser(user) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Server][EndpointUserCreate] Failed to insert new user \"%s\" data: %s", user.Login, err) |
||||||
|
http.Error(w, "Failed to create user", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
logger.Info("[Server][EndpointUserCreate] Created a new user with login \"%s\"", user.Login) |
||||||
|
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) |
||||||
|
} |
||||||
|
|
||||||
|
// Retrieve user data
|
||||||
|
defer req.Body.Close() |
||||||
|
|
||||||
|
// Authentication check
|
||||||
|
if !IsUserAuthorizedReq(req, s.db) { |
||||||
|
http.Error(w, "Authentication error", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
contents, err := io.ReadAll(req.Body) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Server][EndpointUserUpdate] Failed to read request body: %s", err) |
||||||
|
http.Error(w, "Failed to read request body", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
var user db.User |
||||||
|
err = json.Unmarshal(contents, &user) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Server][EndpointUserUpdate] Failed to unmarshal user data: %s", err) |
||||||
|
http.Error(w, "User JSON unmarshal error", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check whether the user in request is the user specified in JSON
|
||||||
|
login, _, _ := req.BasicAuth() |
||||||
|
if login != user.Login { |
||||||
|
// Gotcha!
|
||||||
|
logger.Warning("[Server][EndpointUserUpdate] %s tried to update user information of %s!", login, user.Login) |
||||||
|
http.Error(w, "Logins do not match", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Update
|
||||||
|
err = s.db.UserUpdate(user) |
||||||
|
if err != nil { |
||||||
|
http.Error(w, "Failed to update user", http.StatusInternalServerError) |
||||||
|
logger.Error("[Server][EndpointUserUpdate] Failed to update \"%s\": %s", user.Login, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
logger.Info("[Server][EndpointUserUpdate] Updated a user with login \"%s\"", user.Login) |
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) EndpointUserDelete(w http.ResponseWriter, req *http.Request) { |
||||||
|
if req.Method != http.MethodPost { |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
defer req.Body.Close() |
||||||
|
|
||||||
|
// Authentication check
|
||||||
|
if !IsUserAuthorizedReq(req, s.db) { |
||||||
|
http.Error(w, "Authentication error", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Delete
|
||||||
|
login, _, _ := req.BasicAuth() |
||||||
|
err := s.db.DeleteUser(login) |
||||||
|
if err != nil { |
||||||
|
http.Error(w, "Failed to delete user", http.StatusInternalServerError) |
||||||
|
logger.Error("[Server][EndpointUserDelete] Failed to delete \"%s\": %s", login, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
logger.Info("[Server][EndpointUserDelete] Deleted a user with login \"%s\"", login) |
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) EndpointUserGet(w http.ResponseWriter, req *http.Request) { |
||||||
|
if req.Method != http.MethodGet { |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
defer req.Body.Close() |
||||||
|
|
||||||
|
// Authentication check
|
||||||
|
if !IsUserAuthorizedReq(req, s.db) { |
||||||
|
http.Error(w, "Authentication error", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Get information from the database
|
||||||
|
login, _, _ := req.BasicAuth() |
||||||
|
userDB, err := s.db.GetUser(login) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Server][EndpointUserGet] Failed to retrieve information on \"%s\": %s", login, err) |
||||||
|
http.Error(w, "Failed to fetch information", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
userDBBytes, err := json.Marshal(&userDB) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Server][EndpointUserGet] Failed to marshal information on \"%s\": %s", login, err) |
||||||
|
http.Error(w, "Failed to marshal information", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Send
|
||||||
|
w.Write(userDBBytes) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) EndpointTodoUpdate(w http.ResponseWriter, req *http.Request) { |
||||||
|
defer req.Body.Close() |
||||||
|
|
||||||
|
if req.Method != http.MethodPost { |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check authentication information
|
||||||
|
if !IsUserAuthorizedReq(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 { |
||||||
|
http.Error(w, "Invalid TODO ID", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check if the user owns this TODO
|
||||||
|
if !s.db.DoesUserOwnTodo(todoID, GetLoginFromAuth(req)) { |
||||||
|
http.Error(w, "You don't own this TODO", 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 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) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) EndpointTodoDelete(w http.ResponseWriter, req *http.Request) { |
||||||
|
defer req.Body.Close() |
||||||
|
|
||||||
|
if req.Method != http.MethodPost { |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Delete an existing TODO
|
||||||
|
|
||||||
|
// Check if this user actually exists and the password is valid
|
||||||
|
if !IsUserAuthorizedReq(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 { |
||||||
|
http.Error(w, "Invalid TODO ID", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check if the user owns this TODO
|
||||||
|
if !s.db.DoesUserOwnTodo(todoID, GetLoginFromAuth(req)) { |
||||||
|
http.Error(w, "You don't own this TODO", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Now delete
|
||||||
|
err = s.db.DeleteTodo(todoID) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Server] Failed to delete %s's TODO: %s", GetLoginFromAuth(req), err) |
||||||
|
http.Error(w, "Failed to delete TODO", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Success!
|
||||||
|
logger.Info("[Server] Deleted TODO with ID %d", todoID) |
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) EndpointTodoCreate(w http.ResponseWriter, req *http.Request) { |
||||||
|
// Create a new TODO
|
||||||
|
defer req.Body.Close() |
||||||
|
// Read body
|
||||||
|
body, err := io.ReadAll(req.Body) |
||||||
|
if err != nil { |
||||||
|
logger.Warning("[Server] Failed to read request body to create a new TODO: %s", err) |
||||||
|
http.Error(w, "Failed to read body", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Unmarshal JSON
|
||||||
|
var newTodo db.Todo |
||||||
|
err = json.Unmarshal(body, &newTodo) |
||||||
|
if err != nil { |
||||||
|
logger.Warning("[Server] Received invalid TODO JSON for creation: %s", err) |
||||||
|
http.Error(w, "Invalid TODO JSON", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check for authentication problems
|
||||||
|
if !IsUserAuthorizedReq(req, s.db) { |
||||||
|
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Add TODO to the database
|
||||||
|
newTodo.OwnerLogin = GetLoginFromAuth(req) |
||||||
|
newTodo.TimeCreatedUnix = uint64(time.Now().Unix()) |
||||||
|
err = s.db.CreateTodo(newTodo) |
||||||
|
if err != nil { |
||||||
|
http.Error(w, "Failed to create TODO", http.StatusInternalServerError) |
||||||
|
logger.Error("[Server] Failed to put a new todo (%+v) into the db: %s", newTodo, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Success!
|
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
logger.Info("[Server] Created a new TODO for %s", newTodo.OwnerLogin) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) EndpointUserTodosGet(w http.ResponseWriter, req *http.Request) { |
||||||
|
// Retrieve TODO information
|
||||||
|
|
||||||
|
defer req.Body.Close() |
||||||
|
|
||||||
|
// Authentication check
|
||||||
|
if !IsUserAuthorizedReq(req, s.db) { |
||||||
|
http.Error(w, "Authentication error", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check authentication information
|
||||||
|
if !IsUserAuthorizedReq(req, s.db) { |
||||||
|
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Get all user TODOs
|
||||||
|
todos, err := s.db.GetAllUserTodos(GetLoginFromAuth(req)) |
||||||
|
if err != nil { |
||||||
|
http.Error(w, "Failed to get TODOs", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Marshal to JSON
|
||||||
|
todosBytes, err := json.Marshal(&todos) |
||||||
|
if err != nil { |
||||||
|
http.Error(w, "Failed to marhsal TODOs JSON", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Send out
|
||||||
|
w.Header().Add("Content-Type", "application/json") |
||||||
|
w.Write(todosBytes) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) EndpointTodoGroupDelete(w http.ResponseWriter, req *http.Request) { |
||||||
|
// Delete an existing group
|
||||||
|
defer req.Body.Close() |
||||||
|
|
||||||
|
// Read body
|
||||||
|
body, err := io.ReadAll(req.Body) |
||||||
|
if err != nil { |
||||||
|
logger.Warning("[Server] Failed to read request body to possibly delete a TODO group: %s", err) |
||||||
|
http.Error(w, "Failed to read body", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Unmarshal JSON
|
||||||
|
var group db.TodoGroup |
||||||
|
err = json.Unmarshal(body, &group) |
||||||
|
if err != nil { |
||||||
|
logger.Warning("[Server] Received invalid TODO group JSON for deletion: %s", err) |
||||||
|
http.Error(w, "Invalid TODO group JSON", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check if given user actually owns this group
|
||||||
|
if !IsUserAuthorizedReq(req, s.db) { |
||||||
|
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if !s.db.DoesUserOwnGroup(group.ID, GetLoginFromAuth(req)) { |
||||||
|
http.Error(w, "You don't own this group", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// 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) |
||||||
|
http.Error(w, "Failed to delete TODO group", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Success!
|
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) EndpointTodoGroupCreate(w http.ResponseWriter, req *http.Request) { |
||||||
|
// Create a new TODO group
|
||||||
|
defer req.Body.Close() |
||||||
|
// Read body
|
||||||
|
body, err := io.ReadAll(req.Body) |
||||||
|
if err != nil { |
||||||
|
logger.Warning("[Server] Failed to read request body to create a new TODO group: %s", err) |
||||||
|
http.Error(w, "Failed to read body", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Unmarshal JSON
|
||||||
|
var newGroup db.TodoGroup |
||||||
|
err = json.Unmarshal(body, &newGroup) |
||||||
|
if err != nil { |
||||||
|
logger.Warning("[Server] Received invalid TODO group JSON for creation: %s", err) |
||||||
|
http.Error(w, "Invalid TODO group JSON", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check for authentication problems
|
||||||
|
if !IsUserAuthorizedReq(req, s.db) { |
||||||
|
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Add group to the database
|
||||||
|
newGroup.OwnerLogin = GetLoginFromAuth(req) |
||||||
|
newGroup.TimeCreatedUnix = uint64(time.Now().Unix()) |
||||||
|
err = s.db.CreateTodoGroup(newGroup) |
||||||
|
if err != nil { |
||||||
|
http.Error(w, "Failed to create TODO group", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Success!
|
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
logger.Info("[Server] Created a new TODO group for %s", newGroup.OwnerLogin) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) EndpointTodoGroupGet(w http.ResponseWriter, req *http.Request) { |
||||||
|
// Retrieve all todo groups
|
||||||
|
defer req.Body.Close() |
||||||
|
|
||||||
|
// Check authentication information
|
||||||
|
if !IsUserAuthorizedReq(req, s.db) { |
||||||
|
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Get groups
|
||||||
|
groups, err := s.db.GetAllUserTodoGroups(GetLoginFromAuth(req)) |
||||||
|
if err != nil { |
||||||
|
http.Error(w, "Failed to get TODO groups", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Marshal to JSON
|
||||||
|
groupBytes, err := json.Marshal(&groups) |
||||||
|
if err != nil { |
||||||
|
http.Error(w, "Failed to marhsal TODO groups JSON", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Send out
|
||||||
|
w.Header().Add("Content-Type", "application/json") |
||||||
|
w.Write(groupBytes) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) EndpointTodoGroupUpdate(w http.ResponseWriter, req *http.Request) { |
||||||
|
// Check authentication information
|
||||||
|
if !IsUserAuthorizedReq(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 group: %s", err) |
||||||
|
http.Error(w, "Failed to read body", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Unmarshal JSON
|
||||||
|
var group db.TodoGroup |
||||||
|
err = json.Unmarshal(body, &group) |
||||||
|
if err != nil { |
||||||
|
logger.Warning("[Server] Received invalid TODO group JSON in order to update: %s", err) |
||||||
|
http.Error(w, "Invalid group JSON", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// TODO
|
||||||
|
err = s.db.UpdateTodoGroup(group.ID, group) |
||||||
|
if err != nil { |
||||||
|
logger.Warning("[Server] Failed to update TODO group: %s", err) |
||||||
|
http.Error(w, "Failed to update", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
/* |
||||||
|
dela - web TODO list |
||||||
|
Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
|
||||||
|
package server |
||||||
|
|
||||||
|
import ( |
||||||
|
"html/template" |
||||||
|
"path/filepath" |
||||||
|
) |
||||||
|
|
||||||
|
// Constructs a pageName template via inserting basePageName in pagesDir
|
||||||
|
func getPage(pagesDir string, basePageName string, pageName string) (*template.Template, error) { |
||||||
|
page, err := template.ParseFiles( |
||||||
|
filepath.Join(pagesDir, basePageName), |
||||||
|
filepath.Join(pagesDir, pageName), |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return page, nil |
||||||
|
} |
@ -0,0 +1,178 @@ |
|||||||
|
/* |
||||||
|
dela - web TODO list |
||||||
|
Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
|
||||||
|
package server |
||||||
|
|
||||||
|
import ( |
||||||
|
"Unbewohnte/dela/conf" |
||||||
|
"Unbewohnte/dela/db" |
||||||
|
"Unbewohnte/dela/logger" |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
PagesDirName string = "pages" |
||||||
|
StaticDirName string = "static" |
||||||
|
ScriptsDirName string = "scripts" |
||||||
|
) |
||||||
|
|
||||||
|
type Server struct { |
||||||
|
config conf.Conf |
||||||
|
db *db.DB |
||||||
|
http http.Server |
||||||
|
} |
||||||
|
|
||||||
|
// Creates a new server instance with provided config
|
||||||
|
func New(config conf.Conf) (*Server, error) { |
||||||
|
var server Server = Server{} |
||||||
|
server.config = config |
||||||
|
|
||||||
|
// check if required directories are present
|
||||||
|
_, err := os.Stat(filepath.Join(config.BaseContentDir, PagesDirName)) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Server] A directory with HTML pages is not available: %s", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Join(config.BaseContentDir, ScriptsDirName)) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Server] A directory with scripts is not available: %s", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Join(config.BaseContentDir, StaticDirName)) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Server] A directory with static content is not available: %s", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// get database working
|
||||||
|
serverDB, err := db.FromFile(filepath.Join(config.BaseContentDir, config.ProdDBName)) |
||||||
|
if err != nil { |
||||||
|
// Create one then
|
||||||
|
serverDB, err = db.Create(filepath.Join(config.BaseContentDir, config.ProdDBName)) |
||||||
|
if err != nil { |
||||||
|
logger.Error("Failed to create a new database: %s", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
server.db = serverDB |
||||||
|
logger.Info("Opened a database successfully") |
||||||
|
|
||||||
|
// start constructing an http server configuration
|
||||||
|
server.http = http.Server{ |
||||||
|
Addr: fmt.Sprintf(":%d", server.config.Port), |
||||||
|
} |
||||||
|
|
||||||
|
// configure paths' callbacks
|
||||||
|
mux := http.NewServeMux() |
||||||
|
mux.Handle( |
||||||
|
"/static/", |
||||||
|
http.StripPrefix("/static/", http.FileServer( |
||||||
|
http.Dir(filepath.Join(server.config.BaseContentDir, StaticDirName))), |
||||||
|
), |
||||||
|
) |
||||||
|
|
||||||
|
mux.Handle( |
||||||
|
"/scripts/", |
||||||
|
http.StripPrefix("/scripts/", http.FileServer( |
||||||
|
http.Dir(filepath.Join(server.config.BaseContentDir, ScriptsDirName))), |
||||||
|
), |
||||||
|
) |
||||||
|
|
||||||
|
// handle page requests
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
switch req.URL.Path { |
||||||
|
case "/": |
||||||
|
requestedPage, err := getPage( |
||||||
|
filepath.Join(server.config.BaseContentDir, PagesDirName), "base.html", "index.html", |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
http.Redirect(w, req, "/about", http.StatusTemporaryRedirect) |
||||||
|
logger.Error("[Server][/] Failed to get a page: %s", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
requestedPage.ExecuteTemplate(w, "index.html", nil) |
||||||
|
default: |
||||||
|
requestedPage, err := getPage( |
||||||
|
filepath.Join(server.config.BaseContentDir, PagesDirName), |
||||||
|
"base.html", |
||||||
|
req.URL.Path[1:]+".html", |
||||||
|
) |
||||||
|
if err == nil { |
||||||
|
requestedPage.ExecuteTemplate(w, req.URL.Path[1:]+".html", nil) |
||||||
|
} else { |
||||||
|
http.Error(w, "Page processing error", http.StatusInternalServerError) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
mux.HandleFunc("/api/user/get", server.EndpointUserGet) // Non specific
|
||||||
|
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/get", server.EndpointUserTodosGet) // Non specific
|
||||||
|
mux.HandleFunc("/api/todo/delete/", server.EndpointTodoDelete) // Specific
|
||||||
|
mux.HandleFunc("/api/todo/update/", server.EndpointTodoUpdate) // Specific
|
||||||
|
mux.HandleFunc("/api/group/create", server.EndpointTodoGroupCreate) // Non specific
|
||||||
|
mux.HandleFunc("/api/group/get/", server.EndpointTodoGroupGet) // Specific
|
||||||
|
mux.HandleFunc("/api/group/update/", server.EndpointTodoGroupUpdate) // Specific
|
||||||
|
mux.HandleFunc("/api/group/delete/", server.EndpointTodoGroupDelete) // Specific
|
||||||
|
|
||||||
|
server.http.Handler = mux |
||||||
|
logger.Info("[Server] Created an HTTP server instance") |
||||||
|
|
||||||
|
return &server, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Launches server instance
|
||||||
|
func (s *Server) Start() error { |
||||||
|
if s.config.CertFilePath != "" && s.config.KeyFilePath != "" { |
||||||
|
logger.Info("[Server] Using TLS") |
||||||
|
logger.Info("[Server] HTTP server is going live on port %d!", s.config.Port) |
||||||
|
|
||||||
|
err := s.http.ListenAndServeTLS(s.config.CertFilePath, s.config.KeyFilePath) |
||||||
|
if err != nil && err != http.ErrServerClosed { |
||||||
|
logger.Error("[Server] Fatal server error: %s", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
} else { |
||||||
|
logger.Info("[Server] Not using TLS") |
||||||
|
logger.Info("[Server] HTTP server is going live on port %d!", s.config.Port) |
||||||
|
|
||||||
|
err := s.http.ListenAndServe() |
||||||
|
if err != nil && err != http.ErrServerClosed { |
||||||
|
logger.Error("[Server] Fatal server error: %s", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Stops the server immediately
|
||||||
|
func (s *Server) Stop() { |
||||||
|
ctx, cfunc := context.WithDeadline(context.Background(), time.Now().Add(time.Second*10)) |
||||||
|
s.http.Shutdown(ctx) |
||||||
|
cfunc() |
||||||
|
} |
@ -0,0 +1,86 @@ |
|||||||
|
/* |
||||||
|
dela - web TODO list |
||||||
|
Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/ |
||||||
|
|
||||||
|
package server |
||||||
|
|
||||||
|
import ( |
||||||
|
"Unbewohnte/dela/db" |
||||||
|
"net/http" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
MinimalLoginLength uint = 3 |
||||||
|
MinimalUsernameLength uint = 3 |
||||||
|
MinimalPasswordLength uint = 5 |
||||||
|
) |
||||||
|
|
||||||
|
// Check if user is valid. Returns false and a reason-string if not
|
||||||
|
func IsUserValid(user db.User) (bool, string) { |
||||||
|
if uint(len(user.Login)) < MinimalLoginLength { |
||||||
|
return false, "Login is too small" |
||||||
|
} |
||||||
|
|
||||||
|
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!
|
||||||
|
return false, "Password has a non printable ASCII character" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return true, "" |
||||||
|
} |
||||||
|
|
||||||
|
// Checks if such a user exists and compares passwords. Returns true if such user exists and passwords do match
|
||||||
|
func IsUserAuthorized(db *db.DB, user db.User) bool { |
||||||
|
userDB, err := db.GetUser(user.Login) |
||||||
|
if err != nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if userDB.Password != user.Password { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
return IsUserAuthorized(dbase, db.User{ |
||||||
|
Login: login, |
||||||
|
Password: password, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func GetLoginFromAuth(req *http.Request) string { |
||||||
|
login, _, _ := req.BasicAuth() |
||||||
|
return login |
||||||
|
} |
@ -0,0 +1,593 @@ |
|||||||
|
/*! |
||||||
|
* Bootstrap Reboot v5.3.0 (https://getbootstrap.com/) |
||||||
|
* Copyright 2011-2023 The Bootstrap Authors |
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
||||||
|
*/ |
||||||
|
:root, |
||||||
|
[data-bs-theme=light] { |
||||||
|
--bs-blue: #0d6efd; |
||||||
|
--bs-indigo: #6610f2; |
||||||
|
--bs-purple: #6f42c1; |
||||||
|
--bs-pink: #d63384; |
||||||
|
--bs-red: #dc3545; |
||||||
|
--bs-orange: #fd7e14; |
||||||
|
--bs-yellow: #ffc107; |
||||||
|
--bs-green: #198754; |
||||||
|
--bs-teal: #20c997; |
||||||
|
--bs-cyan: #0dcaf0; |
||||||
|
--bs-black: #000; |
||||||
|
--bs-white: #fff; |
||||||
|
--bs-gray: #6c757d; |
||||||
|
--bs-gray-dark: #343a40; |
||||||
|
--bs-gray-100: #f8f9fa; |
||||||
|
--bs-gray-200: #e9ecef; |
||||||
|
--bs-gray-300: #dee2e6; |
||||||
|
--bs-gray-400: #ced4da; |
||||||
|
--bs-gray-500: #adb5bd; |
||||||
|
--bs-gray-600: #6c757d; |
||||||
|
--bs-gray-700: #495057; |
||||||
|
--bs-gray-800: #343a40; |
||||||
|
--bs-gray-900: #212529; |
||||||
|
--bs-primary: #0d6efd; |
||||||
|
--bs-secondary: #6c757d; |
||||||
|
--bs-success: #198754; |
||||||
|
--bs-info: #0dcaf0; |
||||||
|
--bs-warning: #ffc107; |
||||||
|
--bs-danger: #dc3545; |
||||||
|
--bs-light: #f8f9fa; |
||||||
|
--bs-dark: #212529; |
||||||
|
--bs-primary-rgb: 13, 110, 253; |
||||||
|
--bs-secondary-rgb: 108, 117, 125; |
||||||
|
--bs-success-rgb: 25, 135, 84; |
||||||
|
--bs-info-rgb: 13, 202, 240; |
||||||
|
--bs-warning-rgb: 255, 193, 7; |
||||||
|
--bs-danger-rgb: 220, 53, 69; |
||||||
|
--bs-light-rgb: 248, 249, 250; |
||||||
|
--bs-dark-rgb: 33, 37, 41; |
||||||
|
--bs-primary-text-emphasis: #052c65; |
||||||
|
--bs-secondary-text-emphasis: #2b2f32; |
||||||
|
--bs-success-text-emphasis: #0a3622; |
||||||
|
--bs-info-text-emphasis: #055160; |
||||||
|
--bs-warning-text-emphasis: #664d03; |
||||||
|
--bs-danger-text-emphasis: #58151c; |
||||||
|
--bs-light-text-emphasis: #495057; |
||||||
|
--bs-dark-text-emphasis: #495057; |
||||||
|
--bs-primary-bg-subtle: #cfe2ff; |
||||||
|
--bs-secondary-bg-subtle: #e2e3e5; |
||||||
|
--bs-success-bg-subtle: #d1e7dd; |
||||||
|
--bs-info-bg-subtle: #cff4fc; |
||||||
|
--bs-warning-bg-subtle: #fff3cd; |
||||||
|
--bs-danger-bg-subtle: #f8d7da; |
||||||
|
--bs-light-bg-subtle: #fcfcfd; |
||||||
|
--bs-dark-bg-subtle: #ced4da; |
||||||
|
--bs-primary-border-subtle: #9ec5fe; |
||||||
|
--bs-secondary-border-subtle: #c4c8cb; |
||||||
|
--bs-success-border-subtle: #a3cfbb; |
||||||
|
--bs-info-border-subtle: #9eeaf9; |
||||||
|
--bs-warning-border-subtle: #ffe69c; |
||||||
|
--bs-danger-border-subtle: #f1aeb5; |
||||||
|
--bs-light-border-subtle: #e9ecef; |
||||||
|
--bs-dark-border-subtle: #adb5bd; |
||||||
|
--bs-white-rgb: 255, 255, 255; |
||||||
|
--bs-black-rgb: 0, 0, 0; |
||||||
|
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; |
||||||
|
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; |
||||||
|
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); |
||||||
|
--bs-body-font-family: var(--bs-font-sans-serif); |
||||||
|
--bs-body-font-size: 1rem; |
||||||
|
--bs-body-font-weight: 400; |
||||||
|
--bs-body-line-height: 1.5; |
||||||
|
--bs-body-color: #212529; |
||||||
|
--bs-body-color-rgb: 33, 37, 41; |
||||||
|
--bs-body-bg: #fff; |
||||||
|
--bs-body-bg-rgb: 255, 255, 255; |
||||||
|
--bs-emphasis-color: #000; |
||||||
|
--bs-emphasis-color-rgb: 0, 0, 0; |
||||||
|
--bs-secondary-color: rgba(33, 37, 41, 0.75); |
||||||
|
--bs-secondary-color-rgb: 33, 37, 41; |
||||||
|
--bs-secondary-bg: #e9ecef; |
||||||
|
--bs-secondary-bg-rgb: 233, 236, 239; |
||||||
|
--bs-tertiary-color: rgba(33, 37, 41, 0.5); |
||||||
|
--bs-tertiary-color-rgb: 33, 37, 41; |
||||||
|
--bs-tertiary-bg: #f8f9fa; |
||||||
|
--bs-tertiary-bg-rgb: 248, 249, 250; |
||||||
|
--bs-heading-color: inherit; |
||||||
|
--bs-link-color: #0d6efd; |
||||||
|
--bs-link-color-rgb: 13, 110, 253; |
||||||
|
--bs-link-decoration: underline; |
||||||
|
--bs-link-hover-color: #0a58ca; |
||||||
|
--bs-link-hover-color-rgb: 10, 88, 202; |
||||||
|
--bs-code-color: #d63384; |
||||||
|
--bs-highlight-bg: #fff3cd; |
||||||
|
--bs-border-width: 1px; |
||||||
|
--bs-border-style: solid; |
||||||
|
--bs-border-color: #dee2e6; |
||||||
|
--bs-border-color-translucent: rgba(0, 0, 0, 0.175); |
||||||
|
--bs-border-radius: 0.375rem; |
||||||
|
--bs-border-radius-sm: 0.25rem; |
||||||
|
--bs-border-radius-lg: 0.5rem; |
||||||
|
--bs-border-radius-xl: 1rem; |
||||||
|
--bs-border-radius-xxl: 2rem; |
||||||
|
--bs-border-radius-2xl: var(--bs-border-radius-xxl); |
||||||
|
--bs-border-radius-pill: 50rem; |
||||||
|
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); |
||||||
|
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); |
||||||
|
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); |
||||||
|
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); |
||||||
|
--bs-focus-ring-width: 0.25rem; |
||||||
|
--bs-focus-ring-opacity: 0.25; |
||||||
|
--bs-focus-ring-color: rgba(13, 110, 253, 0.25); |
||||||
|
--bs-form-valid-color: #198754; |
||||||
|
--bs-form-valid-border-color: #198754; |
||||||
|
--bs-form-invalid-color: #dc3545; |
||||||
|
--bs-form-invalid-border-color: #dc3545; |
||||||
|
} |
||||||
|
|
||||||
|
[data-bs-theme=dark] { |
||||||
|
color-scheme: dark; |
||||||
|
--bs-body-color: #adb5bd; |
||||||
|
--bs-body-color-rgb: 173, 181, 189; |
||||||
|
--bs-body-bg: #212529; |
||||||
|
--bs-body-bg-rgb: 33, 37, 41; |
||||||
|
--bs-emphasis-color: #fff; |
||||||
|
--bs-emphasis-color-rgb: 255, 255, 255; |
||||||
|
--bs-secondary-color: rgba(173, 181, 189, 0.75); |
||||||
|
--bs-secondary-color-rgb: 173, 181, 189; |
||||||
|
--bs-secondary-bg: #343a40; |
||||||
|
--bs-secondary-bg-rgb: 52, 58, 64; |
||||||
|
--bs-tertiary-color: rgba(173, 181, 189, 0.5); |
||||||
|
--bs-tertiary-color-rgb: 173, 181, 189; |
||||||
|
--bs-tertiary-bg: #2b3035; |
||||||
|
--bs-tertiary-bg-rgb: 43, 48, 53; |
||||||
|
--bs-primary-text-emphasis: #6ea8fe; |
||||||
|
--bs-secondary-text-emphasis: #a7acb1; |
||||||
|
--bs-success-text-emphasis: #75b798; |
||||||
|
--bs-info-text-emphasis: #6edff6; |
||||||
|
--bs-warning-text-emphasis: #ffda6a; |
||||||
|
--bs-danger-text-emphasis: #ea868f; |
||||||
|
--bs-light-text-emphasis: #f8f9fa; |
||||||
|
--bs-dark-text-emphasis: #dee2e6; |
||||||
|
--bs-primary-bg-subtle: #031633; |
||||||
|
--bs-secondary-bg-subtle: #161719; |
||||||
|
--bs-success-bg-subtle: #051b11; |
||||||
|
--bs-info-bg-subtle: #032830; |
||||||
|
--bs-warning-bg-subtle: #332701; |
||||||
|
--bs-danger-bg-subtle: #2c0b0e; |
||||||
|
--bs-light-bg-subtle: #343a40; |
||||||
|
--bs-dark-bg-subtle: #1a1d20; |
||||||
|
--bs-primary-border-subtle: #084298; |
||||||
|
--bs-secondary-border-subtle: #41464b; |
||||||
|
--bs-success-border-subtle: #0f5132; |
||||||
|
--bs-info-border-subtle: #087990; |
||||||
|
--bs-warning-border-subtle: #997404; |
||||||
|
--bs-danger-border-subtle: #842029; |
||||||
|
--bs-light-border-subtle: #495057; |
||||||
|
--bs-dark-border-subtle: #343a40; |
||||||
|
--bs-heading-color: inherit; |
||||||
|
--bs-link-color: #6ea8fe; |
||||||
|
--bs-link-hover-color: #8bb9fe; |
||||||
|
--bs-link-color-rgb: 110, 168, 254; |
||||||
|
--bs-link-hover-color-rgb: 139, 185, 254; |
||||||
|
--bs-code-color: #e685b5; |
||||||
|
--bs-border-color: #495057; |
||||||
|
--bs-border-color-translucent: rgba(255, 255, 255, 0.15); |
||||||
|
--bs-form-valid-color: #75b798; |
||||||
|
--bs-form-valid-border-color: #75b798; |
||||||
|
--bs-form-invalid-color: #ea868f; |
||||||
|
--bs-form-invalid-border-color: #ea868f; |
||||||
|
} |
||||||
|
|
||||||
|
*, |
||||||
|
*::before, |
||||||
|
*::after { |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) { |
||||||
|
:root { |
||||||
|
scroll-behavior: smooth; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
font-family: var(--bs-body-font-family); |
||||||
|
font-size: var(--bs-body-font-size); |
||||||
|
font-weight: var(--bs-body-font-weight); |
||||||
|
line-height: var(--bs-body-line-height); |
||||||
|
color: var(--bs-body-color); |
||||||
|
text-align: var(--bs-body-text-align); |
||||||
|
background-color: var(--bs-body-bg); |
||||||
|
-webkit-text-size-adjust: 100%; |
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); |
||||||
|
} |
||||||
|
|
||||||
|
hr { |
||||||
|
margin: 1rem 0; |
||||||
|
color: inherit; |
||||||
|
border: 0; |
||||||
|
border-top: var(--bs-border-width) solid; |
||||||
|
opacity: 0.25; |
||||||
|
} |
||||||
|
|
||||||
|
h6, h5, h4, h3, h2, h1 { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
font-weight: 500; |
||||||
|
line-height: 1.2; |
||||||
|
color: var(--bs-heading-color); |
||||||
|
} |
||||||
|
|
||||||
|
h1 { |
||||||
|
font-size: calc(1.375rem + 1.5vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h1 { |
||||||
|
font-size: 2.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h2 { |
||||||
|
font-size: calc(1.325rem + 0.9vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h2 { |
||||||
|
font-size: 2rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h3 { |
||||||
|
font-size: calc(1.3rem + 0.6vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h3 { |
||||||
|
font-size: 1.75rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h4 { |
||||||
|
font-size: calc(1.275rem + 0.3vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h4 { |
||||||
|
font-size: 1.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h5 { |
||||||
|
font-size: 1.25rem; |
||||||
|
} |
||||||
|
|
||||||
|
h6 { |
||||||
|
font-size: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
p { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
abbr[title] { |
||||||
|
-webkit-text-decoration: underline dotted; |
||||||
|
text-decoration: underline dotted; |
||||||
|
cursor: help; |
||||||
|
-webkit-text-decoration-skip-ink: none; |
||||||
|
text-decoration-skip-ink: none; |
||||||
|
} |
||||||
|
|
||||||
|
address { |
||||||
|
margin-bottom: 1rem; |
||||||
|
font-style: normal; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
ol, |
||||||
|
ul { |
||||||
|
padding-left: 2rem; |
||||||
|
} |
||||||
|
|
||||||
|
ol, |
||||||
|
ul, |
||||||
|
dl { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
ol ol, |
||||||
|
ul ul, |
||||||
|
ol ul, |
||||||
|
ul ol { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
dt { |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
|
||||||
|
dd { |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
margin-left: 0; |
||||||
|
} |
||||||
|
|
||||||
|
blockquote { |
||||||
|
margin: 0 0 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
b, |
||||||
|
strong { |
||||||
|
font-weight: bolder; |
||||||
|
} |
||||||
|
|
||||||
|
small { |
||||||
|
font-size: 0.875em; |
||||||
|
} |
||||||
|
|
||||||
|
mark { |
||||||
|
padding: 0.1875em; |
||||||
|
background-color: var(--bs-highlight-bg); |
||||||
|
} |
||||||
|
|
||||||
|
sub, |
||||||
|
sup { |
||||||
|
position: relative; |
||||||
|
font-size: 0.75em; |
||||||
|
line-height: 0; |
||||||
|
vertical-align: baseline; |
||||||
|
} |
||||||
|
|
||||||
|
sub { |
||||||
|
bottom: -0.25em; |
||||||
|
} |
||||||
|
|
||||||
|
sup { |
||||||
|
top: -0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
a { |
||||||
|
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
a:hover { |
||||||
|
--bs-link-color-rgb: var(--bs-link-hover-color-rgb); |
||||||
|
} |
||||||
|
|
||||||
|
a:not([href]):not([class]), a:not([href]):not([class]):hover { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
pre, |
||||||
|
code, |
||||||
|
kbd, |
||||||
|
samp { |
||||||
|
font-family: var(--bs-font-monospace); |
||||||
|
font-size: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
pre { |
||||||
|
display: block; |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
overflow: auto; |
||||||
|
font-size: 0.875em; |
||||||
|
} |
||||||
|
pre code { |
||||||
|
font-size: inherit; |
||||||
|
color: inherit; |
||||||
|
word-break: normal; |
||||||
|
} |
||||||
|
|
||||||
|
code { |
||||||
|
font-size: 0.875em; |
||||||
|
color: var(--bs-code-color); |
||||||
|
word-wrap: break-word; |
||||||
|
} |
||||||
|
a > code { |
||||||
|
color: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
kbd { |
||||||
|
padding: 0.1875rem 0.375rem; |
||||||
|
font-size: 0.875em; |
||||||
|
color: var(--bs-body-bg); |
||||||
|
background-color: var(--bs-body-color); |
||||||
|
border-radius: 0.25rem; |
||||||
|
} |
||||||
|
kbd kbd { |
||||||
|
padding: 0; |
||||||
|
font-size: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
figure { |
||||||
|
margin: 0 0 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
img, |
||||||
|
svg { |
||||||
|
vertical-align: middle; |
||||||
|
} |
||||||
|
|
||||||
|
table { |
||||||
|
caption-side: bottom; |
||||||
|
border-collapse: collapse; |
||||||
|
} |
||||||
|
|
||||||
|
caption { |
||||||
|
padding-top: 0.5rem; |
||||||
|
padding-bottom: 0.5rem; |
||||||
|
color: var(--bs-secondary-color); |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
|
||||||
|
th { |
||||||
|
text-align: inherit; |
||||||
|
text-align: -webkit-match-parent; |
||||||
|
} |
||||||
|
|
||||||
|
thead, |
||||||
|
tbody, |
||||||
|
tfoot, |
||||||
|
tr, |
||||||
|
td, |
||||||
|
th { |
||||||
|
border-color: inherit; |
||||||
|
border-style: solid; |
||||||
|
border-width: 0; |
||||||
|
} |
||||||
|
|
||||||
|
label { |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
button { |
||||||
|
border-radius: 0; |
||||||
|
} |
||||||
|
|
||||||
|
button:focus:not(:focus-visible) { |
||||||
|
outline: 0; |
||||||
|
} |
||||||
|
|
||||||
|
input, |
||||||
|
button, |
||||||
|
select, |
||||||
|
optgroup, |
||||||
|
textarea { |
||||||
|
margin: 0; |
||||||
|
font-family: inherit; |
||||||
|
font-size: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
button, |
||||||
|
select { |
||||||
|
text-transform: none; |
||||||
|
} |
||||||
|
|
||||||
|
[role=button] { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
select { |
||||||
|
word-wrap: normal; |
||||||
|
} |
||||||
|
select:disabled { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { |
||||||
|
display: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
button, |
||||||
|
[type=button], |
||||||
|
[type=reset], |
||||||
|
[type=submit] { |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
button:not(:disabled), |
||||||
|
[type=button]:not(:disabled), |
||||||
|
[type=reset]:not(:disabled), |
||||||
|
[type=submit]:not(:disabled) { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
::-moz-focus-inner { |
||||||
|
padding: 0; |
||||||
|
border-style: none; |
||||||
|
} |
||||||
|
|
||||||
|
textarea { |
||||||
|
resize: vertical; |
||||||
|
} |
||||||
|
|
||||||
|
fieldset { |
||||||
|
min-width: 0; |
||||||
|
padding: 0; |
||||||
|
margin: 0; |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
|
||||||
|
legend { |
||||||
|
float: left; |
||||||
|
width: 100%; |
||||||
|
padding: 0; |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
font-size: calc(1.275rem + 0.3vw); |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
legend { |
||||||
|
font-size: 1.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
legend + * { |
||||||
|
clear: left; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-datetime-edit-fields-wrapper, |
||||||
|
::-webkit-datetime-edit-text, |
||||||
|
::-webkit-datetime-edit-minute, |
||||||
|
::-webkit-datetime-edit-hour-field, |
||||||
|
::-webkit-datetime-edit-day-field, |
||||||
|
::-webkit-datetime-edit-month-field, |
||||||
|
::-webkit-datetime-edit-year-field { |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-inner-spin-button { |
||||||
|
height: auto; |
||||||
|
} |
||||||
|
|
||||||
|
[type=search] { |
||||||
|
outline-offset: -2px; |
||||||
|
-webkit-appearance: textfield; |
||||||
|
} |
||||||
|
|
||||||
|
/* rtl:raw: |
||||||
|
[type="tel"], |
||||||
|
[type="url"], |
||||||
|
[type="email"], |
||||||
|
[type="number"] { |
||||||
|
direction: ltr; |
||||||
|
} |
||||||
|
*/ |
||||||
|
::-webkit-search-decoration { |
||||||
|
-webkit-appearance: none; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-color-swatch-wrapper { |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-file-upload-button { |
||||||
|
font: inherit; |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
|
||||||
|
::file-selector-button { |
||||||
|
font: inherit; |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
|
||||||
|
output { |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
iframe { |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
|
||||||
|
summary { |
||||||
|
display: list-item; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
progress { |
||||||
|
vertical-align: baseline; |
||||||
|
} |
||||||
|
|
||||||
|
[hidden] { |
||||||
|
display: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.css.map */ |
@ -0,0 +1,590 @@ |
|||||||
|
/*! |
||||||
|
* Bootstrap Reboot v5.3.0 (https://getbootstrap.com/) |
||||||
|
* Copyright 2011-2023 The Bootstrap Authors |
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
||||||
|
*/ |
||||||
|
:root, |
||||||
|
[data-bs-theme=light] { |
||||||
|
--bs-blue: #0d6efd; |
||||||
|
--bs-indigo: #6610f2; |
||||||
|
--bs-purple: #6f42c1; |
||||||
|
--bs-pink: #d63384; |
||||||
|
--bs-red: #dc3545; |
||||||
|
--bs-orange: #fd7e14; |
||||||
|
--bs-yellow: #ffc107; |
||||||
|
--bs-green: #198754; |
||||||
|
--bs-teal: #20c997; |
||||||
|
--bs-cyan: #0dcaf0; |
||||||
|
--bs-black: #000; |
||||||
|
--bs-white: #fff; |
||||||
|
--bs-gray: #6c757d; |
||||||
|
--bs-gray-dark: #343a40; |
||||||
|
--bs-gray-100: #f8f9fa; |
||||||
|
--bs-gray-200: #e9ecef; |
||||||
|
--bs-gray-300: #dee2e6; |
||||||
|
--bs-gray-400: #ced4da; |
||||||
|
--bs-gray-500: #adb5bd; |
||||||
|
--bs-gray-600: #6c757d; |
||||||
|
--bs-gray-700: #495057; |
||||||
|
--bs-gray-800: #343a40; |
||||||
|
--bs-gray-900: #212529; |
||||||
|
--bs-primary: #0d6efd; |
||||||
|
--bs-secondary: #6c757d; |
||||||
|
--bs-success: #198754; |
||||||
|
--bs-info: #0dcaf0; |
||||||
|
--bs-warning: #ffc107; |
||||||
|
--bs-danger: #dc3545; |
||||||
|
--bs-light: #f8f9fa; |
||||||
|
--bs-dark: #212529; |
||||||
|
--bs-primary-rgb: 13, 110, 253; |
||||||
|
--bs-secondary-rgb: 108, 117, 125; |
||||||
|
--bs-success-rgb: 25, 135, 84; |
||||||
|
--bs-info-rgb: 13, 202, 240; |
||||||
|
--bs-warning-rgb: 255, 193, 7; |
||||||
|
--bs-danger-rgb: 220, 53, 69; |
||||||
|
--bs-light-rgb: 248, 249, 250; |
||||||
|
--bs-dark-rgb: 33, 37, 41; |
||||||
|
--bs-primary-text-emphasis: #052c65; |
||||||
|
--bs-secondary-text-emphasis: #2b2f32; |
||||||
|
--bs-success-text-emphasis: #0a3622; |
||||||
|
--bs-info-text-emphasis: #055160; |
||||||
|
--bs-warning-text-emphasis: #664d03; |
||||||
|
--bs-danger-text-emphasis: #58151c; |
||||||
|
--bs-light-text-emphasis: #495057; |
||||||
|
--bs-dark-text-emphasis: #495057; |
||||||
|
--bs-primary-bg-subtle: #cfe2ff; |
||||||
|
--bs-secondary-bg-subtle: #e2e3e5; |
||||||
|
--bs-success-bg-subtle: #d1e7dd; |
||||||
|
--bs-info-bg-subtle: #cff4fc; |
||||||
|
--bs-warning-bg-subtle: #fff3cd; |
||||||
|
--bs-danger-bg-subtle: #f8d7da; |
||||||
|
--bs-light-bg-subtle: #fcfcfd; |
||||||
|
--bs-dark-bg-subtle: #ced4da; |
||||||
|
--bs-primary-border-subtle: #9ec5fe; |
||||||
|
--bs-secondary-border-subtle: #c4c8cb; |
||||||
|
--bs-success-border-subtle: #a3cfbb; |
||||||
|
--bs-info-border-subtle: #9eeaf9; |
||||||
|
--bs-warning-border-subtle: #ffe69c; |
||||||
|
--bs-danger-border-subtle: #f1aeb5; |
||||||
|
--bs-light-border-subtle: #e9ecef; |
||||||
|
--bs-dark-border-subtle: #adb5bd; |
||||||
|
--bs-white-rgb: 255, 255, 255; |
||||||
|
--bs-black-rgb: 0, 0, 0; |
||||||
|
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; |
||||||
|
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; |
||||||
|
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); |
||||||
|
--bs-body-font-family: var(--bs-font-sans-serif); |
||||||
|
--bs-body-font-size: 1rem; |
||||||
|
--bs-body-font-weight: 400; |
||||||
|
--bs-body-line-height: 1.5; |
||||||
|
--bs-body-color: #212529; |
||||||
|
--bs-body-color-rgb: 33, 37, 41; |
||||||
|
--bs-body-bg: #fff; |
||||||
|
--bs-body-bg-rgb: 255, 255, 255; |
||||||
|
--bs-emphasis-color: #000; |
||||||
|
--bs-emphasis-color-rgb: 0, 0, 0; |
||||||
|
--bs-secondary-color: rgba(33, 37, 41, 0.75); |
||||||
|
--bs-secondary-color-rgb: 33, 37, 41; |
||||||
|
--bs-secondary-bg: #e9ecef; |
||||||
|
--bs-secondary-bg-rgb: 233, 236, 239; |
||||||
|
--bs-tertiary-color: rgba(33, 37, 41, 0.5); |
||||||
|
--bs-tertiary-color-rgb: 33, 37, 41; |
||||||
|
--bs-tertiary-bg: #f8f9fa; |
||||||
|
--bs-tertiary-bg-rgb: 248, 249, 250; |
||||||
|
--bs-heading-color: inherit; |
||||||
|
--bs-link-color: #0d6efd; |
||||||
|
--bs-link-color-rgb: 13, 110, 253; |
||||||
|
--bs-link-decoration: underline; |
||||||
|
--bs-link-hover-color: #0a58ca; |
||||||
|
--bs-link-hover-color-rgb: 10, 88, 202; |
||||||
|
--bs-code-color: #d63384; |
||||||
|
--bs-highlight-bg: #fff3cd; |
||||||
|
--bs-border-width: 1px; |
||||||
|
--bs-border-style: solid; |
||||||
|
--bs-border-color: #dee2e6; |
||||||
|
--bs-border-color-translucent: rgba(0, 0, 0, 0.175); |
||||||
|
--bs-border-radius: 0.375rem; |
||||||
|
--bs-border-radius-sm: 0.25rem; |
||||||
|
--bs-border-radius-lg: 0.5rem; |
||||||
|
--bs-border-radius-xl: 1rem; |
||||||
|
--bs-border-radius-xxl: 2rem; |
||||||
|
--bs-border-radius-2xl: var(--bs-border-radius-xxl); |
||||||
|
--bs-border-radius-pill: 50rem; |
||||||
|
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); |
||||||
|
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); |
||||||
|
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); |
||||||
|
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); |
||||||
|
--bs-focus-ring-width: 0.25rem; |
||||||
|
--bs-focus-ring-opacity: 0.25; |
||||||
|
--bs-focus-ring-color: rgba(13, 110, 253, 0.25); |
||||||
|
--bs-form-valid-color: #198754; |
||||||
|
--bs-form-valid-border-color: #198754; |
||||||
|
--bs-form-invalid-color: #dc3545; |
||||||
|
--bs-form-invalid-border-color: #dc3545; |
||||||
|
} |
||||||
|
|
||||||
|
[data-bs-theme=dark] { |
||||||
|
color-scheme: dark; |
||||||
|
--bs-body-color: #adb5bd; |
||||||
|
--bs-body-color-rgb: 173, 181, 189; |
||||||
|
--bs-body-bg: #212529; |
||||||
|
--bs-body-bg-rgb: 33, 37, 41; |
||||||
|
--bs-emphasis-color: #fff; |
||||||
|
--bs-emphasis-color-rgb: 255, 255, 255; |
||||||
|
--bs-secondary-color: rgba(173, 181, 189, 0.75); |
||||||
|
--bs-secondary-color-rgb: 173, 181, 189; |
||||||
|
--bs-secondary-bg: #343a40; |
||||||
|
--bs-secondary-bg-rgb: 52, 58, 64; |
||||||
|
--bs-tertiary-color: rgba(173, 181, 189, 0.5); |
||||||
|
--bs-tertiary-color-rgb: 173, 181, 189; |
||||||
|
--bs-tertiary-bg: #2b3035; |
||||||
|
--bs-tertiary-bg-rgb: 43, 48, 53; |
||||||
|
--bs-primary-text-emphasis: #6ea8fe; |
||||||
|
--bs-secondary-text-emphasis: #a7acb1; |
||||||
|
--bs-success-text-emphasis: #75b798; |
||||||
|
--bs-info-text-emphasis: #6edff6; |
||||||
|
--bs-warning-text-emphasis: #ffda6a; |
||||||
|
--bs-danger-text-emphasis: #ea868f; |
||||||
|
--bs-light-text-emphasis: #f8f9fa; |
||||||
|
--bs-dark-text-emphasis: #dee2e6; |
||||||
|
--bs-primary-bg-subtle: #031633; |
||||||
|
--bs-secondary-bg-subtle: #161719; |
||||||
|
--bs-success-bg-subtle: #051b11; |
||||||
|
--bs-info-bg-subtle: #032830; |
||||||
|
--bs-warning-bg-subtle: #332701; |
||||||
|
--bs-danger-bg-subtle: #2c0b0e; |
||||||
|
--bs-light-bg-subtle: #343a40; |
||||||
|
--bs-dark-bg-subtle: #1a1d20; |
||||||
|
--bs-primary-border-subtle: #084298; |
||||||
|
--bs-secondary-border-subtle: #41464b; |
||||||
|
--bs-success-border-subtle: #0f5132; |
||||||
|
--bs-info-border-subtle: #087990; |
||||||
|
--bs-warning-border-subtle: #997404; |
||||||
|
--bs-danger-border-subtle: #842029; |
||||||
|
--bs-light-border-subtle: #495057; |
||||||
|
--bs-dark-border-subtle: #343a40; |
||||||
|
--bs-heading-color: inherit; |
||||||
|
--bs-link-color: #6ea8fe; |
||||||
|
--bs-link-hover-color: #8bb9fe; |
||||||
|
--bs-link-color-rgb: 110, 168, 254; |
||||||
|
--bs-link-hover-color-rgb: 139, 185, 254; |
||||||
|
--bs-code-color: #e685b5; |
||||||
|
--bs-border-color: #495057; |
||||||
|
--bs-border-color-translucent: rgba(255, 255, 255, 0.15); |
||||||
|
--bs-form-valid-color: #75b798; |
||||||
|
--bs-form-valid-border-color: #75b798; |
||||||
|
--bs-form-invalid-color: #ea868f; |
||||||
|
--bs-form-invalid-border-color: #ea868f; |
||||||
|
} |
||||||
|
|
||||||
|
*, |
||||||
|
*::before, |
||||||
|
*::after { |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) { |
||||||
|
:root { |
||||||
|
scroll-behavior: smooth; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
font-family: var(--bs-body-font-family); |
||||||
|
font-size: var(--bs-body-font-size); |
||||||
|
font-weight: var(--bs-body-font-weight); |
||||||
|
line-height: var(--bs-body-line-height); |
||||||
|
color: var(--bs-body-color); |
||||||
|
text-align: var(--bs-body-text-align); |
||||||
|
background-color: var(--bs-body-bg); |
||||||
|
-webkit-text-size-adjust: 100%; |
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); |
||||||
|
} |
||||||
|
|
||||||
|
hr { |
||||||
|
margin: 1rem 0; |
||||||
|
color: inherit; |
||||||
|
border: 0; |
||||||
|
border-top: var(--bs-border-width) solid; |
||||||
|
opacity: 0.25; |
||||||
|
} |
||||||
|
|
||||||
|
h6, h5, h4, h3, h2, h1 { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
font-weight: 500; |
||||||
|
line-height: 1.2; |
||||||
|
color: var(--bs-heading-color); |
||||||
|
} |
||||||
|
|
||||||
|
h1 { |
||||||
|
font-size: calc(1.375rem + 1.5vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h1 { |
||||||
|
font-size: 2.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h2 { |
||||||
|
font-size: calc(1.325rem + 0.9vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h2 { |
||||||
|
font-size: 2rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h3 { |
||||||
|
font-size: calc(1.3rem + 0.6vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h3 { |
||||||
|
font-size: 1.75rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h4 { |
||||||
|
font-size: calc(1.275rem + 0.3vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h4 { |
||||||
|
font-size: 1.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h5 { |
||||||
|
font-size: 1.25rem; |
||||||
|
} |
||||||
|
|
||||||
|
h6 { |
||||||
|
font-size: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
p { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
abbr[title] { |
||||||
|
-webkit-text-decoration: underline dotted; |
||||||
|
text-decoration: underline dotted; |
||||||
|
cursor: help; |
||||||
|
-webkit-text-decoration-skip-ink: none; |
||||||
|
text-decoration-skip-ink: none; |
||||||
|
} |
||||||
|
|
||||||
|
address { |
||||||
|
margin-bottom: 1rem; |
||||||
|
font-style: normal; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
ol, |
||||||
|
ul { |
||||||
|
padding-right: 2rem; |
||||||
|
} |
||||||
|
|
||||||
|
ol, |
||||||
|
ul, |
||||||
|
dl { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
ol ol, |
||||||
|
ul ul, |
||||||
|
ol ul, |
||||||
|
ul ol { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
dt { |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
|
||||||
|
dd { |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
margin-right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
blockquote { |
||||||
|
margin: 0 0 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
b, |
||||||
|
strong { |
||||||
|
font-weight: bolder; |
||||||
|
} |
||||||
|
|
||||||
|
small { |
||||||
|
font-size: 0.875em; |
||||||
|
} |
||||||
|
|
||||||
|
mark { |
||||||
|
padding: 0.1875em; |
||||||
|
background-color: var(--bs-highlight-bg); |
||||||
|
} |
||||||
|
|
||||||
|
sub, |
||||||
|
sup { |
||||||
|
position: relative; |
||||||
|
font-size: 0.75em; |
||||||
|
line-height: 0; |
||||||
|
vertical-align: baseline; |
||||||
|
} |
||||||
|
|
||||||
|
sub { |
||||||
|
bottom: -0.25em; |
||||||
|
} |
||||||
|
|
||||||
|
sup { |
||||||
|
top: -0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
a { |
||||||
|
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
a:hover { |
||||||
|
--bs-link-color-rgb: var(--bs-link-hover-color-rgb); |
||||||
|
} |
||||||
|
|
||||||
|
a:not([href]):not([class]), a:not([href]):not([class]):hover { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
pre, |
||||||
|
code, |
||||||
|
kbd, |
||||||
|
samp { |
||||||
|
font-family: var(--bs-font-monospace); |
||||||
|
font-size: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
pre { |
||||||
|
display: block; |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
overflow: auto; |
||||||
|
font-size: 0.875em; |
||||||
|
} |
||||||
|
pre code { |
||||||
|
font-size: inherit; |
||||||
|
color: inherit; |
||||||
|
word-break: normal; |
||||||
|
} |
||||||
|
|
||||||
|
code { |
||||||
|
font-size: 0.875em; |
||||||
|
color: var(--bs-code-color); |
||||||
|
word-wrap: break-word; |
||||||
|
} |
||||||
|
a > code { |
||||||
|
color: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
kbd { |
||||||
|
padding: 0.1875rem 0.375rem; |
||||||
|
font-size: 0.875em; |
||||||
|
color: var(--bs-body-bg); |
||||||
|
background-color: var(--bs-body-color); |
||||||
|
border-radius: 0.25rem; |
||||||
|
} |
||||||
|
kbd kbd { |
||||||
|
padding: 0; |
||||||
|
font-size: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
figure { |
||||||
|
margin: 0 0 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
img, |
||||||
|
svg { |
||||||
|
vertical-align: middle; |
||||||
|
} |
||||||
|
|
||||||
|
table { |
||||||
|
caption-side: bottom; |
||||||
|
border-collapse: collapse; |
||||||
|
} |
||||||
|
|
||||||
|
caption { |
||||||
|
padding-top: 0.5rem; |
||||||
|
padding-bottom: 0.5rem; |
||||||
|
color: var(--bs-secondary-color); |
||||||
|
text-align: right; |
||||||
|
} |
||||||
|
|
||||||
|
th { |
||||||
|
text-align: inherit; |
||||||
|
text-align: -webkit-match-parent; |
||||||
|
} |
||||||
|
|
||||||
|
thead, |
||||||
|
tbody, |
||||||
|
tfoot, |
||||||
|
tr, |
||||||
|
td, |
||||||
|
th { |
||||||
|
border-color: inherit; |
||||||
|
border-style: solid; |
||||||
|
border-width: 0; |
||||||
|
} |
||||||
|
|
||||||
|
label { |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
button { |
||||||
|
border-radius: 0; |
||||||
|
} |
||||||
|
|
||||||
|
button:focus:not(:focus-visible) { |
||||||
|
outline: 0; |
||||||
|
} |
||||||
|
|
||||||
|
input, |
||||||
|
button, |
||||||
|
select, |
||||||
|
optgroup, |
||||||
|
textarea { |
||||||
|
margin: 0; |
||||||
|
font-family: inherit; |
||||||
|
font-size: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
button, |
||||||
|
select { |
||||||
|
text-transform: none; |
||||||
|
} |
||||||
|
|
||||||
|
[role=button] { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
select { |
||||||
|
word-wrap: normal; |
||||||
|
} |
||||||
|
select:disabled { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { |
||||||
|
display: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
button, |
||||||
|
[type=button], |
||||||
|
[type=reset], |
||||||
|
[type=submit] { |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
button:not(:disabled), |
||||||
|
[type=button]:not(:disabled), |
||||||
|
[type=reset]:not(:disabled), |
||||||
|
[type=submit]:not(:disabled) { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
::-moz-focus-inner { |
||||||
|
padding: 0; |
||||||
|
border-style: none; |
||||||
|
} |
||||||
|
|
||||||
|
textarea { |
||||||
|
resize: vertical; |
||||||
|
} |
||||||
|
|
||||||
|
fieldset { |
||||||
|
min-width: 0; |
||||||
|
padding: 0; |
||||||
|
margin: 0; |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
|
||||||
|
legend { |
||||||
|
float: right; |
||||||
|
width: 100%; |
||||||
|
padding: 0; |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
font-size: calc(1.275rem + 0.3vw); |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
legend { |
||||||
|
font-size: 1.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
legend + * { |
||||||
|
clear: right; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-datetime-edit-fields-wrapper, |
||||||
|
::-webkit-datetime-edit-text, |
||||||
|
::-webkit-datetime-edit-minute, |
||||||
|
::-webkit-datetime-edit-hour-field, |
||||||
|
::-webkit-datetime-edit-day-field, |
||||||
|
::-webkit-datetime-edit-month-field, |
||||||
|
::-webkit-datetime-edit-year-field { |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-inner-spin-button { |
||||||
|
height: auto; |
||||||
|
} |
||||||
|
|
||||||
|
[type=search] { |
||||||
|
outline-offset: -2px; |
||||||
|
-webkit-appearance: textfield; |
||||||
|
} |
||||||
|
|
||||||
|
[type="tel"], |
||||||
|
[type="url"], |
||||||
|
[type="email"], |
||||||
|
[type="number"] { |
||||||
|
direction: ltr; |
||||||
|
} |
||||||
|
::-webkit-search-decoration { |
||||||
|
-webkit-appearance: none; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-color-swatch-wrapper { |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-file-upload-button { |
||||||
|
font: inherit; |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
|
||||||
|
::file-selector-button { |
||||||
|
font: inherit; |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
|
||||||
|
output { |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
iframe { |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
|
||||||
|
summary { |
||||||
|
display: list-item; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
progress { |
||||||
|
vertical-align: baseline; |
||||||
|
} |
||||||
|
|
||||||
|
[hidden] { |
||||||
|
display: none !important; |
||||||
|
} |
||||||
|
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */ |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 775 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 34 KiB |