Unbewohnte
2 years ago
commit
bbcb76d05e
71 changed files with 62050 additions and 0 deletions
@ -0,0 +1,12 @@
|
||||
all: |
||||
mkdir -p bin && \
|
||||
cd src && CGO_ENABLED=0 go build && mv dela ../bin && \
|
||||
cd .. && \
|
||||
cp -r pages bin && \
|
||||
cp -r scripts bin && \
|
||||
cp -r static bin
|
||||
|
||||
|
||||
clean: |
||||
rm -rf bin
|
||||
|
@ -0,0 +1,6 @@
|
||||
# dela - daily events list application (pun интедед) |
||||
|
||||
|
||||
## License |
||||
|
||||
AGPLv3 |
@ -0,0 +1,78 @@
|
||||
{{ define "base" }} |
||||
|
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
|
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<title>dela</title> |
||||
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> |
||||
<script src="/static/bootstrap/js/bootstrap.min.js"></script> |
||||
<!-- <link rel="stylesheet" href="/static/stylesheet.css"> --> |
||||
<link rel="shortcut icon" href="/static/images/favicon.png" type="image/x-icon"> |
||||
</head> |
||||
|
||||
<body class="d-flex flex-column h-100"> |
||||
<header class="d-flex flex-wrap align-items-center justify-content-center justify-content-md-between py-3 mb-4 border-bottom"> |
||||
<div class="col-md-3 mb-2 mb-md-0"> |
||||
<a href="/" class="d-inline-flex link-body-emphasis text-decoration-none"> |
||||
<svg class="bi" width="40" height="32" role="img" aria-label="Bootstrap"><use xlink:href="#bootstrap"></use></svg> |
||||
</a> |
||||
</div> |
||||
|
||||
<!-- <ul class="nav col-12 col-md-auto mb-2 justify-content-center mb-md-0"> |
||||
<li><a href="/" class="nav-link px-2">Home</a></li> |
||||
<li><a href="/about" class="nav-link px-2">About</a></li> |
||||
<li><span href="#" class="nav-link px-2" id="logged-in-as"></span></li> |
||||
</ul> --> |
||||
|
||||
<div class="col-md-3 text-end" id="bar-auth"> |
||||
<a href="/login" class="btn btn-outline-primary me-2">Log in</a> |
||||
<a href="/register" class="btn btn-primary">Register</a> |
||||
</div> |
||||
</header> |
||||
|
||||
<div style="margin: auto; |
||||
margin-top: 5ch; |
||||
margin-bottom: 10ch; |
||||
max-width: 120ch;"> |
||||
{{ template "content" . }} |
||||
</div> |
||||
</body> |
||||
|
||||
</html> |
||||
|
||||
<script src="/scripts/auth.js"></script> |
||||
<script> |
||||
window.onload = async () => { |
||||
let username = getUsername(); |
||||
let password = getUserPassword(); |
||||
|
||||
if (username == null | username == "") { |
||||
return; |
||||
} |
||||
|
||||
// Check if auth info is indeed valid |
||||
let response = await fetch("/api/user", { |
||||
method: "GET", |
||||
headers: { |
||||
"EncryptedBase64": "false", |
||||
"Auth": username + "<-->" + password |
||||
}, |
||||
}); |
||||
|
||||
if (response.ok) { |
||||
let barAuth = document.getElementById("bar-auth"); |
||||
barAuth.innerHTML = "<b>" + username + "</b>" + " | "; |
||||
barAuth.innerHTML += '<button id="log-out-btn" class="btn btn-primary" type="button">Log out</button>'; |
||||
document.getElementById("log-out-btn").addEventListener("click", (event) => { |
||||
// Log out |
||||
forgetAuthInfo(); |
||||
window.location.replace("/"); |
||||
}); |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
{{ end }} |
@ -0,0 +1,15 @@
|
||||
{{ template "base" . }} |
||||
|
||||
{{ define "content" }} |
||||
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column"> |
||||
<main class="px-3"> |
||||
<h1>Dela.</h1> |
||||
<p class="lead">A free and open-source web TODO list</p> |
||||
<p class="lead"> |
||||
<a href="/login" class="btn btn-lg btn-primary">Login</a> |
||||
<a href="/register" class="btn btn-lg btn-primary">Register</a> |
||||
</p> |
||||
</main> |
||||
</div> |
||||
|
||||
{{ end }} |
@ -0,0 +1,9 @@
|
||||
{{ template "base" . }} |
||||
|
||||
{{ define "content" }} |
||||
|
||||
<script> |
||||
|
||||
</script> |
||||
|
||||
{{ end }} |
@ -0,0 +1,58 @@
|
||||
{{ template "base" . }} |
||||
|
||||
{{ define "content" }} |
||||
|
||||
<h3>Log in</h3> |
||||
|
||||
<form name="loginForm" onsubmit="return false;"> |
||||
<p> |
||||
<label for="username" class="form-label">Username</label> <br> |
||||
<input type="text" name="username" minlength="3" required> |
||||
</p> |
||||
|
||||
<p> |
||||
<label for="password" class="form-label">Password</label> <br> |
||||
<input type="password" name="password" minlength="3" required> |
||||
</p> |
||||
|
||||
<p><span id="error_message" class="text-danger"></span></p> |
||||
|
||||
<p> |
||||
<input type="submit" value="Log in" class="btn btn-primary" onmouseup="logIn()"> |
||||
</p> |
||||
</form> |
||||
|
||||
<script> |
||||
async function logIn() { |
||||
let loginForm = document.forms["loginForm"]; |
||||
|
||||
let username = String(loginForm.elements["username"].value).trim(); |
||||
if (username.length < 3) { |
||||
return; |
||||
} |
||||
|
||||
let password = String(loginForm.elements["password"].value); |
||||
if (password.length < 3) { |
||||
return; |
||||
} |
||||
password = sha256(password); |
||||
|
||||
// Check if auth info is indeed valid |
||||
let response = await fetch("/api/user", { |
||||
method: "GET", |
||||
headers: { |
||||
"EncryptedBase64": "false", |
||||
"Auth": username + "<-->" + password |
||||
}, |
||||
}); |
||||
|
||||
if (response.ok) { |
||||
rememberAuthInfo(username, password); |
||||
window.location.replace("/"); |
||||
} else { |
||||
document.getElementById("error_message").innerText = await response.text(); |
||||
} |
||||
} |
||||
|
||||
</script> |
||||
{{ end }} |
@ -0,0 +1,60 @@
|
||||
{{ template "base" . }} |
||||
|
||||
{{ define "content" }} |
||||
|
||||
<h3>Register</h3> |
||||
<form name="registerForm" onsubmit="return false;"> |
||||
<p> |
||||
<label for="username" class="form-label">Username</label> <br> |
||||
<input type="text" name="username" minlength="3" required> |
||||
</p> |
||||
|
||||
<p> |
||||
<label for="password" class="form-label">Password</label> <br> |
||||
<input type="password" name="password" minlength="3" required> |
||||
</p> |
||||
|
||||
<p><span id="error_message" class="text-danger"></span></p> |
||||
|
||||
<p> |
||||
<input type="submit" value="Register" class="btn btn-primary" onmouseup="register();"> |
||||
</p> |
||||
</form> |
||||
|
||||
<script> |
||||
async function register() { |
||||
let registerForm = document.forms["registerForm"]; |
||||
|
||||
let username = String(registerForm.elements["username"].value).trim(); |
||||
if (username.length < 3) { |
||||
return; |
||||
} |
||||
|
||||
let password = String(registerForm.elements["password"].value); |
||||
if (password.length < 3) { |
||||
return; |
||||
} |
||||
|
||||
let passwordSHA256 = sha256(password); |
||||
let postData = { |
||||
username: username, |
||||
password: passwordSHA256, |
||||
}; |
||||
|
||||
let response = await fetch("/api/user", { |
||||
method: "POST", |
||||
headers: { |
||||
"Content-Type": "application/json", |
||||
}, |
||||
body: JSON.stringify(postData), |
||||
}); |
||||
|
||||
if (response.ok) { |
||||
rememberAuthInfo(postData.username, postData.password); |
||||
window.location.replace("/"); |
||||
} else { |
||||
document.getElementById("error_message").innerText = await response.text(); |
||||
} |
||||
}; |
||||
</script> |
||||
{{ end }} |
@ -0,0 +1,569 @@
|
||||
/* |
||||
Copyright (c) 2023 Kasyanov Nikolay Alexeyevich (Unbewohnte) |
||||
*/ |
||||
|
||||
|
||||
const AuthHeaderKey = "Auth"; |
||||
const AuthSeparator = "<->" // username<->password
|
||||
const authStorageUsername = "username"; |
||||
const authStoragePassword = "password"; |
||||
|
||||
|
||||
function encodeStringBase64(string) { |
||||
return btoa( |
||||
encodeURIComponent(string).replace(/%([0-9A-F]{2})/g, function to_bytes(match, p) { |
||||
return String.fromCharCode('0x' + p); |
||||
}) |
||||
); |
||||
} |
||||
|
||||
// Returns properly constructed string containing authentication information
|
||||
function authString(login, password) { |
||||
return String(login) + String(AuthSeparator) + String(password); |
||||
} |
||||
|
||||
// Returns properly constructed string containing authentication information, encodes afterwards
|
||||
function authStringEncoded(login, password) { |
||||
return encodeStringBase64(authString(login, password)); |
||||
} |
||||
|
||||
// Saves auth information to local storage
|
||||
function rememberAuthInfo(username, password) { |
||||
localStorage.setItem(authStorageUsername, username); |
||||
localStorage.setItem(authStoragePassword, password); |
||||
} |
||||
|
||||
// Retrieves user's login from local storage
|
||||
function getUsername() { |
||||
return localStorage.getItem(authStorageUsername); |
||||
} |
||||
|
||||
// Retrieves user's password from local storage
|
||||
function getUserPassword() { |
||||
return localStorage.getItem(authStoragePassword); |
||||
} |
||||
|
||||
// Removes all auth information from local storage
|
||||
function forgetAuthInfo() { |
||||
localStorage.removeItem(authStorageUsername); |
||||
localStorage.removeItem(authStoragePassword); |
||||
} |
||||
|
||||
/** |
||||
* [js-sha256]{@link https://github.com/emn178/js-sha256}
|
||||
* |
||||
* @version 0.9.0 |
||||
* @author Chen, Yi-Cyuan [emn178@gmail.com] |
||||
* @copyright Chen, Yi-Cyuan 2014-2017 |
||||
* @license MIT |
||||
*/ |
||||
/*jslint bitwise: true */ |
||||
(function () { |
||||
'use strict'; |
||||
|
||||
var ERROR = 'input is invalid type'; |
||||
var WINDOW = typeof window === 'object'; |
||||
var root = WINDOW ? window : {}; |
||||
if (root.JS_SHA256_NO_WINDOW) { |
||||
WINDOW = false; |
||||
} |
||||
var WEB_WORKER = !WINDOW && typeof self === 'object'; |
||||
var NODE_JS = !root.JS_SHA256_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node; |
||||
if (NODE_JS) { |
||||
root = global; |
||||
} else if (WEB_WORKER) { |
||||
root = self; |
||||
} |
||||
var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && typeof module === 'object' && module.exports; |
||||
var AMD = typeof define === 'function' && define.amd; |
||||
var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined'; |
||||
var HEX_CHARS = '0123456789abcdef'.split(''); |
||||
var EXTRA = [-2147483648, 8388608, 32768, 128]; |
||||
var SHIFT = [24, 16, 8, 0]; |
||||
var K = [ |
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, |
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, |
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, |
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, |
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, |
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, |
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, |
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 |
||||
]; |
||||
var OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer']; |
||||
|
||||
var blocks = []; |
||||
|
||||
if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) { |
||||
Array.isArray = function (obj) { |
||||
return Object.prototype.toString.call(obj) === '[object Array]'; |
||||
}; |
||||
} |
||||
|
||||
if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) { |
||||
ArrayBuffer.isView = function (obj) { |
||||
return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer; |
||||
}; |
||||
} |
||||
|
||||
var createOutputMethod = function (outputType, is224) { |
||||
return function (message) { |
||||
return new Sha256(is224, true).update(message)[outputType](); |
||||
}; |
||||
}; |
||||
|
||||
var createMethod = function (is224) { |
||||
var method = createOutputMethod('hex', is224); |
||||
if (NODE_JS) { |
||||
method = nodeWrap(method, is224); |
||||
} |
||||
method.create = function () { |
||||
return new Sha256(is224); |
||||
}; |
||||
method.update = function (message) { |
||||
return method.create().update(message); |
||||
}; |
||||
for (var i = 0; i < OUTPUT_TYPES.length; ++i) { |
||||
var type = OUTPUT_TYPES[i]; |
||||
method[type] = createOutputMethod(type, is224); |
||||
} |
||||
return method; |
||||
}; |
||||
|
||||
var nodeWrap = function (method, is224) { |
||||
var crypto = eval("require('crypto')"); |
||||
var Buffer = eval("require('buffer').Buffer"); |
||||
var algorithm = is224 ? 'sha224' : 'sha256'; |
||||
var nodeMethod = function (message) { |
||||
if (typeof message === 'string') { |
||||
return crypto.createHash(algorithm).update(message, 'utf8').digest('hex'); |
||||
} else { |
||||
if (message === null || message === undefined) { |
||||
throw new Error(ERROR); |
||||
} else if (message.constructor === ArrayBuffer) { |
||||
message = new Uint8Array(message); |
||||
} |
||||
} |
||||
if (Array.isArray(message) || ArrayBuffer.isView(message) || |
||||
message.constructor === Buffer) { |
||||
return crypto.createHash(algorithm).update(new Buffer(message)).digest('hex'); |
||||
} else { |
||||
return method(message); |
||||
} |
||||
}; |
||||
return nodeMethod; |
||||
}; |
||||
|
||||
var createHmacOutputMethod = function (outputType, is224) { |
||||
return function (key, message) { |
||||
return new HmacSha256(key, is224, true).update(message)[outputType](); |
||||
}; |
||||
}; |
||||
|
||||
var createHmacMethod = function (is224) { |
||||
var method = createHmacOutputMethod('hex', is224); |
||||
method.create = function (key) { |
||||
return new HmacSha256(key, is224); |
||||
}; |
||||
method.update = function (key, message) { |
||||
return method.create(key).update(message); |
||||
}; |
||||
for (var i = 0; i < OUTPUT_TYPES.length; ++i) { |
||||
var type = OUTPUT_TYPES[i]; |
||||
method[type] = createHmacOutputMethod(type, is224); |
||||
} |
||||
return method; |
||||
}; |
||||
|
||||
function Sha256(is224, sharedMemory) { |
||||
if (sharedMemory) { |
||||
blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = |
||||
blocks[4] = blocks[5] = blocks[6] = blocks[7] = |
||||
blocks[8] = blocks[9] = blocks[10] = blocks[11] = |
||||
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; |
||||
this.blocks = blocks; |
||||
} else { |
||||
this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; |
||||
} |
||||
|
||||
if (is224) { |
||||
this.h0 = 0xc1059ed8; |
||||
this.h1 = 0x367cd507; |
||||
this.h2 = 0x3070dd17; |
||||
this.h3 = 0xf70e5939; |
||||
this.h4 = 0xffc00b31; |
||||
this.h5 = 0x68581511; |
||||
this.h6 = 0x64f98fa7; |
||||
this.h7 = 0xbefa4fa4; |
||||
} else { // 256
|
||||
this.h0 = 0x6a09e667; |
||||
this.h1 = 0xbb67ae85; |
||||
this.h2 = 0x3c6ef372; |
||||
this.h3 = 0xa54ff53a; |
||||
this.h4 = 0x510e527f; |
||||
this.h5 = 0x9b05688c; |
||||
this.h6 = 0x1f83d9ab; |
||||
this.h7 = 0x5be0cd19; |
||||
} |
||||
|
||||
this.block = this.start = this.bytes = this.hBytes = 0; |
||||
this.finalized = this.hashed = false; |
||||
this.first = true; |
||||
this.is224 = is224; |
||||
} |
||||
|
||||
Sha256.prototype.update = function (message) { |
||||
if (this.finalized) { |
||||
return; |
||||
} |
||||
var notString, type = typeof message; |
||||
if (type !== 'string') { |
||||
if (type === 'object') { |
||||
if (message === null) { |
||||
throw new Error(ERROR); |
||||
} else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) { |
||||
message = new Uint8Array(message); |
||||
} else if (!Array.isArray(message)) { |
||||
if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) { |
||||
throw new Error(ERROR); |
||||
} |
||||
} |
||||
} else { |
||||
throw new Error(ERROR); |
||||
} |
||||
notString = true; |
||||
} |
||||
var code, index = 0, i, length = message.length, blocks = this.blocks; |
||||
|
||||
while (index < length) { |
||||
if (this.hashed) { |
||||
this.hashed = false; |
||||
blocks[0] = this.block; |
||||
blocks[16] = blocks[1] = blocks[2] = blocks[3] = |
||||
blocks[4] = blocks[5] = blocks[6] = blocks[7] = |
||||
blocks[8] = blocks[9] = blocks[10] = blocks[11] = |
||||
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; |
||||
} |
||||
|
||||
if (notString) { |
||||
for (i = this.start; index < length && i < 64; ++index) { |
||||
blocks[i >> 2] |= message[index] << SHIFT[i++ & 3]; |
||||
} |
||||
} else { |
||||
for (i = this.start; index < length && i < 64; ++index) { |
||||
code = message.charCodeAt(index); |
||||
if (code < 0x80) { |
||||
blocks[i >> 2] |= code << SHIFT[i++ & 3]; |
||||
} else if (code < 0x800) { |
||||
blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; |
||||
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; |
||||
} else if (code < 0xd800 || code >= 0xe000) { |
||||
blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; |
||||
blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; |
||||
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; |
||||
} else { |
||||
code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); |
||||
blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; |
||||
blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; |
||||
blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; |
||||
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
this.lastByteIndex = i; |
||||
this.bytes += i - this.start; |
||||
if (i >= 64) { |
||||
this.block = blocks[16]; |
||||
this.start = i - 64; |
||||
this.hash(); |
||||
this.hashed = true; |
||||
} else { |
||||
this.start = i; |
||||
} |
||||
} |
||||
if (this.bytes > 4294967295) { |
||||
this.hBytes += this.bytes / 4294967296 << 0; |
||||
this.bytes = this.bytes % 4294967296; |
||||
} |
||||
return this; |
||||
}; |
||||
|
||||
Sha256.prototype.finalize = function () { |
||||
if (this.finalized) { |
||||
return; |
||||
} |
||||
this.finalized = true; |
||||
var blocks = this.blocks, i = this.lastByteIndex; |
||||
blocks[16] = this.block; |
||||
blocks[i >> 2] |= EXTRA[i & 3]; |
||||
this.block = blocks[16]; |
||||
if (i >= 56) { |
||||
if (!this.hashed) { |
||||
this.hash(); |
||||
} |
||||
blocks[0] = this.block; |
||||
blocks[16] = blocks[1] = blocks[2] = blocks[3] = |
||||
blocks[4] = blocks[5] = blocks[6] = blocks[7] = |
||||
blocks[8] = blocks[9] = blocks[10] = blocks[11] = |
||||
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; |
||||
} |
||||
blocks[14] = this.hBytes << 3 | this.bytes >>> 29; |
||||
blocks[15] = this.bytes << 3; |
||||
this.hash(); |
||||
}; |
||||
|
||||
Sha256.prototype.hash = function () { |
||||
var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6, |
||||
h = this.h7, blocks = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc; |
||||
|
||||
for (j = 16; j < 64; ++j) { |
||||
// rightrotate
|
||||
t1 = blocks[j - 15]; |
||||
s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3); |
||||
t1 = blocks[j - 2]; |
||||
s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10); |
||||
blocks[j] = blocks[j - 16] + s0 + blocks[j - 7] + s1 << 0; |
||||
} |
||||
|
||||
bc = b & c; |
||||
for (j = 0; j < 64; j += 4) { |
||||
if (this.first) { |
||||
if (this.is224) { |
||||
ab = 300032; |
||||
t1 = blocks[0] - 1413257819; |
||||
h = t1 - 150054599 << 0; |
||||
d = t1 + 24177077 << 0; |
||||
} else { |
||||
ab = 704751109; |
||||
t1 = blocks[0] - 210244248; |
||||
h = t1 - 1521486534 << 0; |
||||
d = t1 + 143694565 << 0; |
||||
} |
||||
this.first = false; |
||||
} else { |
||||
s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10)); |
||||
s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7)); |
||||
ab = a & b; |
||||
maj = ab ^ (a & c) ^ bc; |
||||
ch = (e & f) ^ (~e & g); |
||||
t1 = h + s1 + ch + K[j] + blocks[j]; |
||||
t2 = s0 + maj; |
||||
h = d + t1 << 0; |
||||
d = t1 + t2 << 0; |
||||
} |
||||
s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10)); |
||||
s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7)); |
||||
da = d & a; |
||||
maj = da ^ (d & b) ^ ab; |
||||
ch = (h & e) ^ (~h & f); |
||||
t1 = g + s1 + ch + K[j + 1] + blocks[j + 1]; |
||||
t2 = s0 + maj; |
||||
g = c + t1 << 0; |
||||
c = t1 + t2 << 0; |
||||
s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10)); |
||||
s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7)); |
||||
cd = c & d; |
||||
maj = cd ^ (c & a) ^ da; |
||||
ch = (g & h) ^ (~g & e); |
||||
t1 = f + s1 + ch + K[j + 2] + blocks[j + 2]; |
||||
t2 = s0 + maj; |
||||
f = b + t1 << 0; |
||||
b = t1 + t2 << 0; |
||||
s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10)); |
||||
s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7)); |
||||
bc = b & c; |
||||
maj = bc ^ (b & d) ^ cd; |
||||
ch = (f & g) ^ (~f & h); |
||||
t1 = e + s1 + ch + K[j + 3] + blocks[j + 3]; |
||||
t2 = s0 + maj; |
||||
e = a + t1 << 0; |
||||
a = t1 + t2 << 0; |
||||
} |
||||
|
||||
this.h0 = this.h0 + a << 0; |
||||
this.h1 = this.h1 + b << 0; |
||||
this.h2 = this.h2 + c << 0; |
||||
this.h3 = this.h3 + d << 0; |
||||
this.h4 = this.h4 + e << 0; |
||||
this.h5 = this.h5 + f << 0; |
||||
this.h6 = this.h6 + g << 0; |
||||
this.h7 = this.h7 + h << 0; |
||||
}; |
||||
|
||||
Sha256.prototype.hex = function () { |
||||
this.finalize(); |
||||
|
||||
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, |
||||
h6 = this.h6, h7 = this.h7; |
||||
|
||||
var hex = HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] + |
||||
HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] + |
||||
HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] + |
||||
HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] + |
||||
HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] + |
||||
HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] + |
||||
HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] + |
||||
HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] + |
||||
HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] + |
||||
HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] + |
||||
HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] + |
||||
HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] + |
||||
HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F] + |
||||
HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] + |
||||
HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] + |
||||
HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] + |
||||
HEX_CHARS[(h4 >> 28) & 0x0F] + HEX_CHARS[(h4 >> 24) & 0x0F] + |
||||
HEX_CHARS[(h4 >> 20) & 0x0F] + HEX_CHARS[(h4 >> 16) & 0x0F] + |
||||
HEX_CHARS[(h4 >> 12) & 0x0F] + HEX_CHARS[(h4 >> 8) & 0x0F] + |
||||
HEX_CHARS[(h4 >> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F] + |
||||
HEX_CHARS[(h5 >> 28) & 0x0F] + HEX_CHARS[(h5 >> 24) & 0x0F] + |
||||
HEX_CHARS[(h5 >> 20) & 0x0F] + HEX_CHARS[(h5 >> 16) & 0x0F] + |
||||
HEX_CHARS[(h5 >> 12) & 0x0F] + HEX_CHARS[(h5 >> 8) & 0x0F] + |
||||
HEX_CHARS[(h5 >> 4) & 0x0F] + HEX_CHARS[h5 & 0x0F] + |
||||
HEX_CHARS[(h6 >> 28) & 0x0F] + HEX_CHARS[(h6 >> 24) & 0x0F] + |
||||
HEX_CHARS[(h6 >> 20) & 0x0F] + HEX_CHARS[(h6 >> 16) & 0x0F] + |
||||
HEX_CHARS[(h6 >> 12) & 0x0F] + HEX_CHARS[(h6 >> 8) & 0x0F] + |
||||
HEX_CHARS[(h6 >> 4) & 0x0F] + HEX_CHARS[h6 & 0x0F]; |
||||
if (!this.is224) { |
||||
hex += HEX_CHARS[(h7 >> 28) & 0x0F] + HEX_CHARS[(h7 >> 24) & 0x0F] + |
||||
HEX_CHARS[(h7 >> 20) & 0x0F] + HEX_CHARS[(h7 >> 16) & 0x0F] + |
||||
HEX_CHARS[(h7 >> 12) & 0x0F] + HEX_CHARS[(h7 >> 8) & 0x0F] + |
||||
HEX_CHARS[(h7 >> 4) & 0x0F] + HEX_CHARS[h7 & 0x0F]; |
||||
} |
||||
return hex; |
||||
}; |
||||
|
||||
Sha256.prototype.toString = Sha256.prototype.hex; |
||||
|
||||
Sha256.prototype.digest = function () { |
||||
this.finalize(); |
||||
|
||||
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, |
||||
h6 = this.h6, h7 = this.h7; |
||||
|
||||
var arr = [ |
||||
(h0 >> 24) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 8) & 0xFF, h0 & 0xFF, |
||||
(h1 >> 24) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 8) & 0xFF, h1 & 0xFF, |
||||
(h2 >> 24) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 8) & 0xFF, h2 & 0xFF, |
||||
(h3 >> 24) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 8) & 0xFF, h3 & 0xFF, |
||||
(h4 >> 24) & 0xFF, (h4 >> 16) & 0xFF, (h4 >> 8) & 0xFF, h4 & 0xFF, |
||||
(h5 >> 24) & 0xFF, (h5 >> 16) & 0xFF, (h5 >> 8) & 0xFF, h5 & 0xFF, |
||||
(h6 >> 24) & 0xFF, (h6 >> 16) & 0xFF, (h6 >> 8) & 0xFF, h6 & 0xFF |
||||
]; |
||||
if (!this.is224) { |
||||
arr.push((h7 >> 24) & 0xFF, (h7 >> 16) & 0xFF, (h7 >> 8) & 0xFF, h7 & 0xFF); |
||||
} |
||||
return arr; |
||||
}; |
||||
|
||||
Sha256.prototype.array = Sha256.prototype.digest; |
||||
|
||||
Sha256.prototype.arrayBuffer = function () { |
||||
this.finalize(); |
||||
|
||||
var buffer = new ArrayBuffer(this.is224 ? 28 : 32); |
||||
var dataView = new DataView(buffer); |
||||
dataView.setUint32(0, this.h0); |
||||
dataView.setUint32(4, this.h1); |
||||
dataView.setUint32(8, this.h2); |
||||
dataView.setUint32(12, this.h3); |
||||
dataView.setUint32(16, this.h4); |
||||
dataView.setUint32(20, this.h5); |
||||
dataView.setUint32(24, this.h6); |
||||
if (!this.is224) { |
||||
dataView.setUint32(28, this.h7); |
||||
} |
||||
return buffer; |
||||
}; |
||||
|
||||
function HmacSha256(key, is224, sharedMemory) { |
||||
var i, type = typeof key; |
||||
if (type === 'string') { |
||||
var bytes = [], length = key.length, index = 0, code; |
||||
for (i = 0; i < length; ++i) { |
||||
code = key.charCodeAt(i); |
||||
if (code < 0x80) { |
||||
bytes[index++] = code; |
||||
} else if (code < 0x800) { |
||||
bytes[index++] = (0xc0 | (code >> 6)); |
||||
bytes[index++] = (0x80 | (code & 0x3f)); |
||||
} else if (code < 0xd800 || code >= 0xe000) { |
||||
bytes[index++] = (0xe0 | (code >> 12)); |
||||
bytes[index++] = (0x80 | ((code >> 6) & 0x3f)); |
||||
bytes[index++] = (0x80 | (code & 0x3f)); |
||||
} else { |
||||
code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff)); |
||||
bytes[index++] = (0xf0 | (code >> 18)); |
||||
bytes[index++] = (0x80 | ((code >> 12) & 0x3f)); |
||||
bytes[index++] = (0x80 | ((code >> 6) & 0x3f)); |
||||
bytes[index++] = (0x80 | (code & 0x3f)); |
||||
} |
||||
} |
||||
key = bytes; |
||||
} else { |
||||
if (type === 'object') { |
||||
if (key === null) { |
||||
throw new Error(ERROR); |
||||
} else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) { |
||||
key = new Uint8Array(key); |
||||
} else if (!Array.isArray(key)) { |
||||
if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) { |
||||
throw new Error(ERROR); |
||||
} |
||||
} |
||||
} else { |
||||
throw new Error(ERROR); |
||||
} |
||||
} |
||||
|
||||
if (key.length > 64) { |
||||
key = (new Sha256(is224, true)).update(key).array(); |
||||
} |
||||
|
||||
var oKeyPad = [], iKeyPad = []; |
||||
for (i = 0; i < 64; ++i) { |
||||
var b = key[i] || 0; |
||||
oKeyPad[i] = 0x5c ^ b; |
||||
iKeyPad[i] = 0x36 ^ b; |
||||
} |
||||
|
||||
Sha256.call(this, is224, sharedMemory); |
||||
|
||||
this.update(iKeyPad); |
||||
this.oKeyPad = oKeyPad; |
||||
this.inner = true; |
||||
this.sharedMemory = sharedMemory; |
||||
} |
||||
HmacSha256.prototype = new Sha256(); |
||||
|
||||
HmacSha256.prototype.finalize = function () { |
||||
Sha256.prototype.finalize.call(this); |
||||
if (this.inner) { |
||||
this.inner = false; |
||||
var innerHash = this.array(); |
||||
Sha256.call(this, this.is224, this.sharedMemory); |
||||
this.update(this.oKeyPad); |
||||
this.update(innerHash); |
||||
Sha256.prototype.finalize.call(this); |
||||
} |
||||
}; |
||||
|
||||
var exports = createMethod(); |
||||
exports.sha256 = exports; |
||||
exports.sha224 = createMethod(true); |
||||
exports.sha256.hmac = createHmacMethod(); |
||||
exports.sha224.hmac = createHmacMethod(true); |
||||
|
||||
if (COMMON_JS) { |
||||
module.exports = exports; |
||||
} else { |
||||
root.sha256 = exports.sha256; |
||||
root.sha224 = exports.sha224; |
||||
if (AMD) { |
||||
define(function () { |
||||
return exports; |
||||
}); |
||||
} |
||||
} |
||||
})(); |
@ -0,0 +1,69 @@
|
||||
package conf |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"io" |
||||
"os" |
||||
) |
||||
|
||||
type Conf struct { |
||||
Port uint16 `json:"port"` |
||||
CertFilePath string `json:"cert_file_path"` |
||||
KeyFilePath string `json:"key_file_path"` |
||||
BaseContentDir string `json:"base_content_dir"` |
||||
ProdDBName string `json:"production_db"` |
||||
} |
||||
|
||||
// Creates a default server configuration
|
||||
func Default() Conf { |
||||
return Conf{ |
||||
Port: 8080, |
||||
CertFilePath: "", |
||||
KeyFilePath: "", |
||||
BaseContentDir: ".", |
||||
ProdDBName: "dela.db", |
||||
} |
||||
} |
||||
|
||||
// Tries to retrieve configuration from given json file
|
||||
func FromFile(path string) (Conf, error) { |
||||
configFile, err := os.Open(path) |
||||
if err != nil { |
||||
return Default(), err |
||||
} |
||||
defer configFile.Close() |
||||
|
||||
confBytes, err := io.ReadAll(configFile) |
||||
if err != nil { |
||||
return Default(), err |
||||
} |
||||
|
||||
var config Conf |
||||
err = json.Unmarshal(confBytes, &config) |
||||
if err != nil { |
||||
return Default(), err |
||||
} |
||||
|
||||
return config, nil |
||||
} |
||||
|
||||
// Create empty configuration file
|
||||
func Create(path string, conf Conf) (Conf, error) { |
||||
configFile, err := os.Create(path) |
||||
if err != nil { |
||||
return Default(), err |
||||
} |
||||
defer configFile.Close() |
||||
|
||||
configJsonBytes, err := json.MarshalIndent(conf, "", " ") |
||||
if err != nil { |
||||
return conf, err |
||||
} |
||||
|
||||
_, err = configFile.Write(configJsonBytes) |
||||
if err != nil { |
||||
return conf, nil |
||||
} |
||||
|
||||
return conf, nil |
||||
} |
@ -0,0 +1,91 @@
|
||||
package db |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"os" |
||||
|
||||
_ "modernc.org/sqlite" |
||||
) |
||||
|
||||
// Database wrapper
|
||||
type DB struct { |
||||
*sql.DB |
||||
} |
||||
|
||||
func setUpTables(db *DB) error { |
||||
// Users
|
||||
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS users( |
||||
username TEXT PRIMARY KEY UNIQUE, |
||||
password TEXT NOT NULL, |
||||
time_created_unix INTEGER)`, |
||||
) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Todo groups
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS todo_groups( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, |
||||
name TEXT, |
||||
time_created_unix INTEGER, |
||||
owner_username TEXT NOT NULL, |
||||
FOREIGN KEY(owner_username) REFERENCES users(username))`, |
||||
) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Todos
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS todos( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, |
||||
group_id INTEGER NOT NULL, |
||||
text TEXT NOT NULL, |
||||
due_unix INTEGER, |
||||
owner_username TEXT NOT NULL, |
||||
FOREIGN KEY(group_id) REFERENCES todo_groups(id), |
||||
FOREIGN KEY(owner_username) REFERENCES users(username))`, |
||||
) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Open database
|
||||
func FromFile(path string) (*DB, error) { |
||||
driver, err := sql.Open("sqlite", path) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
dbase := &DB{driver} |
||||
|
||||
err = setUpTables(dbase) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return dbase, nil |
||||
} |
||||
|
||||
// Create database file
|
||||
func Create(path string) (*DB, error) { |
||||
dbFile, err := os.Create(path) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
dbFile.Close() |
||||
|
||||
driver, err := sql.Open("sqlite", path) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
dbase := &DB{driver} |
||||
|
||||
err = setUpTables(dbase) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return dbase, nil |
||||
} |
@ -0,0 +1,79 @@
|
||||
package db |
||||
|
||||
import ( |
||||
"os" |
||||
"path/filepath" |
||||
"testing" |
||||
) |
||||
|
||||
func TestApi(t *testing.T) { |
||||
db, err := Create(filepath.Join(os.TempDir(), "dela_test.db")) |
||||
if err != nil { |
||||
t.Fatalf("failed to create database: %s", err) |
||||
} |
||||
|
||||
// User
|
||||
user := User{ |
||||
Username: "user1", |
||||
Password: "ruohguoeruoger", |
||||
TimeCreatedUnix: 12421467, |
||||
} |
||||
|
||||
err = db.CreateUser(user) |
||||
if err != nil { |
||||
t.Fatalf("failed to create user: %s", err) |
||||
} |
||||
|
||||
dbUser, err := db.GetUser(user.Username) |
||||
if err != nil { |
||||
t.Fatalf("failed to retrieve created user: %s", err) |
||||
} |
||||
|
||||
if dbUser.Password != user.Password { |
||||
t.Fatalf("user passwords don't match") |
||||
} |
||||
|
||||
// Todos
|
||||
group := TodoGroup{ |
||||
Name: "group1", |
||||
TimeCreatedUnix: 13524534, |
||||
OwnerUsername: user.Username, |
||||
} |
||||
|
||||
err = db.CreateTodoGroup(group) |
||||
if err != nil { |
||||
t.Fatalf("failed to create todo group: %s", err) |
||||
} |
||||
|
||||
err = db.UpdateTodoGroup(1, TodoGroup{Name: "updated_name"}) |
||||
if err != nil { |
||||
t.Fatalf("failed to update todo group: %s", err) |
||||
} |
||||
|
||||
dbGroup, err := db.GetTodoGroup(1) |
||||
if err != nil { |
||||
t.Fatalf("failed to get created TODO group: %s", err) |
||||
} |
||||
|
||||
if dbGroup.Name == group.Name { |
||||
t.Fatalf("name match changed value for a TODO group") |
||||
} |
||||
|
||||
todo := Todo{ |
||||
GroupID: dbGroup.ID, |
||||
Text: "Do the dishes", |
||||
TimeCreatedUnix: dbGroup.TimeCreatedUnix, |
||||
DueUnix: 0, |
||||
OwnerUsername: user.Username, |
||||
} |
||||
err = db.CreateTodo(todo) |
||||
if err != nil { |
||||
t.Fatalf("couldn't create a new TODO: %s", err) |
||||
} |
||||
|
||||
// Now deletion
|
||||
err = db.DeleteUserClean(user.Username) |
||||
if err != nil { |
||||
t.Fatalf("couldn't cleanly delete user with all TODOs: %s", err) |
||||
} |
||||
} |
@ -0,0 +1,230 @@
|
||||
package db |
||||
|
||||
import "database/sql" |
||||
|
||||
// Todo group structure
|
||||
type TodoGroup struct { |
||||
ID uint64 |
||||
Name string |
||||
TimeCreatedUnix uint64 |
||||
OwnerUsername string |
||||
} |
||||
|
||||
// Todo structure
|
||||
type Todo struct { |
||||
ID uint64 |
||||
GroupID uint64 |
||||
Text string |
||||
TimeCreatedUnix uint64 |
||||
DueUnix uint64 |
||||
OwnerUsername string |
||||
} |
||||
|
||||
// Creates a new TODO group in the database
|
||||
func (db *DB) CreateTodoGroup(group TodoGroup) error { |
||||
_, err := db.Exec( |
||||
"INSERT INTO todo_groups(name, time_created_unix, owner_username) VALUES(?, ?, ?)", |
||||
group.Name, |
||||
group.TimeCreatedUnix, |
||||
group.OwnerUsername, |
||||
) |
||||
|
||||
return err |
||||
} |
||||
|
||||
func scanTodoGroup(rows *sql.Rows) (*TodoGroup, error) { |
||||
var newTodoGroup TodoGroup |
||||
err := rows.Scan( |
||||
&newTodoGroup.ID, |
||||
&newTodoGroup.Name, |
||||
&newTodoGroup.TimeCreatedUnix, |
||||
&newTodoGroup.OwnerUsername, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &newTodoGroup, nil |
||||
} |
||||
|
||||
// Retrieves a TODO group with provided ID from the database
|
||||
func (db *DB) GetTodoGroup(id uint64) (*TodoGroup, error) { |
||||
rows, err := db.Query( |
||||
"SELECT * FROM todo_groups WHERE id=?", |
||||
id, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer rows.Close() |
||||
|
||||
rows.Next() |
||||
todoGroup, err := scanTodoGroup(rows) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return todoGroup, nil |
||||
} |
||||
|
||||
// Deletes information about a TODO group of given ID from the database
|
||||
func (db *DB) DeleteTodoGroup(id uint64) error { |
||||
_, err := db.Exec( |
||||
"DELETE FROM todo_groups WHERE id=?", |
||||
id, |
||||
) |
||||
|
||||
return err |
||||
} |
||||
|
||||
// Updates TODO group's name
|
||||
func (db *DB) UpdateTodoGroup(groupID uint64, updatedGroup TodoGroup) error { |
||||
_, err := db.Exec( |
||||
"UPDATE todo_groups SET name=? WHERE id=?", |
||||
updatedGroup.Name, |
||||
groupID, |
||||
) |
||||
|
||||
return err |
||||
} |
||||
|
||||
func scanTodo(rows *sql.Rows) (*Todo, error) { |
||||
var newTodo Todo |
||||
err := rows.Scan( |
||||
&newTodo.ID, |
||||
&newTodo.GroupID, |
||||
&newTodo.Text, |
||||
&newTodo.DueUnix, |
||||
&newTodo.OwnerUsername, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &newTodo, nil |
||||
} |
||||
|
||||
// Retrieves a TODO with given Id from the database
|
||||
func (db *DB) GetTodo(id uint64) (*Todo, error) { |
||||
rows, err := db.Query( |
||||
"SELECT * FROM todos WHERE id=?", |
||||
id, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer rows.Close() |
||||
|
||||
rows.Next() |
||||
todo, err := scanTodo(rows) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return todo, nil |
||||
} |
||||
|
||||
// Creates a new TODO in the database
|
||||
func (db *DB) CreateTodo(todo Todo) error { |
||||
_, err := db.Exec( |
||||
"INSERT INTO todos(group_id, text, due_unix, owner_username) VALUES(?, ?, ?, ?)", |
||||
todo.GroupID, |
||||
todo.Text, |
||||
todo.DueUnix, |
||||
todo.OwnerUsername, |
||||
) |
||||
|
||||
return err |
||||
} |
||||
|
||||
// Deletes information about a TODO of certain ID from the database
|
||||
func (db *DB) DeleteTodo(id uint64) error { |
||||
_, err := db.Exec( |
||||
"DELETE FROM todos WHERE id=?", |
||||
id, |
||||
) |
||||
|
||||
return err |
||||
} |
||||
|
||||
// Updates TODO's due date, text and group id
|
||||
func (db *DB) UpdateTodo(todoID uint64, updatedTodo Todo) error { |
||||
_, err := db.Exec( |
||||
"UPDATE todos SET group_id=?, due_unix=?, text=? WHERE id=?", |
||||
updatedTodo.GroupID, |
||||
updatedTodo.DueUnix, |
||||
updatedTodo.Text, |
||||
todoID, |
||||
) |
||||
|
||||
return err |
||||
} |
||||
|
||||
// Searches and retrieves TODO groups created by the user
|
||||
func (db *DB) GetAllUserTodoGroups(username string) ([]*TodoGroup, error) { |
||||
var todoGroups []*TodoGroup |
||||
|
||||
rows, err := db.Query( |
||||
"SELECT * FROM todo_groups WHERE owner_username=?", |
||||
username, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer rows.Close() |
||||
|
||||
for rows.Next() { |
||||
group, err := scanTodoGroup(rows) |
||||
if err != nil { |
||||
continue |
||||
} |
||||
todoGroups = append(todoGroups, group) |
||||
} |
||||
|
||||
return todoGroups, nil |
||||
} |
||||
|
||||
// Searches and retrieves TODOs created by the user
|
||||
func (db *DB) GetAllUserTodos(username string) ([]*Todo, error) { |
||||
var todos []*Todo |
||||
|
||||
rows, err := db.Query( |
||||
"SELECT * FROM todos WHERE owner_username=?", |
||||
username, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer rows.Close() |
||||
|
||||
for rows.Next() { |
||||
todo, err := scanTodo(rows) |
||||
if err != nil { |
||||
continue |
||||
} |
||||
|
||||
todos = append(todos, todo) |
||||
} |
||||
|
||||
return todos, nil |
||||
} |
||||
|
||||
// Deletes all information regarding TODOs of specified user
|
||||
func (db *DB) DeleteAllUserTodos(username string) error { |
||||
_, err := db.Exec( |
||||
"DELETE FROM todos WHERE owner_username=?", |
||||
username, |
||||
) |
||||
|
||||
return err |
||||
} |
||||
|
||||
// Deletes all information regarding TODO groups of specified user
|
||||
func (db *DB) DeleteAllUserTodoGroups(username string) error { |
||||
_, err := db.Exec( |
||||
"DELETE FROM todo_groups WHERE owner_username=?", |
||||
username, |
||||
) |
||||
|
||||
return err |
||||
} |
@ -0,0 +1,79 @@
|
||||
package db |
||||
|
||||
import "database/sql" |
||||
|
||||
// User structure
|
||||
type User struct { |
||||
Username string |
||||
Password string |
||||
TimeCreatedUnix uint64 |
||||
} |
||||
|
||||
func scanUser(rows *sql.Rows) (*User, error) { |
||||
rows.Next() |
||||
var user User |
||||
err := rows.Scan(&user.Username, &user.Password, &user.TimeCreatedUnix) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &user, nil |
||||
} |
||||
|
||||
// Searches for user with username and returns it
|
||||
func (db *DB) GetUser(username string) (*User, error) { |
||||
rows, err := db.Query("SELECT * FROM users WHERE username=?", username) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer rows.Close() |
||||
|
||||
user, err := scanUser(rows) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return user, nil |
||||
} |
||||
|
||||
// Creates a new user in the database
|
||||
func (db *DB) CreateUser(newUser User) error { |
||||
_, err := db.Exec( |
||||
"INSERT INTO users(username, password, time_created_unix) VALUES(?, ?, ?)", |
||||
newUser.Username, |
||||
newUser.Password, |
||||
newUser.TimeCreatedUnix, |
||||
) |
||||
|
||||
return err |
||||
} |
||||
|
||||
// Deletes user with given username
|
||||
func (db *DB) DeleteUser(username string) error { |
||||
_, err := db.Exec( |
||||
"DELETE FROM users WHERE username=?", |
||||
username, |
||||
) |
||||
|
||||
return err |
||||
} |
||||
|
||||
// Deletes a user and all his TODOs (with groups) as well
|
||||
func (db *DB) DeleteUserClean(username string) error { |
||||
err := db.DeleteAllUserTodoGroups(username) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = db.DeleteAllUserTodos(username) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = db.DeleteUser(username) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,25 @@
|
||||
package encryption |
||||
|
||||
import ( |
||||
"crypto/sha256" |
||||
"encoding/base64" |
||||
"fmt" |
||||
) |
||||
|
||||
// Encodes given string via Base64
|
||||
func EncodeString(str string) string { |
||||
return base64.StdEncoding.EncodeToString([]byte(str)) |
||||
} |
||||
|
||||
// Decodes given string via Base64
|
||||
func DecodeString(encodedStr string) string { |
||||
decodedBytes, _ := base64.StdEncoding.DecodeString(encodedStr) |
||||
return string(decodedBytes) |
||||
} |
||||
|
||||
// Returns HEX string of SHA256'd data
|
||||
func SHA256Hex(data []byte) string { |
||||
hash := sha256.New() |
||||
hash.Write(data) |
||||
return fmt.Sprintf("%x", hash.Sum(nil)) |
||||
} |
@ -0,0 +1,26 @@
|
||||
module Unbewohnte/dela |
||||
|
||||
go 1.20 |
||||
|
||||
require modernc.org/sqlite v1.22.0 |
||||
|
||||
require ( |
||||
github.com/dustin/go-humanize v1.0.1 // indirect |
||||
github.com/google/uuid v1.3.0 // indirect |
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect |
||||
github.com/mattn/go-isatty v0.0.16 // indirect |
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect |
||||
golang.org/x/mod v0.3.0 // indirect |
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect |
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect |
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect |
||||
lukechampine.com/uint128 v1.2.0 // indirect |
||||
modernc.org/cc/v3 v3.40.0 // indirect |
||||
modernc.org/ccgo/v3 v3.16.13 // indirect |
||||
modernc.org/libc v1.22.4 // indirect |
||||
modernc.org/mathutil v1.5.0 // indirect |
||||
modernc.org/memory v1.5.0 // indirect |
||||
modernc.org/opt v0.1.3 // indirect |
||||
modernc.org/strutil v1.1.3 // indirect |
||||
modernc.org/token v1.0.1 // indirect |
||||
) |
@ -0,0 +1,65 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= |
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= |
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= |
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= |
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= |
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= |
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= |
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= |
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= |
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= |
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= |
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= |
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= |
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= |
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= |
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= |
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= |
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= |
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= |
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= |
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs= |
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= |
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= |
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= |
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= |
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= |
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= |
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= |
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= |
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= |
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= |
||||
modernc.org/libc v1.22.4 h1:wymSbZb0AlrjdAVX3cjreCHTPCpPARbQXNz6BHPzdwQ= |
||||
modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= |
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= |
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= |
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= |
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= |
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= |
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= |
||||
modernc.org/sqlite v1.22.0 h1:Uo+wEWePCspy4SAu0w2VbzUHEftOs7yoaWX/cYjsq84= |
||||
modernc.org/sqlite v1.22.0/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0= |
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= |
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= |
||||
modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws= |
||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= |
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= |
||||
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE= |
@ -0,0 +1,42 @@
|
||||
package logger |
||||
|
||||
import ( |
||||
"io" |
||||
"log" |
||||
"os" |
||||
) |
||||
|
||||
// 3 basic loggers in global space
|
||||
var ( |
||||
// neutral information logger
|
||||
infoLog *log.Logger |
||||
// warning-level information logger
|
||||
warningLog *log.Logger |
||||
// errors information logger
|
||||
errorLog *log.Logger |
||||
) |
||||
|
||||
func init() { |
||||
infoLog = log.New(os.Stdout, "[INFO] ", log.Ldate|log.Ltime) |
||||
warningLog = log.New(os.Stdout, "[WARNING] ", log.Ldate|log.Ltime) |
||||
errorLog = log.New(os.Stdout, "[ERROR] ", log.Ldate|log.Ltime) |
||||
} |
||||
|
||||
// Set up loggers to write to the given writer
|
||||
func SetOutput(writer io.Writer) { |
||||
if writer == nil { |
||||
writer = io.Discard |
||||
} |
||||
infoLog.SetOutput(writer) |
||||
warningLog.SetOutput(writer) |
||||
errorLog.SetOutput(writer) |
||||
} |
||||
func Info(format string, a ...interface{}) { |
||||
infoLog.Printf(format, a...) |
||||
} |
||||
func Warning(format string, a ...interface{}) { |
||||
warningLog.Printf(format, a...) |
||||
} |
||||
func Error(format string, a ...interface{}) { |
||||
errorLog.Printf(format, a...) |
||||
} |
@ -0,0 +1,65 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"Unbewohnte/dela/conf" |
||||
"Unbewohnte/dela/logger" |
||||
"Unbewohnte/dela/server" |
||||
"os" |
||||
"path/filepath" |
||||
) |
||||
|
||||
const ConfName string = "conf.json" |
||||
|
||||
var ( |
||||
WDir string |
||||
Conf conf.Conf |
||||
) |
||||
|
||||
func init() { |
||||
// Initialize logging
|
||||
logger.SetOutput(os.Stdout) |
||||
|
||||
// Work out the working directory
|
||||
exePath, err := os.Executable() |
||||
if err != nil { |
||||
logger.Error("[Init] Failed to retrieve executable's path: %s", err) |
||||
os.Exit(1) |
||||
} |
||||
WDir = filepath.Dir(exePath) |
||||
logger.Info("[Init] Working in \"%s\"", WDir) |
||||
|
||||
// Open configuration, create if does not exist
|
||||
Conf, err = conf.FromFile(filepath.Join(WDir, ConfName)) |
||||
if err != nil { |
||||
_, err = conf.Create(filepath.Join(WDir, ConfName), conf.Default()) |
||||
if err != nil { |
||||
logger.Error("[Init] Failed to create a new configuration file: %s", err) |
||||
os.Exit(1) |
||||
} |
||||
logger.Info("[Init] Created a new configuration file") |
||||
os.Exit(0) |
||||
} |
||||
logger.Info("[Init] Opened existing configuration file") |
||||
if Conf.BaseContentDir == "." { |
||||
Conf.BaseContentDir = WDir |
||||
} |
||||
|
||||
// Check if database exists and create it otherwise
|
||||
|
||||
logger.Info("[Init] Successful initializaion!") |
||||
} |
||||
|
||||
func main() { |
||||
server, err := server.New(Conf) |
||||
if err != nil { |
||||
logger.Error("[Main] Failed to initialize a new server with conf (%+v): %s", Conf, err) |
||||
return |
||||
} |
||||
logger.Info("[Main] Successfully initialized a new server instance with conf (%+v)", Conf) |
||||
|
||||
err = server.Start() |
||||
if err != nil { |
||||
logger.Error("[Main] Fatal server failure: %s. Exiting...", err) |
||||
return |
||||
} |
||||
} |
@ -0,0 +1,426 @@
|
||||
package server |
||||
|
||||
import ( |
||||
"Unbewohnte/dela/db" |
||||
"Unbewohnte/dela/logger" |
||||
"encoding/json" |
||||
"io" |
||||
"net/http" |
||||
"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() |
||||
|
||||
// Read body
|
||||
body, err := io.ReadAll(req.Body) |
||||
if err != nil { |
||||
logger.Warning("[Server] Failed to read request body to delete a 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 deletion: %s", err) |
||||
http.Error(w, "Invalid user JSON", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
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 |
||||
} |
||||
|
||||
// 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) TodoEndpoint(w http.ResponseWriter, req *http.Request) { |
||||
switch req.Method { |
||||
case http.MethodDelete: |
||||
// Delete an existing 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 possibly delete a TODO: %s", err) |
||||
http.Error(w, "Failed to read body", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
// Unmarshal JSON
|
||||
var todo db.Todo |
||||
err = json.Unmarshal(body, &todo) |
||||
if err != nil { |
||||
logger.Warning("[Server] Received invalid TODO JSON for deletion: %s", err) |
||||
http.Error(w, "Invalid TODO JSON", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
// Check if given user actually owns this TODO
|
||||
if !IsRequestAuthValid(req, s.db) { |
||||
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
||||
return |
||||
} |
||||
|
||||
if !DoesUserOwnTodo(GetUsernameFromAuth(req), todo.ID, s.db) { |
||||
http.Error(w, "You don't own this TODO", http.StatusForbidden) |
||||
return |
||||
} |
||||
|
||||
// Now delete
|
||||
err = s.db.DeleteTodo(todo.ID) |
||||
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!
|
||||
w.WriteHeader(http.StatusOK) |
||||
|
||||
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) |
||||
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 |
||||
} |
||||
|
||||
todoID, err := GetTodoIDFromReq(req) |
||||
if err != nil { |
||||
http.Error(w, "Invalid TODO ID", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
if !DoesUserOwnTodo(GetUsernameFromAuth(req), todoID, s.db) { |
||||
http.Error(w, "You don't own this TODO", http.StatusForbidden) |
||||
return |
||||
} |
||||
|
||||
// Get TODO
|
||||
todo, err := s.db.GetTodo(todoID) |
||||
if err != nil { |
||||
http.Error(w, "Failed to get TODO", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
// Marshal to JSON
|
||||
todoBytes, err := json.Marshal(&todo) |
||||
if err != nil { |
||||
http.Error(w, "Failed to marhsal TODO JSON", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
// Send out
|
||||
w.Header().Add("Content-Type", "application/json") |
||||
w.Write(todoBytes) |
||||
|
||||
case http.MethodPatch: |
||||
// Change TODO due date and text
|
||||
|
||||
// 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: %s", err) |
||||
http.Error(w, "Failed to read body", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
// Unmarshal JSON
|
||||
var todo db.Todo |
||||
err = json.Unmarshal(body, &todo) |
||||
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 |
||||
} |
||||
|
||||
// TODO
|
||||
err = s.db.UpdateTodo(todo.ID, 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) |
||||
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 todo group
|
||||
|
||||
// Check authentication information
|
||||
if !IsRequestAuthValid(req, s.db) { |
||||
http.Error(w, "Invalid user auth data", http.StatusForbidden) |
||||
return |
||||
} |
||||
|
||||
groupID, err := GetTodoIDFromReq(req) |
||||
if err != nil { |
||||
http.Error(w, "Invalid group ID", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
if !DoesUserOwnTodoGroup(GetUsernameFromAuth(req), groupID, s.db) { |
||||
http.Error(w, "You don't own this group", http.StatusForbidden) |
||||
return |
||||
} |
||||
|
||||
// Get group
|
||||
group, err := s.db.GetTodoGroup(groupID) |
||||
if err != nil { |
||||
http.Error(w, "Failed to get TODO group", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
// Marshal to JSON
|
||||
groupBytes, err := json.Marshal(&group) |
||||
if err != nil { |
||||
http.Error(w, "Failed to marhsal TODO group 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,130 @@
|
||||
package server |
||||
|
||||
import ( |
||||
"Unbewohnte/dela/conf" |
||||
"Unbewohnte/dela/db" |
||||
"bytes" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"os" |
||||
"path/filepath" |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
func TestApi(t *testing.T) { |
||||
// Create a new server
|
||||
config := conf.Default() |
||||
config.BaseContentDir = "../../" |
||||
config.ProdDBName = filepath.Join(os.TempDir(), "dela_test_db.db") |
||||
server, err := New(config) |
||||
if err != nil { |
||||
t.Fatalf("failed to create a new server: %s", err) |
||||
} |
||||
defer os.Remove(config.ProdDBName) |
||||
|
||||
go func() { |
||||
time.Sleep(time.Second * 5) |
||||
server.Stop() |
||||
}() |
||||
|
||||
go func() { |
||||
server.Start() |
||||
}() |
||||
|
||||
// Create a new user
|
||||
newUser := db.User{ |
||||
Username: "user1", |
||||
Password: "ruohguoeruoger", |
||||
TimeCreatedUnix: 12421467, |
||||
} |
||||
newUserJsonBytes, err := json.Marshal(&newUser) |
||||
if err != nil { |
||||
t.Fatalf("could not marshal new user JSON: %s", err) |
||||
} |
||||
|
||||
resp, err := http.Post(fmt.Sprintf("http://localhost:%d/api/user", config.Port), "application/json", bytes.NewBuffer(newUserJsonBytes)) |
||||
if err != nil { |
||||
t.Fatalf("failed to post a new user data: %s", err) |
||||
} |
||||
body, err := io.ReadAll(resp.Body) |
||||
if err != nil { |
||||
t.Fatalf("failed to read response body for user creation: %s", err) |
||||
} |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
t.Fatalf("got non-OK status code for user creation: %s", string(body)) |
||||
} |
||||
resp.Body.Close() |
||||
|
||||
// Create a new TODO group and a TODO
|
||||
newGroup := db.TodoGroup{ |
||||
Name: "group1", |
||||
TimeCreatedUnix: 13524534, |
||||
OwnerUsername: newUser.Username, |
||||
} |
||||
newGroupBytes, err := json.Marshal(&newGroup) |
||||
if err != nil { |
||||
t.Fatalf("could not marshal new user JSON: %s", err) |
||||
} |
||||
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/api/groups", config.Port), bytes.NewBuffer(newGroupBytes)) |
||||
if err != nil { |
||||
t.Fatalf("failed to create a new POST request to create a new TODO group: %s", err) |
||||
} |
||||
req.Header.Add(RequestHeaderAuthKey, fmt.Sprintf("%s%s%s", newUser.Username, RequestHeaderAuthSeparator, newUser.Password)) |
||||
req.Header.Add(RequestHeaderEncodedB64, "false") |
||||
|
||||
resp, err = http.DefaultClient.Do(req) |
||||
if err != nil { |
||||
t.Fatalf("failed to post a new TODO group: %s", err) |
||||
} |
||||
|
||||
body, err = io.ReadAll(resp.Body) |
||||
if err != nil { |
||||
t.Fatalf("failed to read response body for TODO group creation: %s", err) |
||||
} |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
t.Fatalf("got non-OK status code for TODO group creation: %s", string(body)) |
||||
} |
||||
resp.Body.Close() |
||||
|
||||
// TODO creation
|
||||
var newTodo db.Todo = db.Todo{ |
||||
GroupID: newGroup.ID, |
||||
Text: "Do the dishes", |
||||
TimeCreatedUnix: uint64(time.Now().UnixMicro()), |
||||
DueUnix: uint64(time.Now().Add(time.Hour * 5).UnixMicro()), |
||||
OwnerUsername: newUser.Username, |
||||
} |
||||
|
||||
newTodoBytes, err := json.Marshal(&newTodo) |
||||
if err != nil { |
||||
t.Fatalf("could not marshal new Todo: %s", err) |
||||
} |
||||
|
||||
req, err = http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/api/todo", config.Port), bytes.NewBuffer(newTodoBytes)) |
||||
if err != nil { |
||||
t.Fatalf("failed to create a new POST request to create a new TODO: %s", err) |
||||
} |
||||
req.Header.Add(RequestHeaderAuthKey, fmt.Sprintf("%s%s%s", newUser.Username, RequestHeaderAuthSeparator, newUser.Password)) |
||||
req.Header.Add(RequestHeaderEncodedB64, "false") |
||||
|
||||
resp, err = http.DefaultClient.Do(req) |
||||
if err != nil { |
||||
t.Fatalf("failed to post a new Todo: %s", err) |
||||
} |
||||
|
||||
body, err = io.ReadAll(resp.Body) |
||||
if err != nil { |
||||
t.Fatalf("failed to read response body for Todo creation: %s", err) |
||||
} |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
t.Fatalf("got non-OK status code for Todo creation: %s", string(body)) |
||||
} |
||||
resp.Body.Close() |
||||
} |
@ -0,0 +1,122 @@
|
||||
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 |
||||
} |
@ -0,0 +1,21 @@
|
||||
package server |
||||
|
||||
import ( |
||||
"Unbewohnte/dela/logger" |
||||
"html/template" |
||||
"path/filepath" |
||||
) |
||||
|
||||
// Constructs a pageName template via inserting basePageName in pagesDir
|
||||
func getPage(pagesDir string, basePageName string, pageName string) (*template.Template, error) { |
||||
page, err := template.ParseFiles( |
||||
filepath.Join(pagesDir, basePageName), |
||||
filepath.Join(pagesDir, pageName), |
||||
) |
||||
if err != nil { |
||||
logger.Error("Failed to parse page files (pagename is \"%s\"): %s", pageName, err) |
||||
return nil, err |
||||
} |
||||
|
||||
return page, nil |
||||
} |
@ -0,0 +1,166 @@
|
||||
package server |
||||
|
||||
import ( |
||||
"Unbewohnte/dela/conf" |
||||
"Unbewohnte/dela/db" |
||||
"Unbewohnte/dela/logger" |
||||
"context" |
||||
"fmt" |
||||
"net/http" |
||||
"os" |
||||
"path/filepath" |
||||
"time" |
||||
) |
||||
|
||||
const ( |
||||
PagesDirName string = "pages" |
||||
StaticDirName string = "static" |
||||
ScriptsDirName string = "scripts" |
||||
) |
||||
|
||||
type Server struct { |
||||
config conf.Conf |
||||
db *db.DB |
||||
http http.Server |
||||
} |
||||
|
||||
// Creates a new server instance with provided config
|
||||
func New(config conf.Conf) (*Server, error) { |
||||
var server Server = Server{} |
||||
server.config = config |
||||
|
||||
// check if required directories are present
|
||||
_, err := os.Stat(filepath.Join(config.BaseContentDir, PagesDirName)) |
||||
if err != nil { |
||||
logger.Error("[Server] A directory with HTML pages is not available: %s", err) |
||||
return nil, err |
||||
} |
||||
|
||||
_, err = os.Stat(filepath.Join(config.BaseContentDir, ScriptsDirName)) |
||||
if err != nil { |
||||
logger.Error("[Server] A directory with scripts is not available: %s", err) |
||||
return nil, err |
||||
} |
||||
|
||||
_, err = os.Stat(filepath.Join(config.BaseContentDir, StaticDirName)) |
||||
if err != nil { |
||||
logger.Error("[Server] A directory with static content is not available: %s", err) |
||||
return nil, err |
||||
} |
||||
|
||||
// get database working
|
||||
serverDB, err := db.FromFile(filepath.Join(config.BaseContentDir, config.ProdDBName)) |
||||
if err != nil { |
||||
// Create one then
|
||||
serverDB, err = db.Create(filepath.Join(config.BaseContentDir, config.ProdDBName)) |
||||
if err != nil { |
||||
logger.Error("Failed to create a new database: %s", err) |
||||
return nil, err |
||||
} |
||||
} |
||||
server.db = serverDB |
||||
logger.Info("Opened a database successfully") |
||||
|
||||
// start constructing an http server configuration
|
||||
server.http = http.Server{ |
||||
Addr: fmt.Sprintf(":%d", server.config.Port), |
||||
} |
||||
|
||||
// configure paths' callbacks
|
||||
mux := http.NewServeMux() |
||||
mux.Handle( |
||||
"/static/", |
||||
http.StripPrefix("/static/", http.FileServer( |
||||
http.Dir(filepath.Join(server.config.BaseContentDir, StaticDirName))), |
||||
), |
||||
) |
||||
|
||||
mux.Handle( |
||||
"/scripts/", |
||||
http.StripPrefix("/scripts/", http.FileServer( |
||||
http.Dir(filepath.Join(server.config.BaseContentDir, ScriptsDirName))), |
||||
), |
||||
) |
||||
|
||||
// handle page requests
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { |
||||
// Ignore favicon request. It's not an .ico
|
||||
if req.URL.Path == "favicon.ico" { |
||||
return |
||||
} |
||||
|
||||
switch req.URL.Path { |
||||
case "/": |
||||
requestedPage, err := getPage( |
||||
filepath.Join(server.config.BaseContentDir, PagesDirName), "base.html", "index.html", |
||||
) |
||||
if err != nil { |
||||
http.Error(w, "Internal server error", http.StatusInternalServerError) |
||||
} |
||||
|
||||
requestedPage.ExecuteTemplate(w, "index.html", nil) |
||||
|
||||
default: |
||||
requestedPage, err := getPage( |
||||
filepath.Join(server.config.BaseContentDir, PagesDirName), |
||||
"base.html", |
||||
req.URL.Path[1:]+".html", |
||||
) |
||||
if err == nil { |
||||
requestedPage.ExecuteTemplate(w, req.URL.Path[1:]+".html", nil) |
||||
} else { |
||||
// Redirect to the index
|
||||
index, err := getPage( |
||||
filepath.Join(server.config.BaseContentDir, PagesDirName), |
||||
"base.html", |
||||
req.URL.Path[1:]+".html", |
||||
) |
||||
if err != nil { |
||||
http.Error(w, "Internal server error", http.StatusInternalServerError) |
||||
} |
||||
|
||||
index.ExecuteTemplate(w, "index.html", nil) |
||||
} |
||||
} |
||||
}) |
||||
mux.HandleFunc("/api/user", server.UserEndpoint) |
||||
mux.HandleFunc("/api/todo", server.TodoEndpoint) |
||||
mux.HandleFunc("/api/groups", server.TodoGroupEndpoint) |
||||
|
||||
server.http.Handler = mux |
||||
logger.Info("[Server] Created an HTTP server instance") |
||||
|
||||
return &server, nil |
||||
} |
||||
|
||||
// Launches server instance
|
||||
func (s *Server) Start() error { |
||||
if s.config.CertFilePath != "" && s.config.KeyFilePath != "" { |
||||
logger.Info("[Server] Using TLS") |
||||
logger.Info("[Server] HTTP server is going live on port %d!", s.config.Port) |
||||
|
||||
err := s.http.ListenAndServeTLS(s.config.CertFilePath, s.config.KeyFilePath) |
||||
if err != nil && err != http.ErrServerClosed { |
||||
logger.Error("[Server] Fatal server error: %s", err) |
||||
return err |
||||
} |
||||
} else { |
||||
logger.Info("[Server] Not using TLS") |
||||
logger.Info("[Server] HTTP server is going live on port %d!", s.config.Port) |
||||
|
||||
err := s.http.ListenAndServe() |
||||
if err != nil && err != http.ErrServerClosed { |
||||
logger.Error("[Server] Fatal server error: %s", err) |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Stops the server immediately
|
||||
func (s *Server) Stop() { |
||||
ctx, cfunc := context.WithDeadline(context.Background(), time.Now().Add(time.Second*10)) |
||||
s.http.Shutdown(ctx) |
||||
cfunc() |
||||
} |
@ -0,0 +1,33 @@
|
||||
package server |
||||
|
||||
import ( |
||||
"Unbewohnte/dela/db" |
||||
"fmt" |
||||
) |
||||
|
||||
const ( |
||||
MinimalUsernameLength uint = 3 |
||||
ForbiddenUsernameCharacters string = "|<>\"'`\\/\u200b" |
||||
MinimalPasswordLength uint = 5 |
||||
) |
||||
|
||||
// Check if user is valid. Returns false and a reason-string if not
|
||||
func IsUserValid(user db.User) (bool, string) { |
||||
if uint(len(user.Username)) < MinimalUsernameLength { |
||||
return false, "Username is too small" |
||||
} |
||||
|
||||
for _, char := range user.Username { |
||||
for _, forbiddenChar := range ForbiddenUsernameCharacters { |
||||
if char == forbiddenChar { |
||||
return false, fmt.Sprintf("Username contains a forbidden character \"%c\"", char) |
||||
} |
||||
} |
||||
} |
||||
|
||||
if uint(len(user.Password)) < MinimalPasswordLength { |
||||
return false, "Password is too small" |
||||
} |
||||
|
||||
return true, "" |
||||
} |
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,593 @@
|
||||
/*! |
||||
* Bootstrap Reboot v5.3.0 (https://getbootstrap.com/) |
||||
* Copyright 2011-2023 The Bootstrap Authors |
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
||||
*/ |
||||
:root, |
||||
[data-bs-theme=light] { |
||||
--bs-blue: #0d6efd; |
||||
--bs-indigo: #6610f2; |
||||
--bs-purple: #6f42c1; |
||||
--bs-pink: #d63384; |
||||
--bs-red: #dc3545; |
||||
--bs-orange: #fd7e14; |
||||
--bs-yellow: #ffc107; |
||||
--bs-green: #198754; |
||||
--bs-teal: #20c997; |
||||
--bs-cyan: #0dcaf0; |
||||
--bs-black: #000; |
||||
--bs-white: #fff; |
||||
--bs-gray: #6c757d; |
||||
--bs-gray-dark: #343a40; |
||||
--bs-gray-100: #f8f9fa; |
||||
--bs-gray-200: #e9ecef; |
||||
--bs-gray-300: #dee2e6; |
||||
--bs-gray-400: #ced4da; |
||||
--bs-gray-500: #adb5bd; |
||||
--bs-gray-600: #6c757d; |
||||
--bs-gray-700: #495057; |
||||
--bs-gray-800: #343a40; |
||||
--bs-gray-900: #212529; |
||||
--bs-primary: #0d6efd; |
||||
--bs-secondary: #6c757d; |
||||
--bs-success: #198754; |
||||
--bs-info: #0dcaf0; |
||||
--bs-warning: #ffc107; |
||||
--bs-danger: #dc3545; |
||||
--bs-light: #f8f9fa; |
||||
--bs-dark: #212529; |
||||
--bs-primary-rgb: 13, 110, 253; |
||||
--bs-secondary-rgb: 108, 117, 125; |
||||
--bs-success-rgb: 25, 135, 84; |
||||
--bs-info-rgb: 13, 202, 240; |
||||
--bs-warning-rgb: 255, 193, 7; |
||||
--bs-danger-rgb: 220, 53, 69; |
||||
--bs-light-rgb: 248, 249, 250; |
||||
--bs-dark-rgb: 33, 37, 41; |
||||
--bs-primary-text-emphasis: #052c65; |
||||
--bs-secondary-text-emphasis: #2b2f32; |
||||
--bs-success-text-emphasis: #0a3622; |
||||
--bs-info-text-emphasis: #055160; |
||||
--bs-warning-text-emphasis: #664d03; |
||||
--bs-danger-text-emphasis: #58151c; |
||||
--bs-light-text-emphasis: #495057; |
||||
--bs-dark-text-emphasis: #495057; |
||||
--bs-primary-bg-subtle: #cfe2ff; |
||||
--bs-secondary-bg-subtle: #e2e3e5; |
||||
--bs-success-bg-subtle: #d1e7dd; |
||||
--bs-info-bg-subtle: #cff4fc; |
||||
--bs-warning-bg-subtle: #fff3cd; |
||||
--bs-danger-bg-subtle: #f8d7da; |
||||
--bs-light-bg-subtle: #fcfcfd; |
||||
--bs-dark-bg-subtle: #ced4da; |
||||
--bs-primary-border-subtle: #9ec5fe; |
||||
--bs-secondary-border-subtle: #c4c8cb; |
||||
--bs-success-border-subtle: #a3cfbb; |
||||
--bs-info-border-subtle: #9eeaf9; |
||||
--bs-warning-border-subtle: #ffe69c; |
||||
--bs-danger-border-subtle: #f1aeb5; |
||||
--bs-light-border-subtle: #e9ecef; |
||||
--bs-dark-border-subtle: #adb5bd; |
||||
--bs-white-rgb: 255, 255, 255; |
||||
--bs-black-rgb: 0, 0, 0; |
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; |
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; |
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); |
||||
--bs-body-font-family: var(--bs-font-sans-serif); |
||||
--bs-body-font-size: 1rem; |
||||
--bs-body-font-weight: 400; |
||||
--bs-body-line-height: 1.5; |
||||
--bs-body-color: #212529; |
||||
--bs-body-color-rgb: 33, 37, 41; |
||||
--bs-body-bg: #fff; |
||||
--bs-body-bg-rgb: 255, 255, 255; |
||||
--bs-emphasis-color: #000; |
||||
--bs-emphasis-color-rgb: 0, 0, 0; |
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75); |
||||
--bs-secondary-color-rgb: 33, 37, 41; |
||||
--bs-secondary-bg: #e9ecef; |
||||
--bs-secondary-bg-rgb: 233, 236, 239; |
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5); |
||||
--bs-tertiary-color-rgb: 33, 37, 41; |
||||
--bs-tertiary-bg: #f8f9fa; |
||||
--bs-tertiary-bg-rgb: 248, 249, 250; |
||||
--bs-heading-color: inherit; |
||||
--bs-link-color: #0d6efd; |
||||
--bs-link-color-rgb: 13, 110, 253; |
||||
--bs-link-decoration: underline; |
||||
--bs-link-hover-color: #0a58ca; |
||||
--bs-link-hover-color-rgb: 10, 88, 202; |
||||
--bs-code-color: #d63384; |
||||
--bs-highlight-bg: #fff3cd; |
||||
--bs-border-width: 1px; |
||||
--bs-border-style: solid; |
||||
--bs-border-color: #dee2e6; |
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175); |
||||
--bs-border-radius: 0.375rem; |
||||
--bs-border-radius-sm: 0.25rem; |
||||
--bs-border-radius-lg: 0.5rem; |
||||
--bs-border-radius-xl: 1rem; |
||||
--bs-border-radius-xxl: 2rem; |
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl); |
||||
--bs-border-radius-pill: 50rem; |
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); |
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); |
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); |
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); |
||||
--bs-focus-ring-width: 0.25rem; |
||||
--bs-focus-ring-opacity: 0.25; |
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25); |
||||
--bs-form-valid-color: #198754; |
||||
--bs-form-valid-border-color: #198754; |
||||
--bs-form-invalid-color: #dc3545; |
||||
--bs-form-invalid-border-color: #dc3545; |
||||
} |
||||
|
||||
[data-bs-theme=dark] { |
||||
color-scheme: dark; |
||||
--bs-body-color: #adb5bd; |
||||
--bs-body-color-rgb: 173, 181, 189; |
||||
--bs-body-bg: #212529; |
||||
--bs-body-bg-rgb: 33, 37, 41; |
||||
--bs-emphasis-color: #fff; |
||||
--bs-emphasis-color-rgb: 255, 255, 255; |
||||
--bs-secondary-color: rgba(173, 181, 189, 0.75); |
||||
--bs-secondary-color-rgb: 173, 181, 189; |
||||
--bs-secondary-bg: #343a40; |
||||
--bs-secondary-bg-rgb: 52, 58, 64; |
||||
--bs-tertiary-color: rgba(173, 181, 189, 0.5); |
||||
--bs-tertiary-color-rgb: 173, 181, 189; |
||||
--bs-tertiary-bg: #2b3035; |
||||
--bs-tertiary-bg-rgb: 43, 48, 53; |
||||
--bs-primary-text-emphasis: #6ea8fe; |
||||
--bs-secondary-text-emphasis: #a7acb1; |
||||
--bs-success-text-emphasis: #75b798; |
||||
--bs-info-text-emphasis: #6edff6; |
||||
--bs-warning-text-emphasis: #ffda6a; |
||||
--bs-danger-text-emphasis: #ea868f; |
||||
--bs-light-text-emphasis: #f8f9fa; |
||||
--bs-dark-text-emphasis: #dee2e6; |
||||
--bs-primary-bg-subtle: #031633; |
||||
--bs-secondary-bg-subtle: #161719; |
||||
--bs-success-bg-subtle: #051b11; |
||||
--bs-info-bg-subtle: #032830; |
||||
--bs-warning-bg-subtle: #332701; |
||||
--bs-danger-bg-subtle: #2c0b0e; |
||||
--bs-light-bg-subtle: #343a40; |
||||
--bs-dark-bg-subtle: #1a1d20; |
||||
--bs-primary-border-subtle: #084298; |
||||
--bs-secondary-border-subtle: #41464b; |
||||
--bs-success-border-subtle: #0f5132; |
||||
--bs-info-border-subtle: #087990; |
||||
--bs-warning-border-subtle: #997404; |
||||
--bs-danger-border-subtle: #842029; |
||||
--bs-light-border-subtle: #495057; |
||||
--bs-dark-border-subtle: #343a40; |
||||
--bs-heading-color: inherit; |
||||
--bs-link-color: #6ea8fe; |
||||
--bs-link-hover-color: #8bb9fe; |
||||
--bs-link-color-rgb: 110, 168, 254; |
||||
--bs-link-hover-color-rgb: 139, 185, 254; |
||||
--bs-code-color: #e685b5; |
||||
--bs-border-color: #495057; |
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15); |
||||
--bs-form-valid-color: #75b798; |
||||
--bs-form-valid-border-color: #75b798; |
||||
--bs-form-invalid-color: #ea868f; |
||||
--bs-form-invalid-border-color: #ea868f; |
||||
} |
||||
|
||||
*, |
||||
*::before, |
||||
*::after { |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
@media (prefers-reduced-motion: no-preference) { |
||||
:root { |
||||
scroll-behavior: smooth; |
||||
} |
||||
} |
||||
|
||||
body { |
||||
margin: 0; |
||||
font-family: var(--bs-body-font-family); |
||||
font-size: var(--bs-body-font-size); |
||||
font-weight: var(--bs-body-font-weight); |
||||
line-height: var(--bs-body-line-height); |
||||
color: var(--bs-body-color); |
||||
text-align: var(--bs-body-text-align); |
||||
background-color: var(--bs-body-bg); |
||||
-webkit-text-size-adjust: 100%; |
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); |
||||
} |
||||
|
||||
hr { |
||||
margin: 1rem 0; |
||||
color: inherit; |
||||
border: 0; |
||||
border-top: var(--bs-border-width) solid; |
||||
opacity: 0.25; |
||||
} |
||||
|
||||
h6, h5, h4, h3, h2, h1 { |
||||
margin-top: 0; |
||||
margin-bottom: 0.5rem; |
||||
font-weight: 500; |
||||
line-height: 1.2; |
||||
color: var(--bs-heading-color); |
||||
} |
||||
|
||||
h1 { |
||||
font-size: calc(1.375rem + 1.5vw); |
||||
} |
||||
@media (min-width: 1200px) { |
||||
h1 { |
||||
font-size: 2.5rem; |
||||
} |
||||
} |
||||
|
||||
h2 { |
||||
font-size: calc(1.325rem + 0.9vw); |
||||
} |
||||
@media (min-width: 1200px) { |
||||
h2 { |
||||
font-size: 2rem; |
||||
} |
||||
} |
||||
|
||||
h3 { |
||||
font-size: calc(1.3rem + 0.6vw); |
||||
} |
||||
@media (min-width: 1200px) { |
||||
h3 { |
||||
font-size: 1.75rem; |
||||
} |
||||
} |
||||
|
||||
h4 { |
||||
font-size: calc(1.275rem + 0.3vw); |
||||
} |
||||
@media (min-width: 1200px) { |
||||
h4 { |
||||
font-size: 1.5rem; |
||||
} |
||||
} |
||||
|
||||
h5 { |
||||
font-size: 1.25rem; |
||||
} |
||||
|
||||
h6 { |
||||
font-size: 1rem; |
||||
} |
||||
|
||||
p { |
||||
margin-top: 0; |
||||
margin-bottom: 1rem; |
||||
} |
||||
|
||||
abbr[title] { |
||||
-webkit-text-decoration: underline dotted; |
||||
text-decoration: underline dotted; |
||||
cursor: help; |
||||
-webkit-text-decoration-skip-ink: none; |
||||
text-decoration-skip-ink: none; |
||||
} |
||||
|
||||
address { |
||||
margin-bottom: 1rem; |
||||
font-style: normal; |
||||
line-height: inherit; |
||||
} |
||||
|
||||
ol, |
||||
ul { |
||||
padding-left: 2rem; |
||||
} |
||||
|
||||
ol, |
||||
ul, |
||||
dl { |
||||
margin-top: 0; |
||||
margin-bottom: 1rem; |
||||
} |
||||
|
||||
ol ol, |
||||
ul ul, |
||||
ol ul, |
||||
ul ol { |
||||
margin-bottom: 0; |
||||
} |
||||
|
||||
dt { |
||||
font-weight: 700; |
||||
} |
||||
|
||||
dd { |
||||
margin-bottom: 0.5rem; |
||||
margin-left: 0; |
||||
} |
||||
|
||||
blockquote { |
||||
margin: 0 0 1rem; |
||||
} |
||||
|
||||
b, |
||||
strong { |
||||
font-weight: bolder; |
||||
} |
||||
|
||||
small { |
||||
font-size: 0.875em; |
||||
} |
||||
|
||||
mark { |
||||
padding: 0.1875em; |
||||
background-color: var(--bs-highlight-bg); |
||||
} |
||||
|
||||
sub, |
||||
sup { |
||||
position: relative; |
||||
font-size: 0.75em; |
||||
line-height: 0; |
||||
vertical-align: baseline; |
||||
} |
||||
|
||||
sub { |
||||
bottom: -0.25em; |
||||
} |
||||
|
||||
sup { |
||||
top: -0.5em; |
||||
} |
||||
|
||||
a { |
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); |
||||
text-decoration: underline; |
||||
} |
||||
a:hover { |
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb); |
||||
} |
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover { |
||||
color: inherit; |
||||
text-decoration: none; |
||||
} |
||||
|
||||
pre, |
||||
code, |
||||
kbd, |
||||
samp { |
||||
font-family: var(--bs-font-monospace); |
||||
font-size: 1em; |
||||
} |
||||
|
||||
pre { |
||||
display: block; |
||||
margin-top: 0; |
||||
margin-bottom: 1rem; |
||||
overflow: auto; |
||||
font-size: 0.875em; |
||||
} |
||||
pre code { |
||||
font-size: inherit; |
||||
color: inherit; |
||||
word-break: normal; |
||||
} |
||||
|
||||
code { |
||||
font-size: 0.875em; |
||||
color: var(--bs-code-color); |
||||
word-wrap: break-word; |
||||
} |
||||
a > code { |
||||
color: inherit; |
||||
} |
||||
|
||||
kbd { |
||||
padding: 0.1875rem 0.375rem; |
||||
font-size: 0.875em; |
||||
color: var(--bs-body-bg); |
||||
background-color: var(--bs-body-color); |
||||
border-radius: 0.25rem; |
||||
} |
||||
kbd kbd { |
||||
padding: 0; |
||||
font-size: 1em; |
||||
} |
||||
|
||||
figure { |
||||
margin: 0 0 1rem; |
||||
} |
||||
|
||||
img, |
||||
svg { |
||||
vertical-align: middle; |
||||
} |
||||
|
||||
table { |
||||
caption-side: bottom; |
||||
border-collapse: collapse; |
||||
} |
||||
|
||||
caption { |
||||
padding-top: 0.5rem; |
||||
padding-bottom: 0.5rem; |
||||
color: var(--bs-secondary-color); |
||||
text-align: left; |
||||
} |
||||
|
||||
th { |
||||
text-align: inherit; |
||||
text-align: -webkit-match-parent; |
||||
} |
||||
|
||||
thead, |
||||
tbody, |
||||
tfoot, |
||||
tr, |
||||
td, |
||||
th { |
||||
border-color: inherit; |
||||
border-style: solid; |
||||
border-width: 0; |
||||
} |
||||
|
||||
label { |
||||
display: inline-block; |
||||
} |
||||
|
||||
button { |
||||
border-radius: 0; |
||||
} |
||||
|
||||
button:focus:not(:focus-visible) { |
||||
outline: 0; |
||||
} |
||||
|
||||
input, |
||||
button, |
||||
select, |
||||
optgroup, |
||||
textarea { |
||||
margin: 0; |
||||
font-family: inherit; |
||||
font-size: inherit; |
||||
line-height: inherit; |
||||
} |
||||
|
||||
button, |
||||
select { |
||||
text-transform: none; |
||||
} |
||||
|
||||
[role=button] { |
||||
cursor: pointer; |
||||
} |
||||
|
||||
select { |
||||
word-wrap: normal; |
||||
} |
||||
select:disabled { |
||||
opacity: 1; |
||||
} |
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { |
||||
display: none !important; |
||||
} |
||||
|
||||
button, |
||||
[type=button], |
||||
[type=reset], |
||||
[type=submit] { |
||||
-webkit-appearance: button; |
||||
} |
||||
button:not(:disabled), |
||||
[type=button]:not(:disabled), |
||||
[type=reset]:not(:disabled), |
||||
[type=submit]:not(:disabled) { |
||||
cursor: pointer; |
||||
} |
||||
|
||||
::-moz-focus-inner { |
||||
padding: 0; |
||||
border-style: none; |
||||
} |
||||
|
||||
textarea { |
||||
resize: vertical; |
||||
} |
||||
|
||||
fieldset { |
||||
min-width: 0; |
||||
padding: 0; |
||||
margin: 0; |
||||
border: 0; |
||||
} |
||||
|
||||
legend { |
||||
float: left; |
||||
width: 100%; |
||||
padding: 0; |
||||
margin-bottom: 0.5rem; |
||||
font-size: calc(1.275rem + 0.3vw); |
||||
line-height: inherit; |
||||
} |
||||
@media (min-width: 1200px) { |
||||
legend { |
||||
font-size: 1.5rem; |
||||
} |
||||
} |
||||
legend + * { |
||||
clear: left; |
||||
} |
||||
|
||||
::-webkit-datetime-edit-fields-wrapper, |
||||
::-webkit-datetime-edit-text, |
||||
::-webkit-datetime-edit-minute, |
||||
::-webkit-datetime-edit-hour-field, |
||||
::-webkit-datetime-edit-day-field, |
||||
::-webkit-datetime-edit-month-field, |
||||
::-webkit-datetime-edit-year-field { |
||||
padding: 0; |
||||
} |
||||
|
||||
::-webkit-inner-spin-button { |
||||
height: auto; |
||||
} |
||||
|
||||
[type=search] { |
||||
outline-offset: -2px; |
||||
-webkit-appearance: textfield; |
||||
} |
||||
|
||||
/* rtl:raw: |
||||
[type="tel"], |
||||
[type="url"], |
||||
[type="email"], |
||||
[type="number"] { |
||||
direction: ltr; |
||||
} |
||||
*/ |
||||
::-webkit-search-decoration { |
||||
-webkit-appearance: none; |
||||
} |
||||
|
||||
::-webkit-color-swatch-wrapper { |
||||
padding: 0; |
||||
} |
||||
|
||||
::-webkit-file-upload-button { |
||||
font: inherit; |
||||
-webkit-appearance: button; |
||||
} |
||||
|
||||
::file-selector-button { |
||||
font: inherit; |
||||
-webkit-appearance: button; |
||||
} |
||||
|
||||
output { |
||||
display: inline-block; |
||||
} |
||||
|
||||
iframe { |
||||
border: 0; |
||||
} |
||||
|
||||
summary { |
||||
display: list-item; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
progress { |
||||
vertical-align: baseline; |
||||
} |
||||
|
||||
[hidden] { |
||||
display: none !important; |
||||
} |
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */ |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,590 @@
|
||||
/*! |
||||
* Bootstrap Reboot v5.3.0 (https://getbootstrap.com/) |
||||
* Copyright 2011-2023 The Bootstrap Authors |
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
||||
*/ |
||||
:root, |
||||
[data-bs-theme=light] { |
||||
--bs-blue: #0d6efd; |
||||
--bs-indigo: #6610f2; |
||||
--bs-purple: #6f42c1; |
||||
--bs-pink: #d63384; |
||||
--bs-red: #dc3545; |
||||
--bs-orange: #fd7e14; |
||||
--bs-yellow: #ffc107; |
||||
--bs-green: #198754; |
||||
--bs-teal: #20c997; |
||||
--bs-cyan: #0dcaf0; |
||||
--bs-black: #000; |
||||
--bs-white: #fff; |
||||
--bs-gray: #6c757d; |
||||
--bs-gray-dark: #343a40; |
||||
--bs-gray-100: #f8f9fa; |
||||
--bs-gray-200: #e9ecef; |
||||
--bs-gray-300: #dee2e6; |
||||
--bs-gray-400: #ced4da; |
||||
--bs-gray-500: #adb5bd; |
||||
--bs-gray-600: #6c757d; |
||||
--bs-gray-700: #495057; |
||||
--bs-gray-800: #343a40; |
||||
--bs-gray-900: #212529; |
||||
--bs-primary: #0d6efd; |
||||
--bs-secondary: #6c757d; |
||||
--bs-success: #198754; |
||||
--bs-info: #0dcaf0; |
||||
--bs-warning: #ffc107; |
||||
--bs-danger: #dc3545; |
||||
--bs-light: #f8f9fa; |
||||
--bs-dark: #212529; |
||||
--bs-primary-rgb: 13, 110, 253; |
||||
--bs-secondary-rgb: 108, 117, 125; |
||||
--bs-success-rgb: 25, 135, 84; |
||||
--bs-info-rgb: 13, 202, 240; |
||||
--bs-warning-rgb: 255, 193, 7; |
||||
--bs-danger-rgb: 220, 53, 69; |
||||
--bs-light-rgb: 248, 249, 250; |
||||
--bs-dark-rgb: 33, 37, 41; |
||||
--bs-primary-text-emphasis: #052c65; |
||||
--bs-secondary-text-emphasis: #2b2f32; |
||||
--bs-success-text-emphasis: #0a3622; |
||||
--bs-info-text-emphasis: #055160; |
||||
--bs-warning-text-emphasis: #664d03; |
||||
--bs-danger-text-emphasis: #58151c; |
||||
--bs-light-text-emphasis: #495057; |
||||
--bs-dark-text-emphasis: #495057; |
||||
--bs-primary-bg-subtle: #cfe2ff; |
||||
--bs-secondary-bg-subtle: #e2e3e5; |
||||
--bs-success-bg-subtle: #d1e7dd; |
||||
--bs-info-bg-subtle: #cff4fc; |
||||
--bs-warning-bg-subtle: #fff3cd; |
||||
--bs-danger-bg-subtle: #f8d7da; |
||||
--bs-light-bg-subtle: #fcfcfd; |
||||
--bs-dark-bg-subtle: #ced4da; |
||||
--bs-primary-border-subtle: #9ec5fe; |
||||
--bs-secondary-border-subtle: #c4c8cb; |
||||
--bs-success-border-subtle: #a3cfbb; |
||||
--bs-info-border-subtle: #9eeaf9; |
||||
--bs-warning-border-subtle: #ffe69c; |
||||
--bs-danger-border-subtle: #f1aeb5; |
||||
--bs-light-border-subtle: #e9ecef; |
||||
--bs-dark-border-subtle: #adb5bd; |
||||
--bs-white-rgb: 255, 255, 255; |
||||
--bs-black-rgb: 0, 0, 0; |
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; |
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; |
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); |
||||
--bs-body-font-family: var(--bs-font-sans-serif); |
||||
--bs-body-font-size: 1rem; |
||||
--bs-body-font-weight: 400; |
||||
--bs-body-line-height: 1.5; |
||||
--bs-body-color: #212529; |
||||
--bs-body-color-rgb: 33, 37, 41; |
||||
--bs-body-bg: #fff; |
||||
--bs-body-bg-rgb: 255, 255, 255; |
||||
--bs-emphasis-color: #000; |
||||
--bs-emphasis-color-rgb: 0, 0, 0; |
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75); |
||||
--bs-secondary-color-rgb: 33, 37, 41; |
||||
--bs-secondary-bg: #e9ecef; |
||||
--bs-secondary-bg-rgb: 233, 236, 239; |
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5); |
||||
--bs-tertiary-color-rgb: 33, 37, 41; |
||||
--bs-tertiary-bg: #f8f9fa; |
||||
--bs-tertiary-bg-rgb: 248, 249, 250; |
||||
--bs-heading-color: inherit; |
||||
--bs-link-color: #0d6efd; |
||||
--bs-link-color-rgb: 13, 110, 253; |
||||
--bs-link-decoration: underline; |
||||
--bs-link-hover-color: #0a58ca; |
||||
--bs-link-hover-color-rgb: 10, 88, 202; |
||||
--bs-code-color: #d63384; |
||||
--bs-highlight-bg: #fff3cd; |
||||
--bs-border-width: 1px; |
||||
--bs-border-style: solid; |
||||
--bs-border-color: #dee2e6; |
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175); |
||||
--bs-border-radius: 0.375rem; |
||||
--bs-border-radius-sm: 0.25rem; |
||||
--bs-border-radius-lg: 0.5rem; |
||||
--bs-border-radius-xl: 1rem; |
||||
--bs-border-radius-xxl: 2rem; |
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl); |
||||
--bs-border-radius-pill: 50rem; |
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); |
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); |
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); |
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); |
||||
--bs-focus-ring-width: 0.25rem; |
||||
--bs-focus-ring-opacity: 0.25; |
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25); |
||||
--bs-form-valid-color: #198754; |
||||
--bs-form-valid-border-color: #198754; |
||||
--bs-form-invalid-color: #dc3545; |
||||
--bs-form-invalid-border-color: #dc3545; |
||||
} |
||||
|
||||
[data-bs-theme=dark] { |
||||
color-scheme: dark; |
||||
--bs-body-color: #adb5bd; |
||||
--bs-body-color-rgb: 173, 181, 189; |
||||
--bs-body-bg: #212529; |
||||
--bs-body-bg-rgb: 33, 37, 41; |
||||
--bs-emphasis-color: #fff; |
||||
--bs-emphasis-color-rgb: 255, 255, 255; |
||||
--bs-secondary-color: rgba(173, 181, 189, 0.75); |
||||
--bs-secondary-color-rgb: 173, 181, 189; |
||||
--bs-secondary-bg: #343a40; |
||||
--bs-secondary-bg-rgb: 52, 58, 64; |
||||
--bs-tertiary-color: rgba(173, 181, 189, 0.5); |
||||
--bs-tertiary-color-rgb: 173, 181, 189; |
||||
--bs-tertiary-bg: #2b3035; |
||||
--bs-tertiary-bg-rgb: 43, 48, 53; |
||||
--bs-primary-text-emphasis: #6ea8fe; |
||||
--bs-secondary-text-emphasis: #a7acb1; |
||||
--bs-success-text-emphasis: #75b798; |
||||
--bs-info-text-emphasis: #6edff6; |
||||
--bs-warning-text-emphasis: #ffda6a; |
||||
--bs-danger-text-emphasis: #ea868f; |
||||
--bs-light-text-emphasis: #f8f9fa; |
||||
--bs-dark-text-emphasis: #dee2e6; |
||||
--bs-primary-bg-subtle: #031633; |
||||
--bs-secondary-bg-subtle: #161719; |
||||
--bs-success-bg-subtle: #051b11; |
||||
--bs-info-bg-subtle: #032830; |
||||
--bs-warning-bg-subtle: #332701; |
||||
--bs-danger-bg-subtle: #2c0b0e; |
||||
--bs-light-bg-subtle: #343a40; |
||||
--bs-dark-bg-subtle: #1a1d20; |
||||
--bs-primary-border-subtle: #084298; |
||||
--bs-secondary-border-subtle: #41464b; |
||||
--bs-success-border-subtle: #0f5132; |
||||
--bs-info-border-subtle: #087990; |
||||
--bs-warning-border-subtle: #997404; |
||||
--bs-danger-border-subtle: #842029; |
||||
--bs-light-border-subtle: #495057; |
||||
--bs-dark-border-subtle: #343a40; |
||||
--bs-heading-color: inherit; |
||||
--bs-link-color: #6ea8fe; |
||||
--bs-link-hover-color: #8bb9fe; |
||||
--bs-link-color-rgb: 110, 168, 254; |
||||
--bs-link-hover-color-rgb: 139, 185, 254; |
||||
--bs-code-color: #e685b5; |
||||
--bs-border-color: #495057; |
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15); |
||||
--bs-form-valid-color: #75b798; |
||||
--bs-form-valid-border-color: #75b798; |
||||
--bs-form-invalid-color: #ea868f; |
||||
--bs-form-invalid-border-color: #ea868f; |
||||
} |
||||
|
||||
*, |
||||
*::before, |
||||
*::after { |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
@media (prefers-reduced-motion: no-preference) { |
||||
:root { |
||||
scroll-behavior: smooth; |
||||
} |
||||
} |
||||
|
||||
body { |
||||
margin: 0; |
||||
font-family: var(--bs-body-font-family); |
||||
font-size: var(--bs-body-font-size); |
||||
font-weight: var(--bs-body-font-weight); |
||||
line-height: var(--bs-body-line-height); |
||||
color: var(--bs-body-color); |
||||
text-align: var(--bs-body-text-align); |
||||
background-color: var(--bs-body-bg); |
||||
-webkit-text-size-adjust: 100%; |
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); |
||||
} |
||||
|
||||
hr { |
||||
margin: 1rem 0; |
||||
color: inherit; |
||||
border: 0; |
||||
border-top: var(--bs-border-width) solid; |
||||
opacity: 0.25; |
||||
} |
||||
|
||||
h6, h5, h4, h3, h2, h1 { |
||||
margin-top: 0; |
||||
margin-bottom: 0.5rem; |
||||
font-weight: 500; |
||||
line-height: 1.2; |
||||
color: var(--bs-heading-color); |
||||
} |
||||
|
||||
h1 { |
||||
font-size: calc(1.375rem + 1.5vw); |
||||
} |
||||
@media (min-width: 1200px) { |
||||
h1 { |
||||
font-size: 2.5rem; |
||||
} |
||||
} |
||||
|
||||
h2 { |
||||
font-size: calc(1.325rem + 0.9vw); |
||||
} |
||||
@media (min-width: 1200px) { |
||||
h2 { |
||||
font-size: 2rem; |
||||
} |
||||
} |
||||
|
||||
h3 { |
||||
font-size: calc(1.3rem + 0.6vw); |
||||
} |
||||
@media (min-width: 1200px) { |
||||
h3 { |
||||
font-size: 1.75rem; |
||||
} |
||||
} |
||||
|
||||
h4 { |
||||
font-size: calc(1.275rem + 0.3vw); |
||||
} |
||||
@media (min-width: 1200px) { |
||||
h4 { |
||||
font-size: 1.5rem; |
||||
} |
||||
} |
||||
|
||||
h5 { |
||||
font-size: 1.25rem; |
||||
} |
||||
|
||||
h6 { |
||||
font-size: 1rem; |
||||
} |
||||
|
||||
p { |
||||
margin-top: 0; |
||||
margin-bottom: 1rem; |
||||
} |
||||
|
||||
abbr[title] { |
||||
-webkit-text-decoration: underline dotted; |
||||
text-decoration: underline dotted; |
||||
cursor: help; |
||||
-webkit-text-decoration-skip-ink: none; |
||||
text-decoration-skip-ink: none; |
||||
} |
||||
|
||||
address { |
||||
margin-bottom: 1rem; |
||||
font-style: normal; |
||||
line-height: inherit; |
||||
} |
||||
|
||||
ol, |
||||
ul { |
||||
padding-right: 2rem; |
||||
} |
||||
|
||||
ol, |
||||
ul, |
||||
dl { |
||||
margin-top: 0; |
||||
margin-bottom: 1rem; |
||||
} |
||||
|
||||
ol ol, |
||||
ul ul, |
||||
ol ul, |
||||
ul ol { |
||||
margin-bottom: 0; |
||||
} |
||||
|
||||
dt { |
||||
font-weight: 700; |
||||
} |
||||
|
||||
dd { |
||||
margin-bottom: 0.5rem; |
||||
margin-right: 0; |
||||
} |
||||
|
||||
blockquote { |
||||
margin: 0 0 1rem; |
||||
} |
||||
|
||||
b, |
||||
strong { |
||||
font-weight: bolder; |
||||
} |
||||
|
||||
small { |
||||
font-size: 0.875em; |
||||
} |
||||
|
||||
mark { |
||||
padding: 0.1875em; |
||||
background-color: var(--bs-highlight-bg); |
||||
} |
||||
|
||||
sub, |
||||
sup { |
||||
position: relative; |
||||
font-size: 0.75em; |
||||
line-height: 0; |
||||
vertical-align: baseline; |
||||
} |
||||
|
||||
sub { |
||||
bottom: -0.25em; |
||||
} |
||||
|
||||
sup { |
||||
top: -0.5em; |
||||
} |
||||
|
||||
a { |
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); |
||||
text-decoration: underline; |
||||
} |
||||
a:hover { |
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb); |
||||
} |
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover { |
||||
color: inherit; |
||||
text-decoration: none; |
||||
} |
||||
|
||||
pre, |
||||
code, |
||||
kbd, |
||||
samp { |
||||
font-family: var(--bs-font-monospace); |
||||
font-size: 1em; |
||||
} |
||||
|
||||
pre { |
||||
display: block; |
||||
margin-top: 0; |
||||
margin-bottom: 1rem; |
||||
overflow: auto; |
||||
font-size: 0.875em; |
||||
} |
||||
pre code { |
||||
font-size: inherit; |
||||
color: inherit; |
||||
word-break: normal; |
||||
} |
||||
|
||||
code { |
||||
font-size: 0.875em; |
||||
color: var(--bs-code-color); |
||||
word-wrap: break-word; |
||||
} |
||||
a > code { |
||||
color: inherit; |
||||
} |
||||
|
||||
kbd { |
||||
padding: 0.1875rem 0.375rem; |
||||
font-size: 0.875em; |
||||
color: var(--bs-body-bg); |
||||
background-color: var(--bs-body-color); |
||||
border-radius: 0.25rem; |
||||
} |
||||
kbd kbd { |
||||
padding: 0; |
||||
font-size: 1em; |
||||
} |
||||
|
||||
figure { |
||||
margin: 0 0 1rem; |
||||
} |
||||
|
||||
img, |
||||
svg { |
||||
vertical-align: middle; |
||||
} |
||||
|
||||
table { |
||||
caption-side: bottom; |
||||
border-collapse: collapse; |
||||
} |
||||
|
||||
caption { |
||||
padding-top: 0.5rem; |
||||
padding-bottom: 0.5rem; |
||||
color: var(--bs-secondary-color); |
||||
text-align: right; |
||||
} |
||||
|
||||
th { |
||||
text-align: inherit; |
||||
text-align: -webkit-match-parent; |
||||
} |
||||
|
||||
thead, |
||||
tbody, |
||||
tfoot, |
||||
tr, |
||||
td, |
||||
th { |
||||
border-color: inherit; |
||||
border-style: solid; |
||||
border-width: 0; |
||||
} |
||||
|
||||
label { |
||||
display: inline-block; |
||||
} |
||||
|
||||
button { |
||||
border-radius: 0; |
||||
} |
||||
|
||||
button:focus:not(:focus-visible) { |
||||
outline: 0; |
||||
} |
||||
|
||||
input, |
||||
button, |
||||
select, |
||||
optgroup, |
||||
textarea { |
||||
margin: 0; |
||||
font-family: inherit; |
||||
font-size: inherit; |
||||
line-height: inherit; |
||||
} |
||||
|
||||
button, |
||||
select { |
||||
text-transform: none; |
||||
} |
||||
|
||||
[role=button] { |
||||
cursor: pointer; |
||||
} |
||||
|
||||
select { |
||||
word-wrap: normal; |
||||
} |
||||
select:disabled { |
||||
opacity: 1; |
||||
} |
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { |
||||
display: none !important; |
||||
} |
||||
|
||||
button, |
||||
[type=button], |
||||
[type=reset], |
||||
[type=submit] { |
||||
-webkit-appearance: button; |
||||
} |
||||
button:not(:disabled), |
||||
[type=button]:not(:disabled), |
||||
[type=reset]:not(:disabled), |
||||
[type=submit]:not(:disabled) { |
||||
cursor: pointer; |
||||
} |
||||
|
||||
::-moz-focus-inner { |
||||
padding: 0; |
||||
border-style: none; |
||||
} |
||||
|
||||
textarea { |
||||
resize: vertical; |
||||
} |
||||
|
||||
fieldset { |
||||
min-width: 0; |
||||
padding: 0; |
||||
margin: 0; |
||||
border: 0; |
||||
} |
||||
|
||||
legend { |
||||
float: right; |
||||
width: 100%; |
||||
padding: 0; |
||||
margin-bottom: 0.5rem; |
||||
font-size: calc(1.275rem + 0.3vw); |
||||
line-height: inherit; |
||||
} |
||||
@media (min-width: 1200px) { |
||||
legend { |
||||
font-size: 1.5rem; |
||||
} |
||||
} |
||||
legend + * { |
||||
clear: right; |
||||
} |
||||
|
||||
::-webkit-datetime-edit-fields-wrapper, |
||||
::-webkit-datetime-edit-text, |
||||
::-webkit-datetime-edit-minute, |
||||
::-webkit-datetime-edit-hour-field, |
||||
::-webkit-datetime-edit-day-field, |
||||
::-webkit-datetime-edit-month-field, |
||||
::-webkit-datetime-edit-year-field { |
||||
padding: 0; |
||||
} |
||||
|
||||
::-webkit-inner-spin-button { |
||||
height: auto; |
||||
} |
||||
|
||||
[type=search] { |
||||
outline-offset: -2px; |
||||
-webkit-appearance: textfield; |
||||
} |
||||
|
||||
[type="tel"], |
||||
[type="url"], |
||||
[type="email"], |
||||
[type="number"] { |
||||
direction: ltr; |
||||
} |
||||
::-webkit-search-decoration { |
||||
-webkit-appearance: none; |
||||
} |
||||
|
||||
::-webkit-color-swatch-wrapper { |
||||
padding: 0; |
||||
} |
||||
|
||||
::-webkit-file-upload-button { |
||||
font: inherit; |
||||
-webkit-appearance: button; |
||||
} |
||||
|
||||
::file-selector-button { |
||||
font: inherit; |
||||
-webkit-appearance: button; |
||||
} |
||||
|
||||
output { |
||||
display: inline-block; |
||||
} |
||||
|
||||
iframe { |
||||
border: 0; |
||||
} |
||||
|
||||
summary { |
||||
display: list-item; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
progress { |
||||
vertical-align: baseline; |
||||
} |
||||
|
||||
[hidden] { |
||||
display: none !important; |
||||
} |
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */ |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue