Compare commits
No commits in common. "main" and "1.2-1" have entirely different histories.
|
@ -1,4 +1,3 @@
|
||||||
database.db
|
database.db
|
||||||
config.ini
|
config.ini
|
||||||
config.ini.example
|
config.ini.example
|
||||||
.idea
|
|
118
APIDOCS.md
118
APIDOCS.md
|
@ -7,16 +7,34 @@ Headers should be: "Content-type: application/json; charset=UTF-8" for all POSTs
|
||||||
|
|
||||||
POST - /api/signup - provide "username" and "password".
|
POST - /api/signup - provide "username" and "password".
|
||||||
|
|
||||||
POST - /api/login - provide "username" and "password"
|
POST - /api/login - provide "username", "password", "passwordchange" (must be "yes" or "no") and "newpass"
|
||||||
|
|
||||||
|
To prevent the server from knowing the encryption key, the password you provide in the request must be hashed with the SHA-3 with 128 iterations (the hash is hashed again 128 times).
|
||||||
|
|
||||||
|
If you wish to change the user's password, set "passwordchange" to "yes" and "newpass" to the new hash.
|
||||||
|
|
||||||
|
|
||||||
|
Some users use the legacy argon2id mode (by which i mean about 8, so only implement if you feel like it), and to implement argon2id functionality, you hash like this:
|
||||||
|
```
|
||||||
|
Parallelism should be 1
|
||||||
|
|
||||||
|
Iterations should be 256
|
||||||
|
|
||||||
|
Memory Allocated in bytes should be 512
|
||||||
|
|
||||||
|
Length of Hash should be 32 bytes
|
||||||
|
|
||||||
|
The output should be in the encoded format, not the hashed format
|
||||||
|
|
||||||
|
Salt should be the SHA512 of the password
|
||||||
|
```
|
||||||
|
|
||||||
|
(Yes i know this is really bad practice, guess why we are replacing it)
|
||||||
|
|
||||||
|
To test if SHA-3 or argon2 is used, just try the SHA-3 and if 422 gets returned try argon2.
|
||||||
|
|
||||||
|
(For the sake of all of us, change the password to the SHA-3 hash)
|
||||||
|
|
||||||
To prevent the server from knowing the encryption key, the password you provide in the request must be hashed with Argon2ID as per the following parameters:
|
|
||||||
|
|
||||||
- salt: UTF-8 Representation of "I munch Burgers!!" (should be 16 bytes),
|
|
||||||
- parallelism: 1
|
|
||||||
- iterations: 32
|
|
||||||
- memorySize: 19264 bytes
|
|
||||||
- hashLength: 32
|
|
||||||
- outputType: hexadecimal
|
|
||||||
|
|
||||||
Password should be at least 8 characters, username must be under 20 characters and alphanumeric.
|
Password should be at least 8 characters, username must be under 20 characters and alphanumeric.
|
||||||
|
|
||||||
|
@ -26,63 +44,7 @@ Assuming everything went correctly, the server will return a secret key.
|
||||||
|
|
||||||
You'll need to store two things in local storage:
|
You'll need to store two things in local storage:
|
||||||
- The secret key you just got, used to fetch notes, save stuff etc.
|
- The secret key you just got, used to fetch notes, save stuff etc.
|
||||||
- Another password, which is hashed the same way, but with a salt of "I love Burgernotes!" (again, UTF-8 representation, 16 bytes).
|
- A SHA512 hashed password, used as encryption key
|
||||||
|
|
||||||
If you are using the OAuth2 flow (totally optional, I know it's really complex) then you should also store the login password to use later, or put the OAuth2 logic straight after this.
|
|
||||||
|
|
||||||
## 🌐 OAuth2 + Burgerauth
|
|
||||||
|
|
||||||
For security purposes, traditional OAuth2 is not supported. Instead, we use Burgerauth, a custom OAuth2 implementation that provides a unique-yet-consistent "authentication code" for each user. It is created in a special way as to not involve the server, meaning the security of Burgernotes is not compromised by using OAuth2, which is normally a very server-side process.
|
|
||||||
|
|
||||||
### How it works
|
|
||||||
First, perform regular client-side OAuth2 authentication using Burgerauth, as detailed in its [own documentation](https://concord.hectabit.org/HectaBit/burgerauth/src/branch/main/APIDOCS.md). Once regular OAuth2 is complete, you will be given an authentication code, which is important for the next step.
|
|
||||||
|
|
||||||
You now have one of two options:
|
|
||||||
1. If your app is based on the web, you can host a static page provided [here](https://concord.hectabit.org/HectaBit/burgerauth/src/branch/main/keyExchangeRdir.html) on any static service. Redirect to this page with the OAuth2 token stored in localStorage as BURGERAUTH-RDIR-TOKEN. The page will then communicate with a corresponding page on Burgerauth, and transmit the key securely via RSA. You may see the page redirect a couple of times as it communicates the infomation across. All you need to know is that once it is finished, it will redirect back to the page that redirected to it with the key in localStorage as DONOTSHARE-EXCHANGED-KEY.
|
|
||||||
2. If your app is not web-based, you can open up a webview to [here](https://auth.hectabit.org/keyexchangeclient). Once it is finished, it will send a postMessage with the body "finished" to the target "*" and output "finished" to the JavaScript console. The key will be in localStorage as DONOTSHARE-EXCHANGED-KEY.
|
|
||||||
3. Alternatively, you can host a local webserver and host the aforementioned page on it. It will work the same way as the first option, and once it is finished, it will detect that it was not redirected to and instead will set it as a cookie expiring in 5 minutes and then refresh the page. You should detect for the cookie and use its value, and then kill the webserver. This method is not recommended because of its complexity and overhead.
|
|
||||||
|
|
||||||
Once this is finished, you should check if there is an existing OAuth2 entry on the server like this:
|
|
||||||
|
|
||||||
POST - /api/oauth/get - provide "username" and "oauthProvider"
|
|
||||||
oauthProvider is the name of the OAuth2 provider, such as "burgerauth" or "google" (google is used as an example, they do not use the burgerauth extensions and are therefore incompatible).
|
|
||||||
It does not have to be the actual name, but it has to be unique to the provider (per user). The sub given by OpenID Connect is a good choice.
|
|
||||||
|
|
||||||
### 404 is returned
|
|
||||||
No entry has been found, and you have to log in the user as normal.
|
|
||||||
Once this is done, you should create an entry like this:
|
|
||||||
|
|
||||||
POST - /api/oauth/new - provide "secretKey", "oauthProvider" and "encryptedPassword"
|
|
||||||
|
|
||||||
To create encryptedPassword, follow these steps:
|
|
||||||
|
|
||||||
1. Generate a random 16-byte IV.
|
|
||||||
2. Create a JSON structure like this:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"loginPass": "(the SHA-3 password hash created in the login process)",
|
|
||||||
"cryptoPass": "(the SHA-512 password hash stored in localStorage)"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
3. Convert the JSON to a string and then encrypt it using AES-256 GCM using the exchangeKey as the key and the IV created earlier as the IV.
|
|
||||||
4. Create a JSON structure like this:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"iv": "(the IV)",
|
|
||||||
"content": "(the encrypted JSON)"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
5. Finally, convert the JSON into a string, base64 encode it, and send it off as encryptedPassword.
|
|
||||||
|
|
||||||
Do not store the exchangeKey after this point, as it is no longer needed.
|
|
||||||
|
|
||||||
### 200 is returned
|
|
||||||
An entry exists, and encryptedPassword has been returned using JSON.
|
|
||||||
encryptedPassword is the password encrypted using AES-256 GCM, and the IV is included in the JSON, in this format defined above.
|
|
||||||
|
|
||||||
Use the passwords defined in the JSON structure before the last one to log in normally.
|
|
||||||
|
|
||||||
#### Finally, you are done!
|
|
||||||
|
|
||||||
## 🔐 Encryption
|
## 🔐 Encryption
|
||||||
|
|
||||||
|
@ -98,38 +60,28 @@ POST - /api/listnotes - list notes, provide "secretKey"
|
||||||
note titles will have to be decrypted.
|
note titles will have to be decrypted.
|
||||||
|
|
||||||
POST - /api/newnote - create a note, provide "secretKey" and "noteName"
|
POST - /api/newnote - create a note, provide "secretKey" and "noteName"
|
||||||
"noteName" should be encrypted using AES-256 GCM with the DONOTSHARE-password as the key and a random 16-byte IV.
|
"noteName" should be encrypted.
|
||||||
|
|
||||||
POST - /api/readnote - read notes, provide "secretKey" and "noteId"
|
POST - /api/readnote - read notes, provide "secretKey" and "noteId"
|
||||||
note content will have to be decrypted.
|
note content will have to be decrypted.
|
||||||
|
|
||||||
POST - /api/editnote - edit notes, provide "secretKey", "noteId", "title", and "content"
|
POST - /api/editnote - edit notes, provide "secretKey", "noteId", "title", and "content"
|
||||||
"content" should be encrypted using AES-256 GCM with the DONOTSHARE-password as the key and a random 16-byte IV.
|
"content" should be encrypted.
|
||||||
"title" is the first line of the note content, and should be encrypted.
|
"title" is the first line of the note content, and should be encrypted.
|
||||||
|
|
||||||
|
**(Deprecated ⚠️)** POST - /api/editnotetitle - edit note titles, provide "secretKey", "noteId", and "content"
|
||||||
|
"content" should be encrypted.
|
||||||
|
|
||||||
POST - /api/removenote - remove notes, provide "secretKey" and "noteId"
|
POST - /api/removenote - remove notes, provide "secretKey" and "noteId"
|
||||||
|
|
||||||
POST - /api/purgenotes - remove all notes, provide "secretKey"
|
## ⚙️ More stuff
|
||||||
### Please display a warning before this action
|
|
||||||
|
|
||||||
## ⚙️ Account management
|
|
||||||
|
|
||||||
POST - /api/changepassword - change account password, provide "secretKey", "newPassword"
|
|
||||||
encrypt the same way as /api/login
|
|
||||||
|
|
||||||
POST - /api/deleteaccount - delete account, provide "secretKey"
|
POST - /api/deleteaccount - delete account, provide "secretKey"
|
||||||
### Please display a warning before this action
|
please display a warning before this action
|
||||||
|
|
||||||
POST - /api/exportnotes - export notes, provide "secretKey"
|
POST - /api/exportnotes - export notes, provide "secretKey"
|
||||||
note content and title will have to be decrypted
|
note content and title will have to be decrypted
|
||||||
|
|
||||||
POST - /api/importnotes - import notes, provide "secretKey" and "notes"
|
|
||||||
note content and title should be encrypted using AES-256 GCM with the DONOTSHARE-password as the key and a random 16-byte IV and follow the /api/exportnotes format, in a marshalled json string
|
|
||||||
|
|
||||||
POST - /api/sessions/list - show all sessions, provide "secretKey"
|
POST - /api/sessions/list - show all sessions, provide "secretKey"
|
||||||
|
|
||||||
POST - /api/sessions/remove - remove session, provide "secretKey" and "sessionId"
|
POST - /api/sessions/remove - remove session, provide "secretKey" and "sessionId"
|
||||||
|
|
||||||
## 💼 Admin controls
|
|
||||||
|
|
||||||
POST - /api/listusers - lists all users in JSON, provide "masterKey" (set in config.ini)
|
|
||||||
|
|
23
ERRORS.md
23
ERRORS.md
|
@ -1,23 +0,0 @@
|
||||||
# Errors in Burger-based software and how to handle them
|
|
||||||
|
|
||||||
## The console
|
|
||||||
|
|
||||||
All Burger-based software uses a simple logging system that outputs to TTY. Log files are not provided by default and users are expected to use pipes to redirect the logs as they wish.
|
|
||||||
|
|
||||||
A log entry looks something like this:
|
|
||||||
|
|
||||||
| DATE | HUMAN-READABLE TIME | LOGLEVEL | DESCRIPTION |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| 1969/12/31 | 11:59:59 | [INFO] | Added a new user |
|
|
||||||
|
|
||||||
## Log levels
|
|
||||||
|
|
||||||
There are 5 different log levels, with differing amounts of urgency
|
|
||||||
|
|
||||||
| INFO | WARN | ERROR | CRITICAL | FATAL | PROMPT |
|
|
||||||
|---------------------------------------------------------|---------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|-------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
|
|
||||||
| Usually harmless information, like a user being created | A warning about bad practices being used, such as having an unset config option | An error that disrupts user experience and may lead to undesired client-side behaviour | An error that affects all users on the platform | An error critical enough to warrant crashing the server process, usually something like the server being unable to bind to an IP or not being able to create the database | Anything that asks the user for input, like a confirmation dialog (typically has no timestamp) |
|
|
||||||
|
|
||||||
## Error reporting
|
|
||||||
|
|
||||||
Clients will be given 500 status code and an error code if any errors were to affect them. They are told to come to this page for more information. If you are one such client, please go to the issues tab and paste the error code along with some context, so we can fix the bug.
|
|
|
@ -5,12 +5,12 @@ Burgernotes is a simple note-taking app with end-to-end encryption.
|
||||||
To set up Burgernotes, simply run these commands:
|
To set up Burgernotes, simply run these commands:
|
||||||
```
|
```
|
||||||
cp config.ini.example config.ini
|
cp config.ini.example config.ini
|
||||||
./burgernotes init_db
|
python3 init_db
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit config.ini to your liking, then to start the server run:
|
Edit config.ini to your liking, then to start the server run:
|
||||||
```
|
```
|
||||||
./burgernotes
|
python3 main
|
||||||
```
|
```
|
||||||
### Links
|
### Links
|
||||||
[Go to the Burgernotes website](https://notes.hectabit.org)
|
[Go to the Burgernotes website](https://notes.hectabit.org)
|
||||||
|
|
|
@ -2,5 +2,7 @@
|
||||||
|
|
||||||
- Switch to WebSockets for updating notes + live updating of note list and more, this involves redoing some APIs
|
- Switch to WebSockets for updating notes + live updating of note list and more, this involves redoing some APIs
|
||||||
- Compress notes to reduce bandwidth and storage
|
- Compress notes to reduce bandwidth and storage
|
||||||
- Use Burgerauth for authentication
|
|
||||||
- Dedicated domain (not just a subdomain, if anyone can donate a domain let Arzumify know!)
|
- Dedicated domain (not just a subdomain, if anyone can donate a domain let Arzumify know!)
|
||||||
|
- Native Apps (native iOS and Linux apps* are in development)
|
||||||
|
|
||||||
|
*kinda, not really active much :3
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def run_init_db():
|
||||||
|
# Run the init_db.py script using subprocess
|
||||||
|
try:
|
||||||
|
subprocess.run(["python", "init_db"])
|
||||||
|
except Exception as e:
|
||||||
|
print("Error:", e)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
while True:
|
||||||
|
# Run init_db.py
|
||||||
|
run_init_db()
|
||||||
|
|
||||||
|
# Wait for 5 minutes before running again
|
||||||
|
time.sleep(300) # 300 seconds = 5 minutes
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
import configparser
|
||||||
|
from waitress import serve
|
||||||
|
from flask import Flask, render_template, request, url_for, flash, redirect, session, make_response, send_from_directory, stream_with_context, Response, request
|
||||||
|
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read("config.ini")
|
||||||
|
|
||||||
|
HOST = config["config"]["HOST"]
|
||||||
|
PORT = config["config"]["PORT"]
|
||||||
|
SECRET_KEY = config["config"]["SECRET_KEY"]
|
||||||
|
|
||||||
|
app = Flask(__name__, static_folder=None)
|
||||||
|
app.config["SECRET_KEY"] = SECRET_KEY
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def main(e):
|
||||||
|
return render_template("down.html"), 503
|
||||||
|
|
||||||
|
@app.route("/grid.svg")
|
||||||
|
def staticgrid():
|
||||||
|
return Response("""<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="0.25" cy="0.25" r="0.25" fill="white"/><circle cx="9.25" cy="0.25" r="0.25" fill="white"/><circle cx="18.25" cy="0.25" r="0.25" fill="white"/><circle cx="27.25" cy="0.25" r="0.25" fill="white"/><circle cx="36.25" cy="0.25" r="0.25" fill="white"/><circle cx="45.25" cy="0.25" r="0.25" fill="white"/><circle cx="54.25" cy="0.25" r="0.25" fill="white"/><circle cx="63.25" cy="0.25" r="0.25" fill="white"/><circle cx="72.25" cy="0.25" r="0.25" fill="white"/><circle cx="81.25" cy="0.25" r="0.25" fill="white"/><circle cx="90.25" cy="0.25" r="0.25" fill="white"/><circle cx="0.25" cy="9.25" r="0.25" fill="white"/><circle cx="9.25" cy="9.25" r="0.25" fill="white"/><circle cx="18.25" cy="9.25" r="0.25" fill="white"/><circle cx="27.25" cy="9.25" r="0.25" fill="white"/><circle cx="36.25" cy="9.25" r="0.25" fill="white"/><circle cx="45.25" cy="9.25" r="0.25" fill="white"/><circle cx="54.25" cy="9.25" r="0.25" fill="white"/><circle cx="63.25" cy="9.25" r="0.25" fill="white"/><circle cx="72.25" cy="9.25" r="0.25" fill="white"/><circle cx="81.25" cy="9.25" r="0.25" fill="white"/><circle cx="90.25" cy="9.25" r="0.25" fill="white"/><circle cx="0.5" cy="18.5" r="0.5" fill="white"/><circle cx="9.5" cy="18.5" r="0.5" fill="white"/><circle cx="18.5" cy="18.5" r="0.5" fill="white"/><circle cx="27.5" cy="18.5" r="0.5" fill="white"/><circle cx="36.5" cy="18.5" r="0.5" fill="white"/><circle cx="45.5" cy="18.5" r="0.5" fill="white"/><circle cx="54.5" cy="18.5" r="0.5" fill="white"/><circle cx="63.5" cy="18.5" r="0.5" fill="white"/><circle cx="72.5" cy="18.5" r="0.5" fill="white"/><circle cx="81.5" cy="18.5" r="0.5" fill="white"/><circle cx="90.5" cy="18.5" r="0.5" fill="white"/><circle cx="0.5" cy="27.5" r="0.5" fill="white"/><circle cx="9.5" cy="27.5" r="0.5" fill="white"/><circle cx="18.5" cy="27.5" r="0.5" fill="white"/><circle cx="27.5" cy="27.5" r="0.5" fill="white"/><circle cx="36.5" cy="27.5" r="0.5" fill="white"/><circle cx="45.5" cy="27.5" r="0.5" fill="white"/><circle cx="54.5" cy="27.5" r="0.5" fill="white"/><circle cx="63.5" cy="27.5" r="0.5" fill="white"/><circle cx="72.5" cy="27.5" r="0.5" fill="white"/><circle cx="81.5" cy="27.5" r="0.5" fill="white"/><circle cx="90.5" cy="27.5" r="0.5" fill="white"/><circle cx="0.75" cy="36.75" r="0.75" fill="white"/><circle cx="9.75" cy="36.75" r="0.75" fill="white"/><circle cx="18.75" cy="36.75" r="0.75" fill="white"/><circle cx="27.75" cy="36.75" r="0.75" fill="white"/><circle cx="36.75" cy="36.75" r="0.75" fill="white"/><circle cx="45.75" cy="36.75" r="0.75" fill="white"/><circle cx="54.75" cy="36.75" r="0.75" fill="white"/><circle cx="63.75" cy="36.75" r="0.75" fill="white"/><circle cx="72.75" cy="36.75" r="0.75" fill="white"/><circle cx="81.75" cy="36.75" r="0.75" fill="white"/><circle cx="90.75" cy="36.75" r="0.75" fill="white"/><circle cx="0.75" cy="45.75" r="0.75" fill="white"/><circle cx="9.75" cy="45.75" r="0.75" fill="white"/><circle cx="18.75" cy="45.75" r="0.75" fill="white"/><circle cx="27.75" cy="45.75" r="0.75" fill="white"/><circle cx="36.75" cy="45.75" r="0.75" fill="white"/><circle cx="45.75" cy="45.75" r="0.75" fill="white"/><circle cx="54.75" cy="45.75" r="0.75" fill="white"/><circle cx="63.75" cy="45.75" r="0.75" fill="white"/><circle cx="72.75" cy="45.75" r="0.75" fill="white"/><circle cx="81.75" cy="45.75" r="0.75" fill="white"/><circle cx="90.75" cy="45.75" r="0.75" fill="white"/><circle cx="1" cy="55" r="1" fill="white"/><circle cx="10" cy="55" r="1" fill="white"/><circle cx="19" cy="55" r="1" fill="white"/><circle cx="28" cy="55" r="1" fill="white"/><circle cx="37" cy="55" r="1" fill="white"/><circle cx="46" cy="55" r="1" fill="white"/><circle cx="55" cy="55" r="1" fill="white"/><circle cx="64" cy="55" r="1" fill="white"/><circle cx="73" cy="55" r="1" fill="white"/><circle cx="82" cy="55" r="1" fill="white"/><circle cx="91" cy="55" r="1" fill="white"/><circle cx="1" cy="64" r="1" fill="white"/><circle cx="10" cy="64" r="1" fill="white"/><circle cx="19" cy="64" r="1" fill="white"/><circle cx="28" cy="64" r="1" fill="white"/><circle cx="37" cy="64" r="1" fill="white"/><circle cx="46" cy="64" r="1" fill="white"/><circle cx="55" cy="64" r="1" fill="white"/><circle cx="64" cy="64" r="1" fill="white"/><circle cx="73" cy="64" r="1" fill="white"/><circle cx="82" cy="64" r="1" fill="white"/>
|
||||||
|
<circle cx="91" cy="64" r="1" fill="white"/><circle cx="1.25" cy="73.25" r="1.25" fill="white"/><circle cx="10.25" cy="73.25" r="1.25" fill="white"/><circle cx="19.25" cy="73.25" r="1.25" fill="white"/><circle cx="28.25" cy="73.25" r="1.25" fill="white"/><circle cx="37.25" cy="73.25" r="1.25" fill="white"/><circle cx="46.25" cy="73.25" r="1.25" fill="white"/><circle cx="55.25" cy="73.25" r="1.25" fill="white"/><circle cx="64.25" cy="73.25" r="1.25" fill="white"/><circle cx="73.25" cy="73.25" r="1.25" fill="white"/><circle cx="82.25" cy="73.25" r="1.25" fill="white"/><circle cx="91.25" cy="73.25" r="1.25" fill="white"/><circle cx="1.25" cy="82.25" r="1.25" fill="white"/><circle cx="10.25" cy="82.25" r="1.25" fill="white"/><circle cx="19.25" cy="82.25" r="1.25" fill="white"/><circle cx="28.25" cy="82.25" r="1.25" fill="white"/><circle cx="37.25" cy="82.25" r="1.25" fill="white"/><circle cx="46.25" cy="82.25" r="1.25" fill="white"/><circle cx="55.25" cy="82.25" r="1.25" fill="white"/><circle cx="64.25" cy="82.25" r="1.25" fill="white"/><circle cx="73.25" cy="82.25" r="1.25" fill="white"/><circle cx="82.25" cy="82.25" r="1.25" fill="white"/><circle cx="91.25" cy="82.25" r="1.25" fill="white"/><circle cx="1.5" cy="91.5" r="1.5" fill="white"/>
|
||||||
|
<circle cx="10.5" cy="91.5" r="1.5" fill="white"/><circle cx="19.5" cy="91.5" r="1.5" fill="white"/><circle cx="28.5" cy="91.5" r="1.5" fill="white"/><circle cx="37.5" cy="91.5" r="1.5" fill="white"/><circle cx="46.5" cy="91.5" r="1.5" fill="white"/><circle cx="55.5" cy="91.5" r="1.5" fill="white"/><circle cx="64.5" cy="91.5" r="1.5" fill="white"/><circle cx="73.5" cy="91.5" r="1.5" fill="white"/><circle cx="82.5" cy="91.5" r="1.5" fill="white"/><circle cx="91.5" cy="91.5" r="1.5" fill="white"/>
|
||||||
|
</svg>""", mimetype="image/svg+xml")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("[INFO] Server started")
|
||||||
|
serve(app, host=HOST, port=PORT)
|
||||||
|
print("[INFO] Server stopped")
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Define the old and new URLs
|
||||||
|
OLD_URL="https://notes.canary.hectabit.org/api"
|
||||||
|
NEW_URL="https://notes.canary.hectabit.org/api"
|
||||||
|
|
||||||
|
# Recursively search and replace in files under the current directory
|
||||||
|
find . -type f -exec sed -i "s|$OLD_URL|$NEW_URL|g" {} +
|
58
go.mod
58
go.mod
|
@ -1,58 +0,0 @@
|
||||||
module hectabit.org/burgernotes
|
|
||||||
|
|
||||||
go 1.22
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/catalinc/hashcash v1.0.0
|
|
||||||
github.com/gin-contrib/sessions v1.0.1
|
|
||||||
github.com/gin-gonic/gin v1.10.0
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.22
|
|
||||||
github.com/spf13/viper v1.19.0
|
|
||||||
golang.org/x/crypto v0.23.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/bytedance/sonic v1.11.6 // indirect
|
|
||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
|
||||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
|
||||||
github.com/gorilla/context v1.1.2 // indirect
|
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
|
||||||
github.com/gorilla/sessions v1.2.2 // indirect
|
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
|
||||||
github.com/spf13/afero v1.11.0 // indirect
|
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
|
||||||
golang.org/x/net v0.25.0 // indirect
|
|
||||||
golang.org/x/sys v0.21.0 // indirect
|
|
||||||
golang.org/x/text v0.16.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
|
144
go.sum
144
go.sum
|
@ -1,144 +0,0 @@
|
||||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
|
||||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
|
||||||
github.com/catalinc/hashcash v1.0.0 h1:DiI2kBNCczy7y3xJnLddIl7KGx0yP4B7irFZZ+yzzwc=
|
|
||||||
github.com/catalinc/hashcash v1.0.0/go.mod h1:ldWL6buwYCK4VqIkLbZuFbGUoJceSafm8duCEQYw9Jw=
|
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
|
||||||
github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI=
|
|
||||||
github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM=
|
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
|
||||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
|
||||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
|
||||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
|
||||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
|
||||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
|
||||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
|
||||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
|
||||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
|
||||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
|
||||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
|
||||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
|
||||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
|
||||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
|
||||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
|
||||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
|
||||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
|
||||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
|
||||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
|
||||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
def generatedb():
|
||||||
|
connection = sqlite3.connect("database.db")
|
||||||
|
|
||||||
|
with open("schema.sql") as f:
|
||||||
|
connection.executescript(f.read())
|
||||||
|
|
||||||
|
cur = connection.cursor()
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
print("[INFO] Generated database")
|
||||||
|
|
||||||
|
if not os.path.exists("database.db"):
|
||||||
|
generatedb()
|
||||||
|
else:
|
||||||
|
answer = input("Proceeding will overwrite the database. Proceed? (y/N)")
|
||||||
|
if "y" in answer.lower():
|
||||||
|
generatedb()
|
||||||
|
elif "n" in answer.lower():
|
||||||
|
print("Stopped")
|
||||||
|
elif ":3" in answer:
|
||||||
|
print(":3")
|
||||||
|
else:
|
||||||
|
print("Stopped")
|
|
@ -0,0 +1,546 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
import secrets
|
||||||
|
import configparser
|
||||||
|
import asyncio
|
||||||
|
from hypercorn.config import Config
|
||||||
|
from hypercorn.asyncio import serve
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from quart import Quart, request, url_for, flash, redirect, session, make_response, send_from_directory, stream_with_context, Response, request
|
||||||
|
|
||||||
|
# Parse configuration file, and check if anything is wrong with it
|
||||||
|
if not os.path.exists("config.ini"):
|
||||||
|
print("config.ini does not exist")
|
||||||
|
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read("config.ini")
|
||||||
|
|
||||||
|
HOST = config["config"]["HOST"]
|
||||||
|
PORT = config["config"]["PORT"]
|
||||||
|
SECRET_KEY = config["config"]["SECRET_KEY"]
|
||||||
|
MAX_STORAGE = config["config"]["MAX_STORAGE"]
|
||||||
|
|
||||||
|
if SECRET_KEY == "supersecretkey" or SECRET_KEY == "placeholder":
|
||||||
|
print("[WARNING] Secret key not set")
|
||||||
|
|
||||||
|
# Define Quart
|
||||||
|
app = Quart(__name__)
|
||||||
|
app.config["SECRET_KEY"] = SECRET_KEY
|
||||||
|
|
||||||
|
# Database functions
|
||||||
|
def get_db_connection():
|
||||||
|
conn = sqlite3.connect("database.db")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def get_user(id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
post = conn.execute("SELECT * FROM users WHERE id = ?",
|
||||||
|
(id,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if post is None:
|
||||||
|
return None
|
||||||
|
return post
|
||||||
|
|
||||||
|
def get_note(id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
post = conn.execute("SELECT * FROM notes WHERE id = ?",
|
||||||
|
(id,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if post is None:
|
||||||
|
return None
|
||||||
|
return post
|
||||||
|
|
||||||
|
def get_space(id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
notes = conn.execute("SELECT content, title FROM notes WHERE creator = ? ORDER BY id DESC;", (id,)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
spacetaken = 0
|
||||||
|
for x in notes:
|
||||||
|
spacetaken = spacetaken + len(x["content"].encode("utf-8"))
|
||||||
|
spacetaken = spacetaken + len(x["title"].encode("utf-8"))
|
||||||
|
return spacetaken
|
||||||
|
|
||||||
|
def get_note_count(id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
notes = conn.execute("SELECT content, title FROM notes WHERE creator = ? ORDER BY id DESC;", (id,)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
notecount = 0
|
||||||
|
for x in notes:
|
||||||
|
notecount = notecount + 1
|
||||||
|
return notecount
|
||||||
|
|
||||||
|
def get_session(id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
post = conn.execute("SELECT * FROM sessions WHERE session = ?",
|
||||||
|
(id,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if post is None:
|
||||||
|
return None
|
||||||
|
return post
|
||||||
|
|
||||||
|
def get_session_from_sessionid(id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
post = conn.execute("SELECT * FROM sessions WHERE sessionid = ?",
|
||||||
|
(id,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if post is None:
|
||||||
|
return None
|
||||||
|
return post
|
||||||
|
|
||||||
|
def check_username_taken(username):
|
||||||
|
conn = get_db_connection()
|
||||||
|
post = conn.execute("SELECT * FROM users WHERE lower(username) = ?",
|
||||||
|
(username.lower(),)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if post is None:
|
||||||
|
return None
|
||||||
|
return post["id"]
|
||||||
|
|
||||||
|
# Disable CORS
|
||||||
|
@app.after_request
|
||||||
|
async def add_cors_headers(response):
|
||||||
|
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||||
|
response.headers.add("Access-Control-Allow-Headers", "*")
|
||||||
|
response.headers.add("Access-Control-Allow-Methods", "*")
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Live editing store
|
||||||
|
messages = {}
|
||||||
|
|
||||||
|
@app.route("/api/version", methods=("GET", "POST"))
|
||||||
|
async def apiversion():
|
||||||
|
return "Burgernotes Version 1.2"
|
||||||
|
|
||||||
|
@app.route("/api/signup", methods=("GET", "POST"))
|
||||||
|
async def apisignup():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
username = data["username"]
|
||||||
|
password = data["password"]
|
||||||
|
|
||||||
|
if username == "":
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
if len(username) > 20:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
if not username.isalnum():
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
if password == "":
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
if len(password) < 14:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
if not check_username_taken(username) == None:
|
||||||
|
return {}, 409
|
||||||
|
|
||||||
|
hashedpassword = generate_password_hash(password)
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("INSERT INTO users (username, password, created) VALUES (?, ?, ?)",
|
||||||
|
(username, hashedpassword, str(time.time())))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
userID = check_username_taken(username)
|
||||||
|
user = get_user(userID)
|
||||||
|
|
||||||
|
randomCharacters = secrets.token_hex(512)
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)",
|
||||||
|
(randomCharacters, userID, request.headers.get("user-agent")))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"key": randomCharacters
|
||||||
|
}, 200
|
||||||
|
|
||||||
|
@app.route("/api/login", methods=("GET", "POST"))
|
||||||
|
async def apilogin():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
username = data["username"]
|
||||||
|
password = data["password"]
|
||||||
|
passwordchange = data["passwordchange"]
|
||||||
|
newpass = data["newpass"]
|
||||||
|
|
||||||
|
check_username_thing = check_username_taken(username)
|
||||||
|
|
||||||
|
if check_username_thing == None:
|
||||||
|
return {}, 401
|
||||||
|
|
||||||
|
userID = check_username_taken(username)
|
||||||
|
user = get_user(userID)
|
||||||
|
|
||||||
|
if not check_password_hash(user["password"], (password)):
|
||||||
|
return {}, 401
|
||||||
|
|
||||||
|
randomCharacters = secrets.token_hex(512)
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)",
|
||||||
|
(randomCharacters, userID, request.headers.get("user-agent")))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if passwordchange == "yes":
|
||||||
|
hashedpassword = generate_password_hash(newpass)
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("UPDATE users SET password = ? WHERE username = ?", (hashedpassword, username))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"key": randomCharacters,
|
||||||
|
}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/userinfo", methods=("GET", "POST"))
|
||||||
|
async def apiuserinfo():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
datatemplate = {
|
||||||
|
"username": user["username"],
|
||||||
|
"id": user["id"],
|
||||||
|
"created": user["created"],
|
||||||
|
"storageused": get_space(user["id"]),
|
||||||
|
"storagemax": MAX_STORAGE,
|
||||||
|
"notecount": get_note_count(user["id"])
|
||||||
|
}
|
||||||
|
return datatemplate
|
||||||
|
|
||||||
|
@app.route("/api/listnotes", methods=("GET", "POST"))
|
||||||
|
async def apilistnotes():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
notes = conn.execute("SELECT * FROM notes WHERE creator = ? ORDER BY edited DESC;", (user["id"],)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
datatemplate = []
|
||||||
|
|
||||||
|
for note in notes:
|
||||||
|
notetemplate = {
|
||||||
|
"id": note["id"],
|
||||||
|
"title": note["title"]
|
||||||
|
}
|
||||||
|
datatemplate.append(notetemplate)
|
||||||
|
|
||||||
|
return datatemplate, 200
|
||||||
|
|
||||||
|
@app.route("/api/exportnotes", methods=("GET", "POST"))
|
||||||
|
async def apiexportnotes():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
notes = conn.execute("SELECT * FROM notes WHERE creator = ? ORDER BY id DESC;", (user["id"],)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
datatemplate = []
|
||||||
|
|
||||||
|
for note in notes:
|
||||||
|
notetemplate = {
|
||||||
|
"id": note["id"],
|
||||||
|
"created": note["created"],
|
||||||
|
"edited": note["edited"],
|
||||||
|
"title": note["title"],
|
||||||
|
"content": note["content"]
|
||||||
|
}
|
||||||
|
datatemplate.append(notetemplate)
|
||||||
|
|
||||||
|
return datatemplate, 200
|
||||||
|
|
||||||
|
@app.route("/api/newnote", methods=("GET", "POST"))
|
||||||
|
async def apinewnote():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
noteName = data["noteName"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("INSERT INTO notes (title, content, creator, created, edited) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
(noteName, "", user["id"], str(time.time()), str(time.time())))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {}, 200
|
||||||
|
|
||||||
|
@app.route("/api/readnote", methods=("GET", "POST"))
|
||||||
|
async def apireadnote():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
noteId = data["noteId"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
note = get_note(noteId)
|
||||||
|
|
||||||
|
if (note != None):
|
||||||
|
if (user["id"] == note["creator"]):
|
||||||
|
contenttemplate = {
|
||||||
|
"content": note["content"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return contenttemplate, 200
|
||||||
|
else:
|
||||||
|
return {}, 422
|
||||||
|
else:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
@app.route("/api/waitforedit", methods=("GET", "POST"))
|
||||||
|
async def waitforedit():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
complete = true
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
while user["id"] not in messages or not messages[user["id"]]:
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
if elapsed_time >= 20:
|
||||||
|
break
|
||||||
|
complete = false
|
||||||
|
|
||||||
|
message = messages[user["id"]].pop(0)
|
||||||
|
del messages[user["id"]]
|
||||||
|
|
||||||
|
if complete == true:
|
||||||
|
return {
|
||||||
|
"note": message
|
||||||
|
}, 200
|
||||||
|
else:
|
||||||
|
return 400
|
||||||
|
|
||||||
|
@app.route("/api/editnote", methods=("GET", "POST"))
|
||||||
|
async def apieditnote():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
noteId = data["noteId"]
|
||||||
|
content = data["content"]
|
||||||
|
title = data["title"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
note = get_note(noteId)
|
||||||
|
|
||||||
|
if get_space(user["id"]) + len(content.encode("utf-8")) > int(MAX_STORAGE):
|
||||||
|
return {}, 418
|
||||||
|
|
||||||
|
if (note != None):
|
||||||
|
if (user["id"] == note["creator"]):
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("UPDATE notes SET content = ?, title = ?, edited = ? WHERE id = ?", (content, title, str(time.time()), noteId))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if user["id"] not in messages:
|
||||||
|
messages[user["id"]] = []
|
||||||
|
messages[user["id"]].append(noteId)
|
||||||
|
|
||||||
|
return {}, 200
|
||||||
|
else:
|
||||||
|
return {}, 403
|
||||||
|
else:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/editnotetitle", methods=("GET", "POST"))
|
||||||
|
async def apieditnotetitle():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
noteId = data["noteId"]
|
||||||
|
content = data["content"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
note = get_note(noteId)
|
||||||
|
|
||||||
|
if get_space(user["id"]) + len(content.encode("utf-8")) > int(MAX_STORAGE):
|
||||||
|
return {}, 418
|
||||||
|
|
||||||
|
if (note != None):
|
||||||
|
if (user["id"] == note["creator"]):
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("UPDATE notes SET title = ?, edited = ? WHERE id = ?", (content, str(time.time()), noteId))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {}, 200
|
||||||
|
else:
|
||||||
|
return {}, 403
|
||||||
|
else:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/removenote", methods=("GET", "POST"))
|
||||||
|
async def apiremovenote():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
noteId = data["noteId"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
note = get_note(noteId)
|
||||||
|
|
||||||
|
if (note != None):
|
||||||
|
if (user["id"] == note["creator"]):
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("DELETE FROM notes WHERE id = ?", (noteId,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {}, 200
|
||||||
|
else:
|
||||||
|
return {}, 403
|
||||||
|
else:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/deleteaccount", methods=("GET", "POST"))
|
||||||
|
async def apideleteaccount():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("DELETE FROM notes WHERE creator = ?", (userCookie["id"],))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("DELETE FROM users WHERE id = ?", (userCookie["id"],))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {}, 200
|
||||||
|
|
||||||
|
@app.route("/api/sessions/list", methods=("GET", "POST"))
|
||||||
|
async def apisessionslist():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
sessions = conn.execute("SELECT * FROM sessions WHERE id = ? ORDER BY id DESC;", (user["id"],)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
datatemplate = []
|
||||||
|
|
||||||
|
for x in sessions:
|
||||||
|
device = x["device"]
|
||||||
|
thisSession = False
|
||||||
|
if (x["session"] == secretKey):
|
||||||
|
thisSession = True
|
||||||
|
sessiontemplate = {
|
||||||
|
"id": x["sessionid"],
|
||||||
|
"thisSession": thisSession,
|
||||||
|
"device": device
|
||||||
|
}
|
||||||
|
datatemplate.append(sessiontemplate)
|
||||||
|
|
||||||
|
return datatemplate, 200
|
||||||
|
|
||||||
|
@app.route("/api/sessions/remove", methods=("GET", "POST"))
|
||||||
|
async def apisessionsremove():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
sessionId = data["sessionId"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
session = get_session_from_sessionid(sessionId)
|
||||||
|
|
||||||
|
if (session != None):
|
||||||
|
if (user["id"] == session["id"]):
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("DELETE FROM sessions WHERE sessionid = ?", (session["sessionid"],))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {}, 200
|
||||||
|
else:
|
||||||
|
return {}, 403
|
||||||
|
else:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/listusers", methods=("GET", "POST"))
|
||||||
|
async def listusers():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
masterkey = data["masterkey"]
|
||||||
|
if masterkey == SECRET_KEY:
|
||||||
|
conn = get_db_connection()
|
||||||
|
users = conn.execute("SELECT * FROM users").fetchall()
|
||||||
|
conn.close()
|
||||||
|
thing = []
|
||||||
|
for x in users:
|
||||||
|
user_info = {
|
||||||
|
"id": str(x["id"]),
|
||||||
|
"username": str(x["username"]),
|
||||||
|
"space": str(get_space(x["id"]))
|
||||||
|
}
|
||||||
|
thing.append(user_info)
|
||||||
|
return thing, 200
|
||||||
|
else:
|
||||||
|
return {}, 401
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
async def burger(e):
|
||||||
|
return {}, 500
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
async def burger(e):
|
||||||
|
return {}, 404
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
hypercornconfig = Config()
|
||||||
|
hypercornconfig.bind = (HOST + ":" + PORT)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("[INFO] Server started")
|
||||||
|
asyncio.run(serve(app, hypercornconfig))
|
||||||
|
print("[INFO] Server stopped")
|
|
@ -0,0 +1,539 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
import secrets
|
||||||
|
import configparser
|
||||||
|
import asyncio
|
||||||
|
from hypercorn.config import Config
|
||||||
|
from hypercorn.asyncio import serve
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from quart import Quart, request, url_for, flash, redirect, session, make_response, send_from_directory, stream_with_context, Response, request
|
||||||
|
|
||||||
|
# Parse configuration file, and check if anything is wrong with it
|
||||||
|
if not os.path.exists("config.ini"):
|
||||||
|
print("config.ini does not exist")
|
||||||
|
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read("config.ini")
|
||||||
|
|
||||||
|
HOST = config["config"]["HOST"]
|
||||||
|
PORT = config["config"]["PORT"]
|
||||||
|
SECRET_KEY = config["config"]["SECRET_KEY"]
|
||||||
|
MAX_STORAGE = config["config"]["MAX_STORAGE"]
|
||||||
|
|
||||||
|
if SECRET_KEY == "supersecretkey" or SECRET_KEY == "placeholder":
|
||||||
|
print("[WARNING] Secret key not set")
|
||||||
|
|
||||||
|
# Define Quart
|
||||||
|
app = Quart(__name__)
|
||||||
|
app.config["SECRET_KEY"] = SECRET_KEY
|
||||||
|
|
||||||
|
# Database functions
|
||||||
|
def get_db_connection():
|
||||||
|
conn = sqlite3.connect("database.db")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def get_user(id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
post = conn.execute("SELECT * FROM users WHERE id = ?",
|
||||||
|
(id,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if post is None:
|
||||||
|
return None
|
||||||
|
return post
|
||||||
|
|
||||||
|
def get_note(id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
post = conn.execute("SELECT * FROM notes WHERE id = ?",
|
||||||
|
(id,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if post is None:
|
||||||
|
return None
|
||||||
|
return post
|
||||||
|
|
||||||
|
def get_space(id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
notes = conn.execute("SELECT content, title FROM notes WHERE creator = ? ORDER BY id DESC;", (id,)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
spacetaken = 0
|
||||||
|
for x in notes:
|
||||||
|
spacetaken = spacetaken + len(x["content"].encode("utf-8"))
|
||||||
|
spacetaken = spacetaken + len(x["title"].encode("utf-8"))
|
||||||
|
return spacetaken
|
||||||
|
|
||||||
|
def get_note_count(id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
notes = conn.execute("SELECT content, title FROM notes WHERE creator = ? ORDER BY id DESC;", (id,)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
notecount = 0
|
||||||
|
for x in notes:
|
||||||
|
notecount = notecount + 1
|
||||||
|
return notecount
|
||||||
|
|
||||||
|
def get_session(id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
post = conn.execute("SELECT * FROM sessions WHERE session = ?",
|
||||||
|
(id,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if post is None:
|
||||||
|
return None
|
||||||
|
return post
|
||||||
|
|
||||||
|
def get_session_from_sessionid(id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
post = conn.execute("SELECT * FROM sessions WHERE sessionid = ?",
|
||||||
|
(id,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if post is None:
|
||||||
|
return None
|
||||||
|
return post
|
||||||
|
|
||||||
|
def check_username_taken(username):
|
||||||
|
conn = get_db_connection()
|
||||||
|
post = conn.execute("SELECT * FROM users WHERE lower(username) = ?",
|
||||||
|
(username.lower(),)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if post is None:
|
||||||
|
return None
|
||||||
|
return post["id"]
|
||||||
|
|
||||||
|
# Disable CORS
|
||||||
|
@app.after_request
|
||||||
|
async def add_cors_headers(response):
|
||||||
|
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||||
|
response.headers.add("Access-Control-Allow-Headers", "*")
|
||||||
|
response.headers.add("Access-Control-Allow-Methods", "*")
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Live editing store
|
||||||
|
messages = {}
|
||||||
|
|
||||||
|
@app.route("/api/version", methods=("GET", "POST"))
|
||||||
|
async def apiversion():
|
||||||
|
return "Burgernotes Version 1.2"
|
||||||
|
|
||||||
|
@app.route("/api/signup", methods=("GET", "POST"))
|
||||||
|
async def apisignup():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
username = data["username"]
|
||||||
|
password = data["password"]
|
||||||
|
|
||||||
|
if username == "":
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
if len(username) > 20:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
if not username.isalnum():
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
if password == "":
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
if len(password) < 14:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
if not check_username_taken(username) == None:
|
||||||
|
return {}, 409
|
||||||
|
|
||||||
|
hashedpassword = generate_password_hash(password)
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("INSERT INTO users (username, password, created) VALUES (?, ?, ?)",
|
||||||
|
(username, hashedpassword, str(time.time())))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
userID = check_username_taken(username)
|
||||||
|
user = get_user(userID)
|
||||||
|
|
||||||
|
randomCharacters = secrets.token_hex(512)
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)",
|
||||||
|
(randomCharacters, userID, request.headers.get("user-agent")))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"key": randomCharacters
|
||||||
|
}, 200
|
||||||
|
|
||||||
|
@app.route("/api/login", methods=("GET", "POST"))
|
||||||
|
async def apilogin():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
username = data["username"]
|
||||||
|
password = data["password"]
|
||||||
|
passwordchange = data["passwordchange"]
|
||||||
|
newpass = data["newpass"]
|
||||||
|
|
||||||
|
check_username_thing = check_username_taken(username)
|
||||||
|
|
||||||
|
if check_username_thing == None:
|
||||||
|
return {}, 401
|
||||||
|
|
||||||
|
userID = check_username_taken(username)
|
||||||
|
user = get_user(userID)
|
||||||
|
|
||||||
|
if not check_password_hash(user["password"], (password)):
|
||||||
|
return {}, 401
|
||||||
|
|
||||||
|
randomCharacters = secrets.token_hex(512)
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)",
|
||||||
|
(randomCharacters, userID, request.headers.get("user-agent")))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if passwordchange == "yes":
|
||||||
|
hashedpassword = generate_password_hash(newpass)
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("UPDATE users SET password = ? WHERE username = ?", (hashedpassword, username))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"key": randomCharacters,
|
||||||
|
}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/userinfo", methods=("GET", "POST"))
|
||||||
|
async def apiuserinfo():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
datatemplate = {
|
||||||
|
"username": user["username"],
|
||||||
|
"id": user["id"],
|
||||||
|
"created": user["created"],
|
||||||
|
"storageused": get_space(user["id"]),
|
||||||
|
"storagemax": MAX_STORAGE,
|
||||||
|
"notecount": get_note_count(user["id"])
|
||||||
|
}
|
||||||
|
return datatemplate
|
||||||
|
|
||||||
|
@app.route("/api/listnotes", methods=("GET", "POST"))
|
||||||
|
async def apilistnotes():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
notes = conn.execute("SELECT * FROM notes WHERE creator = ? ORDER BY edited DESC;", (user["id"],)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
datatemplate = []
|
||||||
|
|
||||||
|
for note in notes:
|
||||||
|
notetemplate = {
|
||||||
|
"id": note["id"],
|
||||||
|
"title": note["title"]
|
||||||
|
}
|
||||||
|
datatemplate.append(notetemplate)
|
||||||
|
|
||||||
|
return datatemplate, 200
|
||||||
|
|
||||||
|
@app.route("/api/exportnotes", methods=("GET", "POST"))
|
||||||
|
async def apiexportnotes():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
notes = conn.execute("SELECT * FROM notes WHERE creator = ? ORDER BY id DESC;", (user["id"],)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
datatemplate = []
|
||||||
|
|
||||||
|
for note in notes:
|
||||||
|
notetemplate = {
|
||||||
|
"id": note["id"],
|
||||||
|
"created": note["created"],
|
||||||
|
"edited": note["edited"],
|
||||||
|
"title": note["title"],
|
||||||
|
"content": note["content"]
|
||||||
|
}
|
||||||
|
datatemplate.append(notetemplate)
|
||||||
|
|
||||||
|
return datatemplate, 200
|
||||||
|
|
||||||
|
@app.route("/api/newnote", methods=("GET", "POST"))
|
||||||
|
async def apinewnote():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
noteName = data["noteName"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("INSERT INTO notes (title, content, creator, created, edited) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
(noteName, "", user["id"], str(time.time()), str(time.time())))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {}, 200
|
||||||
|
|
||||||
|
@app.route("/api/readnote", methods=("GET", "POST"))
|
||||||
|
async def apireadnote():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
noteId = data["noteId"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
note = get_note(noteId)
|
||||||
|
|
||||||
|
if (note != None):
|
||||||
|
if (user["id"] == note["creator"]):
|
||||||
|
contenttemplate = {
|
||||||
|
"content": note["content"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return contenttemplate, 200
|
||||||
|
else:
|
||||||
|
return {}, 422
|
||||||
|
else:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
@app.route("/api/waitforedit", methods=("GET", "POST"))
|
||||||
|
async def waitforedit():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
complete = true
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
while user["id"] not in messages or not messages[user["id"]]:
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
if elapsed_time >= 20:
|
||||||
|
break
|
||||||
|
complete = false
|
||||||
|
|
||||||
|
message = messages[user["id"]].pop(0)
|
||||||
|
del messages[user["id"]]
|
||||||
|
|
||||||
|
if complete == true:
|
||||||
|
return {
|
||||||
|
"note": message
|
||||||
|
}, 200
|
||||||
|
else:
|
||||||
|
return 400
|
||||||
|
|
||||||
|
@app.route("/api/editnote", methods=("GET", "POST"))
|
||||||
|
async def apieditnote():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
noteId = data["noteId"]
|
||||||
|
content = data["content"]
|
||||||
|
title = data["title"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
note = get_note(noteId)
|
||||||
|
|
||||||
|
if get_space(user["id"]) + len(content.encode("utf-8")) > int(MAX_STORAGE):
|
||||||
|
return {}, 418
|
||||||
|
|
||||||
|
if (note != None):
|
||||||
|
if (user["id"] == note["creator"]):
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("UPDATE notes SET content = ?, title = ?, edited = ? WHERE id = ?", (content, title, str(time.time()), noteId))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if user["id"] not in messages:
|
||||||
|
messages[user["id"]] = []
|
||||||
|
messages[user["id"]].append(noteId)
|
||||||
|
|
||||||
|
return {}, 200
|
||||||
|
else:
|
||||||
|
return {}, 403
|
||||||
|
else:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/editnotetitle", methods=("GET", "POST"))
|
||||||
|
async def apieditnotetitle():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
noteId = data["noteId"]
|
||||||
|
content = data["content"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
note = get_note(noteId)
|
||||||
|
|
||||||
|
if get_space(user["id"]) + len(content.encode("utf-8")) > int(MAX_STORAGE):
|
||||||
|
return {}, 418
|
||||||
|
|
||||||
|
if (note != None):
|
||||||
|
if (user["id"] == note["creator"]):
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("UPDATE notes SET title = ?, edited = ? WHERE id = ?", (content, str(time.time()), noteId))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {}, 200
|
||||||
|
else:
|
||||||
|
return {}, 403
|
||||||
|
else:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/removenote", methods=("GET", "POST"))
|
||||||
|
async def apiremovenote():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
noteId = data["noteId"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
note = get_note(noteId)
|
||||||
|
|
||||||
|
if (note != None):
|
||||||
|
if (user["id"] == note["creator"]):
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("DELETE FROM notes WHERE id = ?", (noteId,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {}, 200
|
||||||
|
else:
|
||||||
|
return {}, 403
|
||||||
|
else:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/deleteaccount", methods=("GET", "POST"))
|
||||||
|
async def apideleteaccount():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("DELETE FROM notes WHERE creator = ?", (userCookie["id"],))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("DELETE FROM users WHERE id = ?", (userCookie["id"],))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {}, 200
|
||||||
|
|
||||||
|
@app.route("/api/sessions/list", methods=("GET", "POST"))
|
||||||
|
async def apisessionslist():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
sessions = conn.execute("SELECT * FROM sessions WHERE id = ? ORDER BY id DESC;", (user["id"],)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
datatemplate = []
|
||||||
|
|
||||||
|
for x in sessions:
|
||||||
|
device = x["device"]
|
||||||
|
thisSession = False
|
||||||
|
if (x["session"] == secretKey):
|
||||||
|
thisSession = True
|
||||||
|
sessiontemplate = {
|
||||||
|
"id": x["sessionid"],
|
||||||
|
"thisSession": thisSession,
|
||||||
|
"device": device
|
||||||
|
}
|
||||||
|
datatemplate.append(sessiontemplate)
|
||||||
|
|
||||||
|
return datatemplate, 200
|
||||||
|
|
||||||
|
@app.route("/api/sessions/remove", methods=("GET", "POST"))
|
||||||
|
async def apisessionsremove():
|
||||||
|
if request.method == "POST":
|
||||||
|
data = await request.get_json()
|
||||||
|
secretKey = data["secretKey"]
|
||||||
|
sessionId = data["sessionId"]
|
||||||
|
|
||||||
|
userCookie = get_session(secretKey)
|
||||||
|
user = get_user(userCookie["id"])
|
||||||
|
|
||||||
|
session = get_session_from_sessionid(sessionId)
|
||||||
|
|
||||||
|
if (session != None):
|
||||||
|
if (user["id"] == session["id"]):
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("DELETE FROM sessions WHERE sessionid = ?", (session["sessionid"],))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {}, 200
|
||||||
|
else:
|
||||||
|
return {}, 403
|
||||||
|
else:
|
||||||
|
return {}, 422
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/listusers/<secretkey>", methods=("GET", "POST"))
|
||||||
|
def listusers(secretkey):
|
||||||
|
if secretkey == SECRET_KEY:
|
||||||
|
conn = get_db_connection()
|
||||||
|
users = conn.execute("SELECT * FROM users").fetchall()
|
||||||
|
conn.close()
|
||||||
|
thing = ""
|
||||||
|
for x in users:
|
||||||
|
thing = str(x["id"]) + " - " + x["username"] + " - " + str(get_space(x["id"])) + "<br>" + thing
|
||||||
|
|
||||||
|
return thing
|
||||||
|
else:
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
async def burger(e):
|
||||||
|
return {}, 500
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
async def burger(e):
|
||||||
|
return {}, 404
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
hypercornconfig = Config()
|
||||||
|
hypercornconfig.bind = (HOST + ":" + PORT)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("[INFO] Server started")
|
||||||
|
asyncio.run(serve(app, hypercornconfig))
|
||||||
|
print("[INFO] Server stopped")
|
|
@ -0,0 +1,31 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
print("type n to cancel")
|
||||||
|
answer = input("delete user, what is user id?")
|
||||||
|
|
||||||
|
if (answer == "n"):
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
def get_db_connection():
|
||||||
|
conn = sqlite3.connect("database.db")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
print("deleting notes")
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
notes = conn.execute("DELETE FROM notes WHERE creator = ?", (int(answer),))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print("deleting account")
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("DELETE FROM users WHERE id = ?", (int(answer),))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print("success")
|
|
@ -0,0 +1,3 @@
|
||||||
|
quart
|
||||||
|
hypercorn
|
||||||
|
werkzeug
|
15
schema.sql
15
schema.sql
|
@ -1,13 +1,12 @@
|
||||||
DROP TABLE IF EXISTS users;
|
DROP TABLE IF EXISTS users;
|
||||||
DROP TABLE IF EXISTS notes;
|
DROP TABLE IF EXISTS notes;
|
||||||
DROP TABLE IF EXISTS oauth;
|
DROP TABLE IF EXISTS sessions;
|
||||||
|
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
created TEXT NOT NULL,
|
created TEXT NOT NULL,
|
||||||
username TEXT NOT NULL,
|
username TEXT NOT NULL,
|
||||||
password TEXT NOT NULL,
|
password TEXT NOT NULL
|
||||||
migrated INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE notes (
|
CREATE TABLE notes (
|
||||||
|
@ -19,9 +18,9 @@ CREATE TABLE notes (
|
||||||
title TEXT NOT NULL
|
title TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE oauth (
|
CREATE TABLE sessions (
|
||||||
|
sessionid INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
session TEXT NOT NULL,
|
||||||
id INTEGER NOT NULL,
|
id INTEGER NOT NULL,
|
||||||
oauthProvider TEXT NOT NULL,
|
device TEXT NOT NULL DEFAULT "?"
|
||||||
encryptedPasswd TEXT NOT NULL,
|
);
|
||||||
UNIQUE (id, oauthProvider)
|
|
||||||
)
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
def get_db_connection():
|
||||||
|
conn = sqlite3.connect("database.db")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
if os.path.exists("database.db"):
|
||||||
|
print("vacuuming..")
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.execute("VACUUM")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print("success")
|
||||||
|
else:
|
||||||
|
print("database not found")
|
Loading…
Reference in New Issue