Compare commits
No commits in common. '3a0ab781f7853d1638ebe07b2a5f515b49a4e402' and '44f325cf6097726932b42c4f1f6db666a04bb777' have entirely different histories.
3a0ab781f7
...
44f325cf60
58 changed files with 1285 additions and 2825 deletions
@ -1,4 +1,4 @@ |
|||||||
dela.zip |
|
||||||
bin/ |
bin/ |
||||||
TODO |
TODO |
||||||
conf.json |
dela.db |
||||||
|
dela.zip |
@ -0,0 +1,26 @@ |
|||||||
|
Copyright (c) 2017 The Sqlite Authors. All rights reserved. |
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without |
||||||
|
modification, are permitted provided that the following conditions are met: |
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this |
||||||
|
list of conditions and the following disclaimer. |
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice, |
||||||
|
this list of conditions and the following disclaimer in the documentation |
||||||
|
and/or other materials provided with the distribution. |
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors |
||||||
|
may be used to endorse or promote products derived from this software without |
||||||
|
specific prior written permission. |
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -1,80 +1,31 @@ |
|||||||
 |
# dela (pun интедед) - daily events list application, aka web TODO list |
||||||
|
|
||||||
# Dela - dead simple web TODO list |
|
||||||
|
|
||||||
## About |
## About |
||||||
Dela is a web TODO list application which can be hosted on your server and accessed via browser. Current capabilities include: |
|
||||||
|
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 |
- Password protected account system |
||||||
- TODO creation |
- TODO creation |
||||||
- TODO completion |
- TODO completion |
||||||
- TODO deletion |
|
||||||
- TODO drag-and-drop on categories |
|
||||||
- Due date selection on TODO creation |
|
||||||
|
|
||||||
## Build |
## Build |
||||||
Dela is written in Go, so you need to have a Go compiler. |
|
||||||
|
|
||||||
For ease of compilation it is also recommended to have `make`. |
|
||||||
|
|
||||||
To automatically compile Dela and have a ready-to-go solution, run: |
|
||||||
|
|
||||||
``` |
``` |
||||||
make |
make |
||||||
``` |
``` |
||||||
|
|
||||||
a portable `/bin` directory should appear where binary and base contents are located. |
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). |
||||||
|
|
||||||
|
|
||||||
### Manual build |
|
||||||
Or you can manually compile with `go build` inside the `src` directory. Move the binary and put it alongside `pages`, `scripts` and `static` (or specify the path to the contents directory via configuration file which is created automatically upon first launch). |
|
||||||
|
|
||||||
|
|
||||||
### Portable build |
|
||||||
To get a portable archive run: |
|
||||||
|
|
||||||
``` |
``` |
||||||
make portable |
make portable |
||||||
``` |
``` |
||||||
|
|
||||||
which will create `dela.zip` archive with all the content directories and the binary which can be freely transported to other machines (of the same OS and architecture) |
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) |
||||||
|
|
||||||
### Cross compilation |
|
||||||
For cross-platform compilation there is: |
|
||||||
|
|
||||||
``` |
|
||||||
make cross |
|
||||||
``` |
|
||||||
|
|
||||||
which cross compiles the project for linux, freebsd, windows and darwin systems. Portable solutions will be put in `/bin` directory in the corresponding subdirectories. |
|
||||||
|
|
||||||
## Use |
## Use |
||||||
|
|
||||||
### Configuration file |
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. |
||||||
After the first run a configuration file will be put alongside the executable, upon the second launch the server will start and the service will be accessible on the specified port. |
|
||||||
|
|
||||||
Currently configuration file contains these filelds: |
|
||||||
```json |
|
||||||
{ |
|
||||||
"port": 8080, |
|
||||||
"cert_file_path": "", |
|
||||||
"key_file_path": "", |
|
||||||
"base_content_dir": ".", |
|
||||||
"production_db_name": "dela.db" |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
| Field | Description | |
|
||||||
| --- | ----------- | |
|
||||||
| port | port on which the service will run | |
|
||||||
| cert_file_path | path to the SSL certificate file | |
|
||||||
| key_file_path | path to the SSL certificate key file | |
|
||||||
| base_content_dir | path to the directory with `pages`, `scripts` and `static` subdirectories | |
|
||||||
| production_db_name | SQLite3 database file path | |
|
||||||
|
|
||||||
|
|
||||||
### SSL certificates |
|
||||||
If you intend to use SSL certificates - there are corresponding fields in the configuration file. |
|
||||||
|
|
||||||
## License |
## License |
||||||
Dela is licensed under AGPL |
|
||||||
|
AGPLv3 |
Binary file not shown.
@ -1,23 +1,15 @@ |
|||||||
{{ template "base" . }} |
{{ template "base" . }} |
||||||
|
|
||||||
{{ define "content" }} |
{{ define "content" }} |
||||||
|
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column"> |
||||||
<main class="container my-5"> |
<main class="px-3"> |
||||||
<div class="px-4 pt-5 my-5 text-center border shadow-lg"> |
<h1>Dela.</h1> |
||||||
<h1 class="display-4 fw-bold text-body-emphasis">Dela</h1> |
<p class="lead">A free and open-source web TODO list</p> |
||||||
<div class="col-lg-6 mx-auto"> |
<p class="lead"> |
||||||
<p class="lead mb-4">a dead simple and minimalistic web TODO list</p> |
<a href="/login" class="btn btn-lg btn-primary">Login</a> |
||||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center mb-5"> |
<a href="/register" class="btn btn-lg btn-primary">Register</a> |
||||||
<a href="/register" class="btn btn-primary btn-lg px-4 me-md-2 fw-bold">Register</a> |
</p> |
||||||
<a href="/login" class="btn btn-outline-secondary btn-lg px-4">Log in</a> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<div class="overflow-hidden" style="max-height: 30vh;"> |
|
||||||
<div class="container px-5"> |
|
||||||
<img src="/static/images/dela_main.png" class="img-fluid border rounded-3 shadow-lg mb-4" alt="Dela interface" width="980" height="700" loading="lazy"> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</main> |
</main> |
||||||
|
</div> |
||||||
|
|
||||||
{{ end }} |
{{ end }} |
@ -1,343 +0,0 @@ |
|||||||
{{ template "base" . }} |
|
||||||
|
|
||||||
{{ define "content" }} |
|
||||||
|
|
||||||
<h1 style="display: none;" id="categoryId">{{.CurrentGroupId}}</h1> |
|
||||||
|
|
||||||
<!-- Main --> |
|
||||||
<main class="d-flex flex-wrap"> |
|
||||||
|
|
||||||
<!-- MODALS --> |
|
||||||
<!-- Delete Confirmation Modal --> |
|
||||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true"> |
|
||||||
<div class="modal-dialog"> |
|
||||||
<div class="modal-content"> |
|
||||||
<div class="modal-header"> |
|
||||||
<h5 class="modal-title" id="deleteModalLabel">Confirm Deletion</h5> |
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|
||||||
</div> |
|
||||||
<div class="modal-body"> |
|
||||||
Are you sure you want to delete this ToDo? |
|
||||||
</div> |
|
||||||
<div class="modal-footer"> |
|
||||||
<button type="button" class="btn btn-danger" id="confirmDeleteButton">Delete</button> |
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<!-- End delete confirmation modal --> |
|
||||||
|
|
||||||
<!-- Paint Canvas Modal --> |
|
||||||
<div class="modal fade" id="paintModal" tabindex="-1" aria-labelledby="paintModal" aria-hidden="true"> |
|
||||||
<div class="modal-dialog"> |
|
||||||
<div class="modal-content"> |
|
||||||
<div class="modal-header"> |
|
||||||
<h5 class="modal-title" id="paintModal">Draw Note</h5> |
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="clearCanvas();"></button> |
|
||||||
</div> |
|
||||||
<div class="modal-body w-100 d-flex flex-column justify-content-center align-items-center"> |
|
||||||
{{ template "paint" . }} |
|
||||||
</div> |
|
||||||
<div class="modal-footer"> |
|
||||||
<button type="button" class="btn btn-primary" id="saveCanvasButton" data-bs-dismiss="modal">Save</button> |
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="clearCanvas();">Cancel</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<!-- End Paint Canvas Modal --> |
|
||||||
|
|
||||||
<!-- ToDo display Modal --> |
|
||||||
<div class="modal fade" id="todoModal" tabindex="-1" aria-labelledby="todoModalLabel" aria-hidden="true"> |
|
||||||
<div class="modal-dialog"> |
|
||||||
<div class="modal-content"> |
|
||||||
<div class="modal-header"> |
|
||||||
<h5 class="modal-title" id="todoModalLabel">TODO Details</h5> |
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|
||||||
</div> |
|
||||||
<div class="modal-body"> |
|
||||||
<p><strong>Text:</strong> <span id="modalTodoText"></span></p> |
|
||||||
<p><strong>Created:</strong> <span id="modalTodoCreated"></span></p> |
|
||||||
<p><strong>Due:</strong> <span id="modalTodoDue"></span></p> |
|
||||||
<p><strong>Completion time:</strong> <span id="modalTodoCompletionTime"></span></p> |
|
||||||
<img id="modalTodoImage" src="" class="img-fluid" style="display: none;"> |
|
||||||
</div> |
|
||||||
<div class="modal-footer"> |
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<!-- Sidebar --> |
|
||||||
<div id="sidebar" class="col border-right shadow-lg 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"> |
|
||||||
<img class="bi pe-none me-2" width="30" height="24" src="/static/images/arrows-fullscreen.svg"> |
|
||||||
<span class="fs-5 fw-semibold">Categories</span> |
|
||||||
</a> |
|
||||||
|
|
||||||
<div class="list-group list-group-flush border-bottom scrollarea"> |
|
||||||
{{ range .Groups }} |
|
||||||
<a id="group-{{.ID}}" href="/group/{{.ID}}" class="list-group-item list-group-item-action py-3 lh-sm {{if eq .ID $.CurrentGroupId}} active {{end}}" aria-current="true" ondragover="allowDrop(event);" ondrop="drop(event);"> |
|
||||||
<div id="group-{{.ID}}" class="d-flex w-100 align-items-center justify-content-between"> |
|
||||||
<strong id="group-{{.ID}}" class="mb-1">{{ .Name }}</strong> |
|
||||||
<small id="group-{{.ID}}">{{ .TimeCreated }}</small> |
|
||||||
</div> |
|
||||||
{{ if not .Removable }} |
|
||||||
<div id="group-{{.ID}}" class="col-10 mb-1 small">Not removable</div> |
|
||||||
{{ end }} |
|
||||||
</a> |
|
||||||
{{ end }} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Main ToDos section --> |
|
||||||
<div class="p-2 flex-grow-1"> |
|
||||||
<form action="javascript:void(0);" id="todoForm"> |
|
||||||
<div class="row g-3 align-items-center"> |
|
||||||
<div class="col-md"> |
|
||||||
<label for="newTodoText" class="form-label">TODO Text</label> |
|
||||||
<input type="text" class="form-control" id="newTodoText" placeholder="Enter TODO text" required> |
|
||||||
</div> |
|
||||||
<div class="col-md"> |
|
||||||
<label for="newTodoDue" class="form-label">Due Date</label> |
|
||||||
<input type="date" class="form-control" name="newTodoDue" id="newTodoDue" required> |
|
||||||
</div> |
|
||||||
<div class="col-auto"> |
|
||||||
<button type="button" class="btn btn-primary" id="newTodoPaint" onclick="openPaintModal();">Paint</button> |
|
||||||
</div> |
|
||||||
<div class="col-auto"> |
|
||||||
<button type="submit" id="newTodoSubmit" class="btn btn-primary">Add</button> |
|
||||||
<button type="button" id="show-done" class="btn btn-secondary">Show Done</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</form> |
|
||||||
|
|
||||||
<div class="container text-center"> |
|
||||||
<!-- Due --> |
|
||||||
<table class="table table-hover" id="due-todos"> |
|
||||||
<thead> |
|
||||||
<th>Image</th> |
|
||||||
<th>ToDo</th> |
|
||||||
<th>Created</th> |
|
||||||
<th>Due</th> |
|
||||||
</thead> |
|
||||||
<tbody class="text-break"> |
|
||||||
{{ range .Todos }} |
|
||||||
{{ if not .IsDone }} |
|
||||||
<tr onclick="openTodoModal('{{.ID}}', '{{.Text}}', '{{.TimeCreated}}', '{{.Due}}', null, '{{ printf "%s" .Image }}');" draggable="true" id="todo-{{.ID}}" ondragstart="dragStart(event);"> |
|
||||||
{{ if not .Image }} |
|
||||||
<!-- Display transparent white pixel --> |
|
||||||
<td><img class="todo-image" src='data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' width="64px" height="64px"></td> |
|
||||||
{{ else }} |
|
||||||
<td><img class="todo-image" src='{{ printf "%s" .Image }}' width="64px" height="64px"></td> |
|
||||||
{{ end }} |
|
||||||
<td class="todo-text text-wrap text-break">{{ .Text }}</td> |
|
||||||
<td class="todo-created">{{ .TimeCreated }}</td> |
|
||||||
<td class="todo-due">{{ .Due }}</td> |
|
||||||
<td class="todo-due-unix" style="display: none;">{{ .DueUnix }}</td> |
|
||||||
<td> |
|
||||||
<button class="btn btn-success" onclick="markAsDoneRefresh('{{.ID}}');"> |
|
||||||
<img src='/static/images/check.svg'> |
|
||||||
</button> |
|
||||||
<button class="btn btn-danger" onclick="openDeleteModal('{{.ID}}');"> |
|
||||||
<img src='/static/images/trash3-fill.svg'> |
|
||||||
</button> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
{{ end }} |
|
||||||
{{ end }} |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
|
|
||||||
<!-- Completed --> |
|
||||||
<table class="table table-hover" style="display: none;" id="completed-todos"> |
|
||||||
<thead> |
|
||||||
<th>Image</th> |
|
||||||
<th>ToDo</th> |
|
||||||
<th>Created</th> |
|
||||||
<th>Completed</th> |
|
||||||
</thead> |
|
||||||
<tbody class="text-break"> |
|
||||||
{{ range .Todos }} |
|
||||||
{{ if .IsDone }} |
|
||||||
<tr onclick="openTodoModal('{{.ID}}', '{{.Text}}', '{{.TimeCreated}}', '{{.Due}}', '{{.CompletionTime}}', '{{ printf "%s" .Image }}');"> |
|
||||||
{{ if not .Image }} |
|
||||||
<!-- Display transparent white pixel --> |
|
||||||
<td><img src='data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' width="64px" height="64px"></td> |
|
||||||
{{ else }} |
|
||||||
<td><img src='{{ printf "%s" .Image }}' width="64px" height="64px"></td> |
|
||||||
{{ end }} |
|
||||||
<td>{{ .Text }}</td> |
|
||||||
<td>{{ .TimeCreated }}</td> |
|
||||||
<td>{{ .CompletionTime }}</td> |
|
||||||
<td> |
|
||||||
<button class="btn btn-danger" onclick="deleteTodoRefresh('{{.ID}}');"><img src='/static/images/trash3-fill.svg'></button> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
{{ end }} |
|
||||||
{{ end }} |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</main> |
|
||||||
|
|
||||||
|
|
||||||
<script> |
|
||||||
let todoToDeleteId; |
|
||||||
|
|
||||||
// TODO deletion modal functions |
|
||||||
function openDeleteModal(id) { |
|
||||||
todoToDeleteId = id; // Save ID for handleTodoDelete to work properly |
|
||||||
|
|
||||||
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'), {}); |
|
||||||
|
|
||||||
// Remove any existing event listener to prevent multiple bindings |
|
||||||
const confirmDeleteButton = document.getElementById('confirmDeleteButton'); |
|
||||||
confirmDeleteButton.removeEventListener('click', handleTodoDelete); |
|
||||||
|
|
||||||
// Add the event listener for the delete action |
|
||||||
confirmDeleteButton.addEventListener('click', handleTodoDelete); |
|
||||||
|
|
||||||
deleteModal.show(); |
|
||||||
} |
|
||||||
|
|
||||||
async function handleTodoDelete() { |
|
||||||
await deleteTodoRefresh(todoToDeleteId); // Call the delete function |
|
||||||
const deleteModal = bootstrap.Modal.getInstance(document.getElementById('deleteModal')); |
|
||||||
deleteModal.hide(); // Hide the modal |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
function openPaintModal() { |
|
||||||
const paintModal = new bootstrap.Modal(document.getElementById('paintModal'), {});; |
|
||||||
paintModal.show(); |
|
||||||
} |
|
||||||
|
|
||||||
// Mark TODO as done |
|
||||||
async function markAsDoneRefresh(id) { |
|
||||||
await markAsDone(id); |
|
||||||
window.location.reload(); |
|
||||||
} |
|
||||||
|
|
||||||
// Delete TODO with ID |
|
||||||
async function deleteTodoRefresh(id) { |
|
||||||
await deleteTodo(id); |
|
||||||
window.location.reload(); |
|
||||||
} |
|
||||||
|
|
||||||
async function showDone() { |
|
||||||
// Hide not done, show done |
|
||||||
let completedTodos = document.getElementById("completed-todos"); |
|
||||||
completedTodos.style.display = "table"; |
|
||||||
|
|
||||||
let dueTodos = document.getElementById("due-todos"); |
|
||||||
dueTodos.style.display = "none"; |
|
||||||
} |
|
||||||
|
|
||||||
function allowDrop(event) { |
|
||||||
event.preventDefault(); |
|
||||||
} |
|
||||||
|
|
||||||
function dragStart(event) { |
|
||||||
event.dataTransfer.setData("text", event.target.id); |
|
||||||
event.dataTransfer.effectAllowed = "move"; |
|
||||||
} |
|
||||||
|
|
||||||
async function drop(event) { |
|
||||||
event.preventDefault(); |
|
||||||
|
|
||||||
var todoPageId = event.dataTransfer.getData("text"); |
|
||||||
let draggedTodo = document.getElementById(todoPageId); |
|
||||||
let todoId = todoPageId.split("-")[1]; |
|
||||||
let targetGroupId = event.target.id.split("-")[1]; |
|
||||||
if (targetGroupId == document.getElementById("categoryId").innerText) { |
|
||||||
// Do nothing |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
// Update todo's group ID |
|
||||||
let result = await updateTodo(todoId, { |
|
||||||
text: draggedTodo.getElementsByClassName("todo-text")[0].innerText, |
|
||||||
groupId: Number(targetGroupId), |
|
||||||
dueUnix: Number(draggedTodo.getElementsByClassName("todo-due-unix")[0].innerText), |
|
||||||
image: Array.from(draggedTodo.getElementsByClassName("todo-image")[0].src, char => char.charCodeAt(0)) |
|
||||||
}); |
|
||||||
|
|
||||||
window.location.reload(); |
|
||||||
} |
|
||||||
|
|
||||||
function openTodoModal(id, text, created, due, completionTime, image) { |
|
||||||
document.getElementById('modalTodoText').innerText = text; |
|
||||||
document.getElementById('modalTodoCreated').innerText = created; |
|
||||||
document.getElementById('modalTodoDue').innerText = due; |
|
||||||
document.getElementById('modalTodoCompletionTime').innerText = completionTime; |
|
||||||
|
|
||||||
let img = document.getElementById('modalTodoImage'); |
|
||||||
if (img) { |
|
||||||
img.src = image; |
|
||||||
img.style.display = 'block'; |
|
||||||
} else { |
|
||||||
img.style.display = 'none'; |
|
||||||
} |
|
||||||
|
|
||||||
const todoModal = new bootstrap.Modal(document.getElementById('todoModal')); |
|
||||||
todoModal.show(); |
|
||||||
} |
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async function() { |
|
||||||
document.getElementById("newTodoText").focus(); |
|
||||||
|
|
||||||
let showDoneButton = document.getElementById("show-done"); |
|
||||||
showDoneButton.addEventListener("click", (event) => { |
|
||||||
// Rename the button |
|
||||||
showDoneButton.innerText = "Show To Do"; |
|
||||||
showDoneButton.className = "btn btn-success"; |
|
||||||
// Show done |
|
||||||
showDone(); |
|
||||||
|
|
||||||
// Make it "reset to default" |
|
||||||
showDoneButton.addEventListener("click", (event) => { |
|
||||||
location.reload(); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
|
|
||||||
// "Add" button |
|
||||||
document.getElementById("newTodoSubmit").addEventListener("click", async (event) => { |
|
||||||
let newTodoTextInput = document.getElementById("newTodoText"); |
|
||||||
let newTodoText = newTodoTextInput.value; |
|
||||||
if (newTodoText.length < 1) { |
|
||||||
newTodoTextInput.setCustomValidity("At least one character is needed!"); |
|
||||||
return; |
|
||||||
} else { |
|
||||||
newTodoTextInput.setCustomValidity(""); |
|
||||||
} |
|
||||||
newTodoTextInput.value = ""; |
|
||||||
|
|
||||||
let newTodoDueInput = document.getElementById("newTodoDue"); |
|
||||||
let dueTimeStamp = Date.parse(newTodoDueInput.value) / 1000; |
|
||||||
|
|
||||||
let groupId = document.getElementById("categoryId").innerText; |
|
||||||
|
|
||||||
let canvasImage = getCanvasImage(); |
|
||||||
if (canvasImage) { |
|
||||||
canvasImage = Array.from(canvasImage, char => char.charCodeAt(0)); |
|
||||||
} |
|
||||||
|
|
||||||
// Make a request |
|
||||||
let response = await postNewTodo( |
|
||||||
{text: newTodoText, groupId: Number(groupId), dueUnix: Number(dueTimeStamp), image: canvasImage} |
|
||||||
); |
|
||||||
if (response.ok) { |
|
||||||
location.reload(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
</script> |
|
||||||
|
|
||||||
{{ end }} |
|
@ -1,13 +0,0 @@ |
|||||||
{{ 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 class="text-danger display-2">Error!</h1> |
|
||||||
<img src="/static/images/emoji-frown.svg" alt="Sad face" width="128px" > |
|
||||||
<p>Sorry! Something went wrong somewhere!</p> |
|
||||||
<p><u>Try to reload the faulty page or try again later</u></p> |
|
||||||
</div> |
|
||||||
</main> |
|
||||||
|
|
||||||
{{ end }} |
|
@ -1,99 +0,0 @@ |
|||||||
{{ define "paint" }} |
|
||||||
|
|
||||||
<canvas class="row border border-secondary" id="drawingCanvas" width="256" height="256"></canvas> |
|
||||||
<input class="row border border-secondary" type="color" id="colorPicker" value="#000000" aria-label="Drawing color"> |
|
||||||
|
|
||||||
<script> |
|
||||||
const canvas = document.getElementById('drawingCanvas'); |
|
||||||
const ctx = canvas.getContext('2d'); |
|
||||||
const colorPicker = document.getElementById('colorPicker'); |
|
||||||
let drawing = false; |
|
||||||
|
|
||||||
function startDrawing(x, y) { |
|
||||||
drawing = true; |
|
||||||
ctx.beginPath(); |
|
||||||
ctx.moveTo(x, y); |
|
||||||
}; |
|
||||||
|
|
||||||
function draw(x, y){ |
|
||||||
if (drawing) { |
|
||||||
ctx.strokeStyle = colorPicker.value; |
|
||||||
ctx.lineWidth = 5; |
|
||||||
ctx.lineTo(x, y); |
|
||||||
ctx.stroke(); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
function stopDrawing() { |
|
||||||
drawing = false; |
|
||||||
ctx.closePath(); |
|
||||||
}; |
|
||||||
|
|
||||||
function getMousePos(event) { |
|
||||||
const rect = canvas.getBoundingClientRect(); |
|
||||||
return { |
|
||||||
x: event.clientX - rect.left, |
|
||||||
y: event.clientY - rect.top |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
function getTouchPos(event) { |
|
||||||
const rect = canvas.getBoundingClientRect(); |
|
||||||
return { |
|
||||||
x: event.touches[0].clientX - rect.left, |
|
||||||
y: event.touches[0].clientY - rect.top |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
// Mouse events |
|
||||||
canvas.addEventListener('mousedown', (e) => { |
|
||||||
const pos = getMousePos(e); |
|
||||||
startDrawing(pos.x, pos.y); |
|
||||||
}); |
|
||||||
|
|
||||||
canvas.addEventListener('mousemove', (e) => { |
|
||||||
const pos = getMousePos(e); |
|
||||||
draw(pos.x, pos.y); |
|
||||||
}); |
|
||||||
|
|
||||||
canvas.addEventListener('mouseup', stopDrawing); |
|
||||||
canvas.addEventListener('mouseleave', stopDrawing); |
|
||||||
|
|
||||||
// Touch events |
|
||||||
canvas.addEventListener('touchstart', (e) => { |
|
||||||
e.preventDefault(); // Prevent scrolling |
|
||||||
const pos = getTouchPos(e); |
|
||||||
startDrawing(pos.x, pos.y); |
|
||||||
}); |
|
||||||
|
|
||||||
canvas.addEventListener('touchmove', (e) => { |
|
||||||
e.preventDefault(); // Prevent scrolling |
|
||||||
const pos = getTouchPos(e); |
|
||||||
draw(pos.x, pos.y); |
|
||||||
}); |
|
||||||
|
|
||||||
canvas.addEventListener('touchend', stopDrawing); |
|
||||||
|
|
||||||
// Fills with white |
|
||||||
function clearCanvas() { |
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
||||||
} |
|
||||||
|
|
||||||
function isCanvasEmpty() { |
|
||||||
const pixels = new Uint32Array( |
|
||||||
ctx.getImageData(0, 0, canvas.width, canvas.height).data.buffer |
|
||||||
); |
|
||||||
|
|
||||||
return !pixels.some(color => color !== 0); |
|
||||||
} |
|
||||||
|
|
||||||
function getCanvasImage() { |
|
||||||
if (!isCanvasEmpty()) { |
|
||||||
return canvas.toDataURL("image/png"); |
|
||||||
} |
|
||||||
|
|
||||||
return null; |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
{{ end }} |
|
@ -1,95 +1,69 @@ |
|||||||
/* |
/* |
||||||
2024 Kasyanov Nikolay Alexeyevich (Unbewohnte) |
2023 Kasyanov Nikolay Alexeyevich (Unbewohnte) |
||||||
*/ |
*/ |
||||||
|
|
||||||
async function post(url, json) { |
|
||||||
return fetch(url, { |
async function postNewTodo(username, password, new_todo) { |
||||||
|
return fetch("/api/todo", { |
||||||
method: "POST", |
method: "POST", |
||||||
credentials: "include", |
|
||||||
headers: { |
headers: { |
||||||
|
"EncryptedBase64": "false", |
||||||
|
"Auth": username + "<-->" + password, |
||||||
"Content-Type": "application/json", |
"Content-Type": "application/json", |
||||||
}, |
}, |
||||||
body: JSON.stringify(json) |
body: JSON.stringify(new_todo), |
||||||
}) |
}); |
||||||
} |
|
||||||
|
|
||||||
async function postEmailVerification(email, code) { |
|
||||||
return post("/api/user/verify", {"email":email, "code":code}); |
|
||||||
} |
|
||||||
|
|
||||||
async function postNewTodo(newTodo) { |
|
||||||
return post("/api/todo/create", newTodo) |
|
||||||
} |
|
||||||
|
|
||||||
async function postNewGroup(newGroup) { |
|
||||||
return post("/api/group/create", newGroup) |
|
||||||
} |
|
||||||
|
|
||||||
async function postNewUser(newUser) { |
|
||||||
return post("/api/user/create", newUser) |
|
||||||
} |
} |
||||||
|
|
||||||
async function doLogin(userInformation) { |
|
||||||
return post("/api/user/login", userInformation) |
|
||||||
} |
|
||||||
|
|
||||||
async function get(url) { |
async function getTodos(username, password) { |
||||||
return fetch(url, { |
return fetch("/api/todo", { |
||||||
method: "GET", |
method: "GET", |
||||||
credentials: "include", |
headers: { |
||||||
}) |
"EncryptedBase64": "false", |
||||||
|
"Auth": username + "<-->" + password |
||||||
|
}, |
||||||
|
}); |
||||||
} |
} |
||||||
|
|
||||||
async function getUser() { |
|
||||||
return get("/api/user/get"); |
|
||||||
} |
|
||||||
|
|
||||||
async function getTodos() { |
async function getTodoGroups(username, password) { |
||||||
return get("/api/todo/get"); |
return fetch("/api/group", { |
||||||
} |
method: "GET", |
||||||
|
headers: { |
||||||
async function getGroup() { |
"EncryptedBase64": "false", |
||||||
return get("/api/group/get"); |
"Auth": username + "<-->" + password |
||||||
|
}, |
||||||
|
}); |
||||||
} |
} |
||||||
|
|
||||||
async function getAllGroups() { |
async function deleteTodo(username, password, id) { |
||||||
return get("/api/user/get"); |
return fetch("/api/todo/"+String(id), { |
||||||
|
method: "DELETE", |
||||||
|
headers: { |
||||||
|
"EnctyptedBase64": "false", |
||||||
|
"Auth": username + "<-->" + password, |
||||||
|
}, |
||||||
|
}); |
||||||
} |
} |
||||||
|
|
||||||
async function del(url) { |
async function updateTodo(username, password, id, updatedTodo) { |
||||||
return fetch(url, { |
return fetch("/api/todo/"+String(id), { |
||||||
method: "POST", |
method: "POST", |
||||||
credentials: "include", |
|
||||||
headers: { |
headers: { |
||||||
"Content-Type": "application/json", |
"EncryptedBase64": "false", |
||||||
|
"Auth": username + "<-->" + password, |
||||||
}, |
}, |
||||||
}) |
body: JSON.stringify(updatedTodo), |
||||||
} |
}); |
||||||
|
|
||||||
async function deleteTodo(id) { |
|
||||||
return del("/api/todo/delete/"+id); |
|
||||||
} |
|
||||||
|
|
||||||
async function deleteCategory(id) { |
|
||||||
return del("/api/group/delete/"+id); |
|
||||||
} |
|
||||||
|
|
||||||
async function update(url, json) { |
|
||||||
return post(url, json); |
|
||||||
} |
} |
||||||
|
|
||||||
async function updateTodo(id, updatedTodo) { |
async function getUser(username, password) { |
||||||
return update("/api/todo/update/"+id, updatedTodo); |
return fetch("/api/user", { |
||||||
} |
method: "GET", |
||||||
|
headers: { |
||||||
async function markAsDone(id) { |
"EncryptedBase64": "false", |
||||||
return update("/api/todo/markdone/"+id); |
"Auth": username + "<-->" + password |
||||||
} |
}, |
||||||
|
}); |
||||||
async function updateGroup(id, updatedGroup) { |
|
||||||
return update("/api/group/update/"+id, updatedGroup); |
|
||||||
} |
|
||||||
|
|
||||||
async function updateUser(updatedUser) { |
|
||||||
return update("/api/user/update", updatedUser); |
|
||||||
} |
} |
@ -1,186 +0,0 @@ |
|||||||
/* |
|
||||||
dela - web TODO list |
|
||||||
Copyright (C) 2023, 2024 Kasyanov Nikolay Alexeyevich (Unbewohnte) |
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify |
|
||||||
it under the terms of the GNU Affero General Public License as published by |
|
||||||
the Free Software Foundation, either version 3 of the License, or |
|
||||||
(at your option) any later version. |
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, |
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||||
GNU Affero General Public License for more details. |
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License |
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/ |
|
||||||
|
|
||||||
package db |
|
||||||
|
|
||||||
import ( |
|
||||||
"database/sql" |
|
||||||
) |
|
||||||
|
|
||||||
// Todo group structure
|
|
||||||
type TodoGroup struct { |
|
||||||
ID uint64 `json:"id"` |
|
||||||
Name string `json:"name"` |
|
||||||
TimeCreatedUnix uint64 `json:"timeCreatedUnix"` |
|
||||||
OwnerEmail string `json:"ownerEmail"` |
|
||||||
Removable bool `json:"removable"` |
|
||||||
TimeCreated string |
|
||||||
} |
|
||||||
|
|
||||||
func NewTodoGroup(name string, timeCreatedUnix uint64, ownerEmail string, removable bool) TodoGroup { |
|
||||||
return TodoGroup{ |
|
||||||
Name: name, |
|
||||||
TimeCreatedUnix: timeCreatedUnix, |
|
||||||
OwnerEmail: ownerEmail, |
|
||||||
Removable: removable, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Creates a new TODO group in the database
|
|
||||||
func (db *DB) CreateTodoGroup(group TodoGroup) error { |
|
||||||
_, err := db.Exec( |
|
||||||
"INSERT INTO todo_groups(name, time_created_unix, owner_email, removable) VALUES(?, ?, ?, ?)", |
|
||||||
group.Name, |
|
||||||
group.TimeCreatedUnix, |
|
||||||
group.OwnerEmail, |
|
||||||
group.Removable, |
|
||||||
) |
|
||||||
|
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
func scanTodoGroup(rows *sql.Rows) (*TodoGroup, error) { |
|
||||||
var newTodoGroup TodoGroup |
|
||||||
err := rows.Scan( |
|
||||||
&newTodoGroup.ID, |
|
||||||
&newTodoGroup.Name, |
|
||||||
&newTodoGroup.TimeCreatedUnix, |
|
||||||
&newTodoGroup.OwnerEmail, |
|
||||||
&newTodoGroup.Removable, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
// Convert to Basic time string
|
|
||||||
newTodoGroup.TimeCreated = unixToTimeStr(newTodoGroup.TimeCreatedUnix) |
|
||||||
|
|
||||||
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 |
|
||||||
} |
|
||||||
|
|
||||||
func (db *DB) GetGroupTodos(groupId uint64) ([]*Todo, error) { |
|
||||||
rows, err := db.Query("SELECT * FROM todos WHERE group_id=?", groupId) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
defer rows.Close() |
|
||||||
|
|
||||||
var todos []*Todo |
|
||||||
|
|
||||||
for rows.Next() { |
|
||||||
todoGroup, err := scanTodo(rows) |
|
||||||
if err != nil { |
|
||||||
return todos, err |
|
||||||
} |
|
||||||
todos = append(todos, todoGroup) |
|
||||||
} |
|
||||||
|
|
||||||
return todos, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Deletes information about a TODO group of given ID from the database
|
|
||||||
func (db *DB) DeleteTodoGroup(id uint64) error { |
|
||||||
_, err := db.Exec( |
|
||||||
"DELETE FROM todo_groups WHERE id=?", |
|
||||||
id, |
|
||||||
) |
|
||||||
|
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// Deletes all ToDos associated with this group and then the group itself
|
|
||||||
func (db *DB) DeleteTodoGroupClean(groupId uint64) error { |
|
||||||
_, err := db.Exec("DELETE FROM todos WHERE group_id=?", |
|
||||||
groupId, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
_, err = db.Exec( |
|
||||||
"DELETE FROM todo_groups WHERE id=?", |
|
||||||
groupId, |
|
||||||
) |
|
||||||
|
|
||||||
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, email string) bool { |
|
||||||
group, err := db.GetTodoGroup(groupId) |
|
||||||
if err != nil { |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
if group.OwnerEmail != email { |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
return true |
|
||||||
} |
|
@ -1,132 +0,0 @@ |
|||||||
package db |
|
||||||
|
|
||||||
import "database/sql" |
|
||||||
|
|
||||||
type Verification struct { |
|
||||||
ID uint64 `json:"id"` |
|
||||||
Email string `json:"email"` |
|
||||||
Code string `json:"code"` |
|
||||||
IssuedUnix uint64 `json:"issued_unix"` |
|
||||||
LifeSeconds uint64 `json:"life_seconds"` |
|
||||||
} |
|
||||||
|
|
||||||
func NewVerification(email string, code string, issuedUnix uint64, lifeSeconds uint64) *Verification { |
|
||||||
return &Verification{ |
|
||||||
Email: email, |
|
||||||
Code: code, |
|
||||||
IssuedUnix: issuedUnix, |
|
||||||
LifeSeconds: lifeSeconds, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func scanVerification(rows *sql.Rows) (*Verification, error) { |
|
||||||
var newVerification Verification |
|
||||||
err := rows.Scan( |
|
||||||
&newVerification.ID, |
|
||||||
&newVerification.Email, |
|
||||||
&newVerification.Code, |
|
||||||
&newVerification.IssuedUnix, |
|
||||||
&newVerification.LifeSeconds, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return &newVerification, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Retrieves a verification with given Id from the database
|
|
||||||
func (db *DB) GetVerification(id uint64) (*Verification, error) { |
|
||||||
rows, err := db.Query( |
|
||||||
"SELECT * FROM verifications WHERE id=?", |
|
||||||
id, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
defer rows.Close() |
|
||||||
|
|
||||||
rows.Next() |
|
||||||
verification, err := scanVerification(rows) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return verification, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Returns the first email verification by email
|
|
||||||
func (db *DB) GetVerificationByEmail(email string) (*Verification, error) { |
|
||||||
rows, err := db.Query( |
|
||||||
"SELECT * FROM verifications WHERE email=?", |
|
||||||
email, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
defer rows.Close() |
|
||||||
|
|
||||||
rows.Next() |
|
||||||
verification, err := scanVerification(rows) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return verification, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Retrieves information on ALL TODOs
|
|
||||||
func (db *DB) GetVerifications() ([]*Verification, error) { |
|
||||||
var verifications []*Verification |
|
||||||
|
|
||||||
rows, err := db.Query("SELECT * FROM verifications") |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
defer rows.Close() |
|
||||||
|
|
||||||
for rows.Next() { |
|
||||||
verification, err := scanVerification(rows) |
|
||||||
if err != nil { |
|
||||||
return verifications, err |
|
||||||
} |
|
||||||
verifications = append(verifications, verification) |
|
||||||
} |
|
||||||
|
|
||||||
return verifications, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Creates a new verification in the database
|
|
||||||
func (db *DB) CreateVerification(verification Verification) error { |
|
||||||
_, err := db.Exec( |
|
||||||
"INSERT INTO verifications(email, code, issued_unix, life_seconds) VALUES(?, ?, ?, ?)", |
|
||||||
verification.Email, |
|
||||||
verification.Code, |
|
||||||
verification.IssuedUnix, |
|
||||||
verification.LifeSeconds, |
|
||||||
) |
|
||||||
|
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// Deletes information about a verification of certain ID from the database
|
|
||||||
func (db *DB) DeleteVerification(id uint64) error { |
|
||||||
_, err := db.Exec( |
|
||||||
"DELETE FROM verifications WHERE id=?", |
|
||||||
id, |
|
||||||
) |
|
||||||
|
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// Updates verification
|
|
||||||
func (db *DB) UpdateVerification(verificationID uint64, updatedTodo Verification) error { |
|
||||||
_, err := db.Exec( |
|
||||||
"UPDATE verifications SET code=?, life_seconds=? WHERE id=?", |
|
||||||
updatedTodo.Code, |
|
||||||
updatedTodo.LifeSeconds, |
|
||||||
verificationID, |
|
||||||
) |
|
||||||
|
|
||||||
return err |
|
||||||
} |
|
@ -1,15 +0,0 @@ |
|||||||
package email |
|
||||||
|
|
||||||
import ( |
|
||||||
"Unbewohnte/dela/conf" |
|
||||||
"net/smtp" |
|
||||||
) |
|
||||||
|
|
||||||
func Auth(conf conf.Conf) smtp.Auth { |
|
||||||
return smtp.PlainAuth( |
|
||||||
"", |
|
||||||
conf.Verification.Emailer.User, |
|
||||||
conf.Verification.Emailer.Password, |
|
||||||
conf.Verification.Emailer.Host, |
|
||||||
) |
|
||||||
} |
|
@ -1,52 +0,0 @@ |
|||||||
package email |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"net/smtp" |
|
||||||
"strings" |
|
||||||
) |
|
||||||
|
|
||||||
type Email struct { |
|
||||||
Sender string |
|
||||||
To []string |
|
||||||
Subject string |
|
||||||
Body string |
|
||||||
} |
|
||||||
|
|
||||||
func NewEmail(sender, subject, body string, to []string) Email { |
|
||||||
return Email{ |
|
||||||
Sender: sender, |
|
||||||
To: to, |
|
||||||
Subject: subject, |
|
||||||
Body: body, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
type Emailer struct { |
|
||||||
Auth smtp.Auth |
|
||||||
Address string |
|
||||||
From string |
|
||||||
} |
|
||||||
|
|
||||||
func NewEmailer(auth smtp.Auth, addr string, from string) *Emailer { |
|
||||||
return &Emailer{ |
|
||||||
Auth: auth, |
|
||||||
Address: addr, |
|
||||||
From: from, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func buildEmail(mail Email) []byte { |
|
||||||
message := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\r\n" |
|
||||||
message += fmt.Sprintf("From: %s\r\n", mail.Sender) |
|
||||||
message += fmt.Sprintf("To: %s\r\n", strings.Join(mail.To, ";")) |
|
||||||
message += fmt.Sprintf("Subject: %s\r\n", mail.Subject) |
|
||||||
message += fmt.Sprintf("\r\n%s\r\n", mail.Body) |
|
||||||
|
|
||||||
return []byte(message) |
|
||||||
} |
|
||||||
|
|
||||||
func (em *Emailer) SendEmail(mail Email) error { |
|
||||||
err := smtp.SendMail(em.Address, em.Auth, em.From, mail.To, buildEmail(mail)) |
|
||||||
return err |
|
||||||
} |
|
@ -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)) |
||||||
|
} |
@ -1,16 +0,0 @@ |
|||||||
package misc |
|
||||||
|
|
||||||
import ( |
|
||||||
"math/rand" |
|
||||||
"strconv" |
|
||||||
) |
|
||||||
|
|
||||||
// Generates a pseudo-random numeric code of required length
|
|
||||||
func GenerateNumericCode(length uint) string { |
|
||||||
code := "" |
|
||||||
for i := 0; uint(i) < length; i++ { |
|
||||||
code += strconv.Itoa(rand.Intn(10)) |
|
||||||
} |
|
||||||
|
|
||||||
return code |
|
||||||
} |
|
@ -0,0 +1,455 @@ |
|||||||
|
/* |
||||||
|
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) UserEndpoint(w http.ResponseWriter, req *http.Request) { |
||||||
|
switch req.Method { |
||||||
|
case http.MethodDelete: |
||||||
|
// Delete an existing user
|
||||||
|
defer req.Body.Close() |
||||||
|
|
||||||
|
username := GetUsernameFromAuth(req) |
||||||
|
|
||||||
|
// Check if auth data is valid
|
||||||
|
if !IsRequestAuthValid(req, s.db) { |
||||||
|
logger.Warning("[Server] %s failed to authenticate as %s", req.RemoteAddr, username) |
||||||
|
http.Error(w, "Invalid user auth data", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// It is, indeed, a user
|
||||||
|
// Delete with all TODOs
|
||||||
|
err := s.db.DeleteUserClean(username) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Server] Failed to delete %s: %s", username, err) |
||||||
|
http.Error(w, "Failed to delete user or TODO contents", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Success!
|
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
|
||||||
|
case http.MethodPost: |
||||||
|
// Create a new user
|
||||||
|
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 user: %s", err) |
||||||
|
http.Error(w, "Failed to read body", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Unmarshal JSON
|
||||||
|
var newUser db.User |
||||||
|
err = json.Unmarshal(body, &newUser) |
||||||
|
if err != nil { |
||||||
|
logger.Warning("[Server] Received invalid user JSON for creation: %s", err) |
||||||
|
http.Error(w, "Invalid user JSON", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check for validity
|
||||||
|
valid, reason := IsUserValid(newUser) |
||||||
|
if !valid { |
||||||
|
logger.Info("[Server] Rejected creating %s for reason: %s", newUser.Username, reason) |
||||||
|
http.Error(w, "Invalid user data: "+reason, http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Add user to the database
|
||||||
|
newUser.TimeCreatedUnix = uint64(time.Now().Unix()) |
||||||
|
err = s.db.CreateUser(newUser) |
||||||
|
if err != nil { |
||||||
|
http.Error(w, "User already exists", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Create an initial TODO group
|
||||||
|
err = s.db.CreateTodoGroup( |
||||||
|
db.TodoGroup{ |
||||||
|
Name: "Todos", |
||||||
|
TimeCreatedUnix: uint64(time.Now().Unix()), |
||||||
|
OwnerUsername: newUser.Username, |
||||||
|
}, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
// Oops, that's VERY bad. Delete newly created user
|
||||||
|
s.db.DeleteUser(newUser.Username) |
||||||
|
logger.Error("[SERVER] Failed to create an initial TODO group for a newly created \"%s\": %s. Deleted.", newUser.Username, err) |
||||||
|
http.Error(w, "Failed to create initial TODO group", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Success!
|
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
logger.Info("[Server] Created a new user \"%s\"", newUser.Username) |
||||||
|
case http.MethodGet: |
||||||
|
// Check if user information is valid
|
||||||
|
if !IsRequestAuthValid(req, s.db) { |
||||||
|
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
default: |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) SpecificTodoEndpoint(w http.ResponseWriter, req *http.Request) { |
||||||
|
switch req.Method { |
||||||
|
case http.MethodDelete: |
||||||
|
// Delete an existing TODO
|
||||||
|
defer req.Body.Close() |
||||||
|
|
||||||
|
// Check if this user actually exists and the password is valid
|
||||||
|
if !IsRequestAuthValid(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 !DoesUserOwnTodo(GetUsernameFromAuth(req), todoID, s.db) { |
||||||
|
http.Error(w, "You don't own this TODO", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// // Mark TODO as done and assign a completion time
|
||||||
|
// updatedTodo, err := s.db.GetTodo(todoID)
|
||||||
|
// if err != nil {
|
||||||
|
// logger.Error("[Server] Failed to get todo with id %d for marking completion: %s", todoID, err)
|
||||||
|
// http.Error(w, "TODO retrieval error", http.StatusInternalServerError)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// updatedTodo.IsDone = true
|
||||||
|
// updatedTodo.CompletionTimeUnix = uint64(time.Now().Unix())
|
||||||
|
|
||||||
|
// err = s.db.UpdateTodo(todoID, *updatedTodo)
|
||||||
|
// if err != nil {
|
||||||
|
// logger.Error("[Server] Failed to update TODO with id %d: %s", todoID, err)
|
||||||
|
// http.Error(w, "Failed to update TODO information", http.StatusInternalServerError)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Now delete
|
||||||
|
err = s.db.DeleteTodo(todoID) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[Server] Failed to delete %s's TODO: %s", GetUsernameFromAuth(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) |
||||||
|
|
||||||
|
case http.MethodPost: |
||||||
|
// Change TODO information
|
||||||
|
|
||||||
|
// Check authentication information
|
||||||
|
if !IsRequestAuthValid(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 !DoesUserOwnTodo(GetUsernameFromAuth(req), todoID, s.db) { |
||||||
|
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) |
||||||
|
|
||||||
|
default: |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) TodoEndpoint(w http.ResponseWriter, req *http.Request) { |
||||||
|
switch req.Method { |
||||||
|
case http.MethodPost: |
||||||
|
// 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 !IsRequestAuthValid(req, s.db) { |
||||||
|
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Add TODO to the database
|
||||||
|
newTodo.OwnerUsername = GetUsernameFromAuth(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.OwnerUsername) |
||||||
|
|
||||||
|
case http.MethodGet: |
||||||
|
// Retrieve TODO information
|
||||||
|
// Check authentication information
|
||||||
|
if !IsRequestAuthValid(req, s.db) { |
||||||
|
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Get all user TODOs
|
||||||
|
todos, err := s.db.GetAllUserTodos(GetUsernameFromAuth(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) |
||||||
|
default: |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) TodoGroupEndpoint(w http.ResponseWriter, req *http.Request) { |
||||||
|
switch req.Method { |
||||||
|
case http.MethodDelete: |
||||||
|
// 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 !IsRequestAuthValid(req, s.db) { |
||||||
|
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if !DoesUserOwnTodoGroup(GetUsernameFromAuth(req), group.ID, s.db) { |
||||||
|
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", GetUsernameFromAuth(req), err) |
||||||
|
http.Error(w, "Failed to delete TODO group", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Success!
|
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
|
||||||
|
case http.MethodPost: |
||||||
|
// 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 !IsRequestAuthValid(req, s.db) { |
||||||
|
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Add group to the database
|
||||||
|
newGroup.OwnerUsername = GetUsernameFromAuth(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.OwnerUsername) |
||||||
|
case http.MethodGet: |
||||||
|
// Retrieve all todo groups
|
||||||
|
|
||||||
|
// Check authentication information
|
||||||
|
if !IsRequestAuthValid(req, s.db) { |
||||||
|
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Get groups
|
||||||
|
groups, err := s.db.GetAllUserTodoGroups(GetUsernameFromAuth(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) |
||||||
|
|
||||||
|
case http.MethodPatch: |
||||||
|
// Check authentication information
|
||||||
|
if !IsRequestAuthValid(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) |
||||||
|
default: |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,140 @@ |
|||||||
|
/* |
||||||
|
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/encryption" |
||||||
|
"net/http" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
RequestHeaderSecurityKey string = "Security-Key" |
||||||
|
// RequestHeaderAuthSeparator string = "\u200b" // username\u200bpassword
|
||||||
|
RequestHeaderAuthSeparator string = "<-->" // username<-->password
|
||||||
|
RequestHeaderAuthKey string = "Auth" |
||||||
|
RequestHeaderTodoIDKey string = "Todo-Key" |
||||||
|
RequestHeaderEncodedB64 string = "EncryptedBase64" // tells whether auth data is encoded in base64
|
||||||
|
) |
||||||
|
|
||||||
|
// Checks if the request header contains a valid full access key string or not
|
||||||
|
func DoesRequestHasFullAccess(req *http.Request, accessKey string) bool { |
||||||
|
var headerAccessKey string |
||||||
|
if req.Header.Get(RequestHeaderEncodedB64) == "true" { |
||||||
|
headerAccessKey = encryption.DecodeString(req.Header.Get(RequestHeaderSecurityKey)) |
||||||
|
} else { |
||||||
|
headerAccessKey = req.Header.Get(RequestHeaderSecurityKey) |
||||||
|
} |
||||||
|
|
||||||
|
if headerAccessKey == "" || headerAccessKey != accessKey { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// Gets auth data from the request and rips the login string from it. Returns ""
|
||||||
|
// if there is no auth data at all
|
||||||
|
func GetUsernameFromAuth(req *http.Request) string { |
||||||
|
var authInfoStr string |
||||||
|
if req.Header.Get(RequestHeaderEncodedB64) == "true" { |
||||||
|
authInfoStr = encryption.DecodeString(req.Header.Get(RequestHeaderAuthKey)) |
||||||
|
} else { |
||||||
|
authInfoStr = req.Header.Get(RequestHeaderAuthKey) |
||||||
|
} |
||||||
|
|
||||||
|
authInfoSplit := strings.Split(authInfoStr, RequestHeaderAuthSeparator) |
||||||
|
if len(authInfoSplit) != 2 { |
||||||
|
// no separator or funny username|password
|
||||||
|
return "" |
||||||
|
} |
||||||
|
username := authInfoSplit[0] |
||||||
|
|
||||||
|
return username |
||||||
|
} |
||||||
|
|
||||||
|
// Verifies if the request contains a valid user auth information (username-password pair)
|
||||||
|
func IsRequestAuthValid(req *http.Request, db *db.DB) bool { |
||||||
|
var authInfoStr string |
||||||
|
if req.Header.Get(RequestHeaderEncodedB64) == "true" { |
||||||
|
authInfoStr = encryption.DecodeString(req.Header.Get(RequestHeaderAuthKey)) |
||||||
|
} else { |
||||||
|
authInfoStr = req.Header.Get(RequestHeaderAuthKey) |
||||||
|
} |
||||||
|
authInfoSplit := strings.Split(authInfoStr, RequestHeaderAuthSeparator) |
||||||
|
if len(authInfoSplit) != 2 { |
||||||
|
// no separator or funny id|password
|
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
username, password := authInfoSplit[0], authInfoSplit[1] |
||||||
|
user, err := db.GetUser(username) |
||||||
|
if err != nil { |
||||||
|
// does not exist
|
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if password != user.Password { |
||||||
|
// password does not match
|
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// Checks if given user owns a todo
|
||||||
|
func DoesUserOwnTodo(username string, todoID uint64, db *db.DB) bool { |
||||||
|
todo, err := db.GetTodo(todoID) |
||||||
|
if err != nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if todo.OwnerUsername != username { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// Checks if given user owns a todo group
|
||||||
|
func DoesUserOwnTodoGroup(username string, todoGroupID uint64, db *db.DB) bool { |
||||||
|
group, err := db.GetTodoGroup(todoGroupID) |
||||||
|
if err != nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if group.OwnerUsername != username { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// Retrieves todo ID from request headers
|
||||||
|
func GetTodoIDFromReq(req *http.Request) (uint64, error) { |
||||||
|
todoIDStr := encryption.DecodeString(req.Header.Get(RequestHeaderTodoIDKey)) |
||||||
|
todoID, err := strconv.ParseUint(todoIDStr, 10, 64) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
|
||||||
|
return todoID, nil |
||||||
|
} |
@ -1,772 +0,0 @@ |
|||||||
/* |
|
||||||
dela - web TODO list |
|
||||||
Copyright (C) 2023, 2024 Kasyanov Nikolay Alexeyevich (Unbewohnte) |
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify |
|
||||||
it under the terms of the GNU Affero General Public License as published by |
|
||||||
the Free Software Foundation, either version 3 of the License, or |
|
||||||
(at your option) any later version. |
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, |
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||||
GNU Affero General Public License for more details. |
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License |
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/ |
|
||||||
|
|
||||||
package server |
|
||||||
|
|
||||||
import ( |
|
||||||
"Unbewohnte/dela/db" |
|
||||||
"Unbewohnte/dela/email" |
|
||||||
"Unbewohnte/dela/logger" |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
"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.Email, err) |
|
||||||
http.Error(w, "Failed to create user", http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
logger.Info("[Server][EndpointUserCreate] Created a new user with email \"%s\"", user.Email) |
|
||||||
|
|
||||||
// Create a non-removable default category
|
|
||||||
err = s.db.CreateTodoGroup(db.NewTodoGroup( |
|
||||||
"Notes", |
|
||||||
uint64(time.Now().Unix()), |
|
||||||
user.Email, |
|
||||||
false, |
|
||||||
)) |
|
||||||
if err != nil { |
|
||||||
http.Error(w, "Failed to create default group", http.StatusInternalServerError) |
|
||||||
logger.Error("[Server][EndpointUserCreate] Failed to create a default group for %s: %s", user.Email, err) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Check if email verification is required
|
|
||||||
if !s.config.Verification.VerifyEmails { |
|
||||||
// Do not verify email
|
|
||||||
|
|
||||||
// Send cookie
|
|
||||||
http.SetCookie(w, &http.Cookie{ |
|
||||||
Name: "auth", |
|
||||||
Value: fmt.Sprintf("%s:%s", user.Email, user.Password), |
|
||||||
SameSite: http.SameSiteStrictMode, |
|
||||||
HttpOnly: false, |
|
||||||
Path: "/", |
|
||||||
Secure: false, |
|
||||||
}) |
|
||||||
|
|
||||||
// Done
|
|
||||||
w.Write([]byte("{\"confirm_email\":false}")) |
|
||||||
|
|
||||||
logger.Info("[Server][EndpointUserCreate] Successfully sent email notification to %s", user.Email) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Send email verification message
|
|
||||||
verification, err := GenerateVerificationCode(s.db, user.Email) |
|
||||||
if err != nil { |
|
||||||
logger.Error("[Server][EndpointUserCreate] Failed to generate verification code for %s: %s", user.Email, err) |
|
||||||
http.Error(w, "Failed to generate confirmation code", http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Send verification email
|
|
||||||
err = s.emailer.SendEmail( |
|
||||||
email.NewEmail( |
|
||||||
s.config.Verification.Emailer.User, |
|
||||||
"Dela: Email verification", |
|
||||||
fmt.Sprintf("Your email verification code: <b>%s</b>\nPlease, verify your email in %f hours.\nThis email was specified during Dela account creation. Ignore this message if it wasn't you", verification.Code, float32(verification.LifeSeconds)/3600), |
|
||||||
[]string{user.Email}, |
|
||||||
), |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
logger.Error("[Server][EndpointUserCreate] Failed to send verification email to %s: %s", user.Email, err) |
|
||||||
http.Error(w, "Failed to send email verification message", http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Autodelete user account if email was not verified in time
|
|
||||||
time.AfterFunc(time.Second*time.Duration(verification.LifeSeconds), func() { |
|
||||||
err = s.db.DeleteUnverifiedUserClean(user.Email) |
|
||||||
if err != nil { |
|
||||||
logger.Error("[Server][EndpointUserCreate] Failed to autodelete unverified user %s: %s", user.Email, err) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
w.Write([]byte("{\"confirm_email\":true}")) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) EndpointUserVerify(w http.ResponseWriter, req *http.Request) { |
|
||||||
if req.Method != http.MethodPost { |
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Retrieve data
|
|
||||||
defer req.Body.Close() |
|
||||||
|
|
||||||
contents, err := io.ReadAll(req.Body) |
|
||||||
if err != nil { |
|
||||||
logger.Error("[Server][EndpointUserVerify] Failed to read request body: %s", err) |
|
||||||
http.Error(w, "Failed to read request body", http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
type verificationAnswer struct { |
|
||||||
Email string `json:"email"` |
|
||||||
Code string `json:"code"` |
|
||||||
} |
|
||||||
|
|
||||||
var answer verificationAnswer |
|
||||||
err = json.Unmarshal(contents, &answer) |
|
||||||
if err != nil { |
|
||||||
logger.Error("[Server][EndpointUserVerify] Failed to unmarshal verification answer: %s", err) |
|
||||||
http.Error(w, "Verification answer JSON unmarshal error", http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Retrieve user
|
|
||||||
user, err := s.db.GetUser(answer.Email) |
|
||||||
if err != nil { |
|
||||||
logger.Error("[Server][EndpointUserVerify] Failed to retrieve information on \"%s\": %s", answer.Email, err) |
|
||||||
http.Error(w, "Failed to get user information", http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Compare codes
|
|
||||||
dbCode, err := s.db.GetVerificationByEmail(user.Email) |
|
||||||
if err != nil { |
|
||||||
logger.Error("[Server][EndpointUserVerify] Could not get verification code from DB for %s: %s", user.Email, err) |
|
||||||
http.Error(w, "Could not retrieve verification information for this email", http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if answer.Code != dbCode.Code { |
|
||||||
// Codes do not match!
|
|
||||||
logger.Error("[Server][EndpointUserVerify] %s sent wrong verification code", user.Email) |
|
||||||
http.Error(w, "Wrong verification code!", http.StatusForbidden) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// All's good!
|
|
||||||
err = s.db.UserSetEmailConfirmed(user.Email) |
|
||||||
if err != nil { |
|
||||||
http.Error(w, "Failed to save confirmation information", http.StatusInternalServerError) |
|
||||||
logger.Error("[Server][EndpointUserVerify] Failed to set confirmed_email to true for %s: %s", user.Email, err) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
logger.Info("[Server][EndpointUserVerify] %s was successfully verified!", user.Email) |
|
||||||
|
|
||||||
// Send cookie
|
|
||||||
http.SetCookie(w, &http.Cookie{ |
|
||||||
Name: "auth", |
|
||||||
Value: fmt.Sprintf("%s:%s", user.Email, user.Password), |
|
||||||
SameSite: http.SameSiteStrictMode, |
|
||||||
HttpOnly: false, |
|
||||||
Path: "/", |
|
||||||
Secure: false, |
|
||||||
}) |
|
||||||
w.WriteHeader(http.StatusOK) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) EndpointUserLogin(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][EndpointUserLogin] 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][EndpointUserLogin] Failed to unmarshal user data: %s", err) |
|
||||||
http.Error(w, "User JSON unmarshal error", http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Check auth data
|
|
||||||
if !IsUserAuthorized(s.db, user) { |
|
||||||
http.Error(w, "Failed auth", http.StatusForbidden) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Send cookie
|
|
||||||
http.SetCookie(w, &http.Cookie{ |
|
||||||
Name: "auth", |
|
||||||
Value: fmt.Sprintf("%s:%s", user.Email, user.Password), |
|
||||||
SameSite: http.SameSiteStrictMode, |
|
||||||
HttpOnly: false, |
|
||||||
Path: "/", |
|
||||||
Secure: false, |
|
||||||
}) |
|
||||||
w.WriteHeader(http.StatusOK) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) EndpointUserUpdate(w http.ResponseWriter, req *http.Request) { |
|
||||||
if req.Method != http.MethodPost { |
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Retrieve user data
|
|
||||||
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
|
|
||||||
email := GetLoginFromReq(req) |
|
||||||
if email != user.Email { |
|
||||||
// Gotcha!
|
|
||||||
logger.Warning("[Server][EndpointUserUpdate] %s tried to update user information of %s!", email, user.Email) |
|
||||||
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.Email, err) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
logger.Info("[Server][EndpointUserUpdate] Updated a user with email \"%s\"", user.Email) |
|
||||||
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
|
|
||||||
email := GetLoginFromReq(req) |
|
||||||
err := s.db.DeleteUser(email) |
|
||||||
if err != nil { |
|
||||||
http.Error(w, "Failed to delete user", http.StatusInternalServerError) |
|
||||||
logger.Error("[Server][EndpointUserDelete] Failed to delete \"%s\": %s", email, err) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
logger.Info("[Server][EndpointUserDelete] Deleted a user with email \"%s\"", email) |
|
||||||
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
|
|
||||||
email := GetLoginFromReq(req) |
|
||||||
userDB, err := s.db.GetUser(email) |
|
||||||
if err != nil { |
|
||||||
logger.Error("[Server][EndpointUserGet] Failed to retrieve information on \"%s\": %s", email, 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", email, 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, GetLoginFromReq(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) EndpointTodoMarkDone(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, GetLoginFromReq(req)) { |
|
||||||
http.Error(w, "You don't own this TODO", http.StatusForbidden) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
todo, err := s.db.GetTodo(todoID) |
|
||||||
if err != nil { |
|
||||||
http.Error(w, "Can't access this TODO", http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Update
|
|
||||||
todo.IsDone = true |
|
||||||
todo.CompletionTimeUnix = uint64(time.Now().Unix()) |
|
||||||
err = s.db.UpdateTodo(todoID, *todo) |
|
||||||
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] Marked TODO as done %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, GetLoginFromReq(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", GetLoginFromReq(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() |
|
||||||
if req.Method != http.MethodPost { |
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// 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
|
|
||||||
if newTodo.GroupID == 0 { |
|
||||||
http.Error(w, "No group ID was provided", http.StatusBadRequest) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if !s.db.DoesUserOwnGroup(newTodo.GroupID, GetLoginFromReq(req)) { |
|
||||||
http.Error(w, "You do not own this group", http.StatusForbidden) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
newTodo.OwnerEmail = GetLoginFromReq(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.OwnerEmail) |
|
||||||
} |
|
||||||
|
|
||||||
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(GetLoginFromReq(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() |
|
||||||
|
|
||||||
if req.Method != http.MethodPost { |
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Check if given user actually owns this group
|
|
||||||
if !IsUserAuthorizedReq(req, s.db) { |
|
||||||
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Get group ID
|
|
||||||
groupId, err := strconv.ParseUint(path.Base(req.URL.Path), 10, 64) |
|
||||||
if err != nil { |
|
||||||
http.Error(w, "Bad Category ID", http.StatusBadRequest) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if !s.db.DoesUserOwnGroup(groupId, GetLoginFromReq(req)) { |
|
||||||
http.Error(w, "You don't own this group", http.StatusForbidden) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
groupDB, err := s.db.GetTodoGroup(groupId) |
|
||||||
if err != nil { |
|
||||||
logger.Error("[Server][EndpointGroupDelete] Failed to fetch TODO group with Id %d: %s", groupId, err) |
|
||||||
http.Error(w, "Failed to retrieve TODO group", http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if !groupDB.Removable { |
|
||||||
// Not removable
|
|
||||||
http.Error(w, "Not removable", http.StatusBadRequest) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Delete all ToDos associated with this group and then delete the group itself
|
|
||||||
err = s.db.DeleteTodoGroupClean(groupId) |
|
||||||
if err != nil { |
|
||||||
logger.Error("[Server][EndpointGroupDelete] Failed to delete %s's TODO group: %s", GetLoginFromReq(req), err) |
|
||||||
http.Error(w, "Failed to delete TODO group", http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Success!
|
|
||||||
logger.Info("[Server][EndpointGroupDelete] Cleanly deleted group ID: %d for %s", groupId, GetLoginFromReq(req)) |
|
||||||
w.WriteHeader(http.StatusOK) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) EndpointTodoGroupCreate(w http.ResponseWriter, req *http.Request) { |
|
||||||
// Create a new TODO group
|
|
||||||
defer req.Body.Close() |
|
||||||
|
|
||||||
if req.Method != http.MethodPost { |
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// 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.OwnerEmail = GetLoginFromReq(req) |
|
||||||
newGroup.TimeCreatedUnix = uint64(time.Now().Unix()) |
|
||||||
newGroup.Removable = true |
|
||||||
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.OwnerEmail) |
|
||||||
} |
|
||||||
|
|
||||||
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(GetLoginFromReq(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) |
|
||||||
} |
|
@ -1,202 +0,0 @@ |
|||||||
|
|
||||||
Apache License |
|
||||||
Version 2.0, January 2004 |
|
||||||
http://www.apache.org/licenses/ |
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
|
||||||
|
|
||||||
1. Definitions. |
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction, |
|
||||||
and distribution as defined by Sections 1 through 9 of this document. |
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by |
|
||||||
the copyright owner that is granting the License. |
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all |
|
||||||
other entities that control, are controlled by, or are under common |
|
||||||
control with that entity. For the purposes of this definition, |
|
||||||
"control" means (i) the power, direct or indirect, to cause the |
|
||||||
direction or management of such entity, whether by contract or |
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity. |
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity |
|
||||||
exercising permissions granted by this License. |
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications, |
|
||||||
including but not limited to software source code, documentation |
|
||||||
source, and configuration files. |
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical |
|
||||||
transformation or translation of a Source form, including but |
|
||||||
not limited to compiled object code, generated documentation, |
|
||||||
and conversions to other media types. |
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or |
|
||||||
Object form, made available under the License, as indicated by a |
|
||||||
copyright notice that is included in or attached to the work |
|
||||||
(an example is provided in the Appendix below). |
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object |
|
||||||
form, that is based on (or derived from) the Work and for which the |
|
||||||
editorial revisions, annotations, elaborations, or other modifications |
|
||||||
represent, as a whole, an original work of authorship. For the purposes |
|
||||||
of this License, Derivative Works shall not include works that remain |
|
||||||
separable from, or merely link (or bind by name) to the interfaces of, |
|
||||||
the Work and Derivative Works thereof. |
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including |
|
||||||
the original version of the Work and any modifications or additions |
|
||||||
to that Work or Derivative Works thereof, that is intentionally |
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner |
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of |
|
||||||
the copyright owner. For the purposes of this definition, "submitted" |
|
||||||
means any form of electronic, verbal, or written communication sent |
|
||||||
to the Licensor or its representatives, including but not limited to |
|
||||||
communication on electronic mailing lists, source code control systems, |
|
||||||
and issue tracking systems that are managed by, or on behalf of, the |
|
||||||
Licensor for the purpose of discussing and improving the Work, but |
|
||||||
excluding communication that is conspicuously marked or otherwise |
|
||||||
designated in writing by the copyright owner as "Not a Contribution." |
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity |
|
||||||
on behalf of whom a Contribution has been received by Licensor and |
|
||||||
subsequently incorporated within the Work. |
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of |
|
||||||
this License, each Contributor hereby grants to You a perpetual, |
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
|
||||||
copyright license to reproduce, prepare Derivative Works of, |
|
||||||
publicly display, publicly perform, sublicense, and distribute the |
|
||||||
Work and such Derivative Works in Source or Object form. |
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of |
|
||||||
this License, each Contributor hereby grants to You a perpetual, |
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
|
||||||
(except as stated in this section) patent license to make, have made, |
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work, |
|
||||||
where such license applies only to those patent claims licensable |
|
||||||
by such Contributor that are necessarily infringed by their |
|
||||||
Contribution(s) alone or by combination of their Contribution(s) |
|
||||||
with the Work to which such Contribution(s) was submitted. If You |
|
||||||
institute patent litigation against any entity (including a |
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work |
|
||||||
or a Contribution incorporated within the Work constitutes direct |
|
||||||
or contributory patent infringement, then any patent licenses |
|
||||||
granted to You under this License for that Work shall terminate |
|
||||||
as of the date such litigation is filed. |
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the |
|
||||||
Work or Derivative Works thereof in any medium, with or without |
|
||||||
modifications, and in Source or Object form, provided that You |
|
||||||
meet the following conditions: |
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or |
|
||||||
Derivative Works a copy of this License; and |
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices |
|
||||||
stating that You changed the files; and |
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works |
|
||||||
that You distribute, all copyright, patent, trademark, and |
|
||||||
attribution notices from the Source form of the Work, |
|
||||||
excluding those notices that do not pertain to any part of |
|
||||||
the Derivative Works; and |
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its |
|
||||||
distribution, then any Derivative Works that You distribute must |
|
||||||
include a readable copy of the attribution notices contained |
|
||||||
within such NOTICE file, excluding those notices that do not |
|
||||||
pertain to any part of the Derivative Works, in at least one |
|
||||||
of the following places: within a NOTICE text file distributed |
|
||||||
as part of the Derivative Works; within the Source form or |
|
||||||
documentation, if provided along with the Derivative Works; or, |
|
||||||
within a display generated by the Derivative Works, if and |
|
||||||
wherever such third-party notices normally appear. The contents |
|
||||||
of the NOTICE file are for informational purposes only and |
|
||||||
do not modify the License. You may add Your own attribution |
|
||||||
notices within Derivative Works that You distribute, alongside |
|
||||||
or as an addendum to the NOTICE text from the Work, provided |
|
||||||
that such additional attribution notices cannot be construed |
|
||||||
as modifying the License. |
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and |
|
||||||
may provide additional or different license terms and conditions |
|
||||||
for use, reproduction, or distribution of Your modifications, or |
|
||||||
for any such Derivative Works as a whole, provided Your use, |
|
||||||
reproduction, and distribution of the Work otherwise complies with |
|
||||||
the conditions stated in this License. |
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise, |
|
||||||
any Contribution intentionally submitted for inclusion in the Work |
|
||||||
by You to the Licensor shall be under the terms and conditions of |
|
||||||
this License, without any additional terms or conditions. |
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify |
|
||||||
the terms of any separate license agreement you may have executed |
|
||||||
with Licensor regarding such Contributions. |
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade |
|
||||||
names, trademarks, service marks, or product names of the Licensor, |
|
||||||
except as required for reasonable and customary use in describing the |
|
||||||
origin of the Work and reproducing the content of the NOTICE file. |
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or |
|
||||||
agreed to in writing, Licensor provides the Work (and each |
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS, |
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
|
||||||
implied, including, without limitation, any warranties or conditions |
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the |
|
||||||
appropriateness of using or redistributing the Work and assume any |
|
||||||
risks associated with Your exercise of permissions under this License. |
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory, |
|
||||||
whether in tort (including negligence), contract, or otherwise, |
|
||||||
unless required by applicable law (such as deliberate and grossly |
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be |
|
||||||
liable to You for damages, including any direct, indirect, special, |
|
||||||
incidental, or consequential damages of any character arising as a |
|
||||||
result of this License or out of the use or inability to use the |
|
||||||
Work (including but not limited to damages for loss of goodwill, |
|
||||||
work stoppage, computer failure or malfunction, or any and all |
|
||||||
other commercial damages or losses), even if such Contributor |
|
||||||
has been advised of the possibility of such damages. |
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing |
|
||||||
the Work or Derivative Works thereof, You may choose to offer, |
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity, |
|
||||||
or other liability obligations and/or rights consistent with this |
|
||||||
License. However, in accepting such obligations, You may act only |
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf |
|
||||||
of any other Contributor, and only if You agree to indemnify, |
|
||||||
defend, and hold each Contributor harmless for any liability |
|
||||||
incurred by, or claims asserted against, such Contributor by reason |
|
||||||
of your accepting any such warranty or additional liability. |
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS |
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work. |
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following |
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]" |
|
||||||
replaced with your own identifying information. (Don't include |
|
||||||
the brackets!) The text should be enclosed in the appropriate |
|
||||||
comment syntax for the file format. We also recommend that a |
|
||||||
file or class name and description of purpose be included on the |
|
||||||
same "printed page" as the copyright notice for easier |
|
||||||
identification within third-party archives. |
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner] |
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||||
you may not use this file except in compliance with the License. |
|
||||||
You may obtain a copy of the License at |
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0 |
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software |
|
||||||
distributed under the License is distributed on an "AS IS" BASIS, |
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||||
See the License for the specific language governing permissions and |
|
||||||
limitations under the License. |
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 51 KiB |
Loading…
Reference in new issue