From 3db8328cacdf2131ff206d353939ca432d136b8f Mon Sep 17 00:00:00 2001 From: arzumify Date: Sun, 20 Oct 2024 10:45:03 +0100 Subject: [PATCH] It's time to rewrite burgernotes!! Again!!!!!!! --- .gitignore | 8 +- APIDOCS.md | 384 +++++--- ERRORS.md | 23 - README.md | 27 +- ROADMAP.md | 7 +- build.sh | 7 + config.ini.example | 5 - generate.go | 1 + go.mod | 59 +- go.sum | 160 +--- main.go | 2152 +++++++++++++++----------------------------- main.proto | 142 +++ schema.sql | 27 - 13 files changed, 1206 insertions(+), 1796 deletions(-) delete mode 100644 ERRORS.md create mode 100755 build.sh delete mode 100644 config.ini.example create mode 100644 generate.go create mode 100644 main.proto delete mode 100644 schema.sql diff --git a/.gitignore b/.gitignore index 715e166..35d1822 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -database.db -config.ini -config.ini.example -.idea \ No newline at end of file +# IntelliJ IDEA +.idea +# Protocol Buffers +git.ailur.dev \ No newline at end of file diff --git a/APIDOCS.md b/APIDOCS.md index 8882753..52ec172 100644 --- a/APIDOCS.md +++ b/APIDOCS.md @@ -1,135 +1,313 @@ -# 🍔 Burgernotes API docs -Use the Burgernotes API to automate tasks, build your own client, and more! +# API documentation -Headers should be: "Content-type: application/json; charset=UTF-8" for all POSTs +The Burgernotes API is a RESTful API that allows you to interact with the Burgernotes service. The API is designed to be simple and easy to use. +It uses Protocol Buffers for serialization and deserialization, and POST requests for all operations. -## 🔑 Authentication +## Protobuf types +These are some basic Protobuf types that are used in the API. +All protocol buffers are using proto3 syntax. +```protobuf +syntax = "proto3"; -POST - /api/signup - provide "username" and "password". +// Token is a string that represents an OAuth2 JWT token. +message Token { + string token = 1; +} -POST - /api/login - provide "username" and "password" +// NoteID is a UUID that represents a note. +message NoteID { + bytes noteId = 1; +} -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 +// NoteID and Token together represent a request involving a note. +message NoteRequest { + NoteID noteId = 1; + Token token = 2; +} -Password should be at least 8 characters, username must be under 20 characters and alphanumeric. +// AESData represents AES-encrypted data. +message AESData { + bytes data = 2; + bytes iv = 3; +} -If username is taken, error code 422 will return. +// NoteMetadata represents the metadata of a note. +message NoteMetadata { + NoteID noteId = 1; + AESData title = 2; +} -Assuming everything went correctly, the server will return a secret key. +// Note represents a note. +message Note { + NoteMetadata metadata = 1; + AESData note = 2; +} -You'll need to store two things in local storage: -- 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). +// Invitation represents an invitation to a note. +message Invitation { + string username = 1; + AESData key = 2; + NoteID noteId = 3; +} -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. +// User represents a user editing notes. +message UserLines { + string username = 1; + bytes uuid = 2; + repeated uint64 lines = 3; +} -## 🌐 OAuth2 + Burgerauth +// Error represents an error. +message Error { + string error = 1; +} -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)" +// ServerError represents a 500 error, with a hex error code. +message ServerError { + bytes errorCode = 1; } ``` -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)" +## Errors +In any response, if an error occurs, it will return an `Error` or `ServerError` message. +### 400 Range +```protobuf +message Error { + string error = 1; } ``` -5. Finally, convert the JSON into a string, base64 encode it, and send it off as encryptedPassword. +The error is formatted to be human-readable, you may display it to the user. +### HTTP 500 +```protobuf +message ServerError { + bytes errorCode = 1; +} +``` +ServerError is a hex byte which represents the error code. You should give a vague error message to the user. -Do not store the exchangeKey after this point, as it is no longer needed. +## Authentication +### /api/signup - POST +#### Request +```protobuf +message ApiSignupRequest { + bytes publicKey = 1; + Token token = 2; +} +``` +#### Response +200 OK +No response body -### 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. +### /api/delete - POST - Show a warning before this action! +#### Request +```protobuf +message Token { + string token = 1; +} +``` +#### Response +HTTP 200 OK +No response body -Use the passwords defined in the JSON structure before the last one to log in normally. +## Interacting with notes +### /api/notes/add - POST +#### Request +```protobuf +message Token { + string token = 1; +} +``` +#### Response +HTTP 200 OK +```protobuf +message NoteID { + bytes noteId = 1; +} +``` -#### Finally, you are done! +### /api/notes/remove - POST +#### Request +```protobuf +message NoteRequest { + NoteID noteId = 1; + Token token +} +``` +#### Response +HTTP 200 OK +No response body -## 🔐 Encryption +### /api/notes/list - POST +#### Request +```protobuf +message Token { + string token = 1; +} +``` +#### Response +HTTP 200 OK +```protobuf +message ApiNotesListResponse { + repeated NoteMetadata notes = 1; +} +``` -Note content and title is encrypted using AES 256-bit. +### /api/notes/get - POST +#### Request +```protobuf +message NoteRequest { + NoteID noteId = 1; + Token token +} +``` +#### Response +HTTP 200 OK +```protobuf +message Note { + NoteMetadata metadata = 1; + AESData note = 2; +} +``` -Encryption password is the SHA512 hashed password we talked about earlier. +### /api/notes/edit - POST +#### Request +```protobuf +message ApiNotesEditRequest { + Note note = 1; + Token token = 2; +} +``` +#### Response +HTTP 200 OK +No response body -## 🕹ī¸ Basic stuff +### /api/notes/purge - POST - Show a warning before this action! +#### Request +```protobuf +message Token { + string token = 1; +} +``` +#### Response +HTTP 200 OK +No response body -POST - /api/userinfo - get user info such as username, provide "secretKey" +## Shared notes - This is not yet implemented +### /api/invite/prepare - POST +#### Request +```protobuf +message ApiInvitePrepareRequest { + string username = 1; + Token token = 2; +} +``` +#### Response +HTTP 200 OK +```protobuf +message ApiInvitePrepareResponse { + bytes ecdhExchange = 1; +} +``` -POST - /api/listnotes - list notes, provide "secretKey" -note titles will have to be decrypted. +### /api/invite/check - POST +#### Request +```protobuf +message Token { + string token = 1; +} +``` +#### Response +HTTP 200 OK +```protobuf +message ApiInviteCheckResponse { + repeated Invitation invitations = 1; +} +``` -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. +### /api/invite/link - POST +#### Request +```protobuf +message ApiInviteLinkRequest { + NoteRequest noteRequest = 1; + int64 timestamp = 2; + bool singleUse = 3; +} +``` +#### Response +HTTP 200 OK +```protobuf +message ApiInviteLinkResponse { + bytes inviteCode = 1; +} +``` -POST - /api/readnote - read notes, provide "secretKey" and "noteId" -note content will have to be decrypted. +### /api/invite/accept - POST +#### Request +```protobuf +message ApiInviteAcceptRequest { + bytes inviteCode = 1; + Token token = 2; +} +``` +#### Response +HTTP 200 OK +```protobuf +message NoteID { + bytes noteId = 1; +} +``` -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. -"title" is the first line of the note content, and should be encrypted. +### /api/invite/leave - POST +#### Request +```protobuf +message NoteRequest { + NoteID noteId = 1; + Token token +} +``` +#### Response +HTTP 200 OK +No response body -POST - /api/removenote - remove notes, provide "secretKey" and "noteId" +### /api/shared - WebSocket +Every heartbeat interval (500ms): +#### Request +```protobuf +message ApiSharedRequest { + repeated uint64 lines = 1; + Token token = 2; +} +``` +#### Response +```protobuf +message ApiSharedResponse { + repeated UserLines users = 1; +} +``` -POST - /api/purgenotes - remove all notes, provide "secretKey" -### Please display a warning before this action +### /api/shared/edit - POST +#### Request +```protobuf +message ApiSharedEditRequest { + repeated AESData lines = 1; + Token token = 2; +} +``` +#### Response +HTTP 200 OK +No response body -## ⚙ī¸ 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" -### Please display a warning before this action - -POST - /api/exportnotes - export notes, provide "secretKey" -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/remove - remove session, provide "secretKey" and "sessionId" - -## ‍đŸ’ŧ Admin controls - -POST - /api/listusers - lists all users in JSON, provide "masterKey" (set in config.ini) +### /api/shared/get - POST +#### Request +```protobuf +message NoteRequest { + NoteID noteId = 1; + Token token +} +``` +#### Response +```protobuf +message ApiSharedGetResponse { + repeated AESData lines = 1; + NoteMetadata metadata = 2; +} +``` \ No newline at end of file diff --git a/ERRORS.md b/ERRORS.md deleted file mode 100644 index dc45bbf..0000000 --- a/ERRORS.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/README.md b/README.md index 52145e3..1852d77 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,33 @@ Burgernotes is a simple note-taking app with end-to-end encryption. ### Setup -To set up Burgernotes, simply run these commands: +To set up Burgernotes, set it up like any other Fulgens Server module. ``` -cp config.ini.example config.ini -./burgernotes init_db +cd /path/to/fulgens/directory +git clone https://git.ailur.dev/ailur/burgernotes.git --depth=1 services-src/burgernotes +``` +If you want to rebuild all of fulgens (recommended), run `./build.sh` in the fulgens directory. +If you only want to build the Burgernotes module, run `services-src/burgernotes/build.sh`. + +### Configuration +Edit the main `config.json` file to include the Burgernotes module in the `services` object. +```json +{ + "burgernotes": { + "subdomain": "notes.example.org", + "hostName": "https://notes.example.org" + } +} ``` -Edit config.ini to your liking, then to start the server run: +### Running +Run the Fulgens server as you normally would. ``` -./burgernotes +./fulgens ``` + ### Links -[Go to the Burgernotes website](https://notes.hectabit.org) +[Go to the Burgernotes website](https://notes.ailur.dev) [API documentation](APIDOCS.md) diff --git a/ROADMAP.md b/ROADMAP.md index a78114b..0a12453 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,5 @@ # Burgernotes Roadmap -- 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 -- Use Burgerauth for authentication -- Dedicated domain (not just a subdomain, if anyone can donate a domain let Arzumify know!) +- Create the frontend +- Implementing shared notes +- Dedicated domain (not just a subdomain, if anyone can donate a domain let Arzumify know!) \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..b3a9d55 --- /dev/null +++ b/build.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +path=$(realpath "$(dirname "$0")") || exit 1 +rm -rf "$path/../../services/burgernotes.fgs" || exit 1 +cd "$path" || exit 1 +protoc -I="$path" --go_out="$path" "$path/protobufs/main.proto" || exit 1 +go build -o "$path/../../services/burgernotes.fgs" --buildmode=plugin -ldflags "-s -w" || exit 1 diff --git a/config.ini.example b/config.ini.example deleted file mode 100644 index 3caa559..0000000 --- a/config.ini.example +++ /dev/null @@ -1,5 +0,0 @@ -[config] -HOST = 0.0.0.0 -PORT = 8080 -SECRET_KEY = supersecretkey -MAX_STORAGE = 25000000 diff --git a/generate.go b/generate.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/generate.go @@ -0,0 +1 @@ +package main diff --git a/go.mod b/go.mod index 37a0038..07f8596 100644 --- a/go.mod +++ b/go.mod @@ -1,58 +1,15 @@ -module hectabit.org/burgernotes +module git.ailur.dev/ailur/burgernotes -go 1.22 +go 1.23.1 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 + git.ailur.dev/ailur/fg-library/v2 v2.0.1 + git.ailur.dev/ailur/fg-nucleus-library v1.0.2 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/uuid v1.6.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 + github.com/go-chi/chi/v5 v5.1.0 + google.golang.org/protobuf v1.35.1 ) diff --git a/go.sum b/go.sum index 51f0fc3..166b9c4 100644 --- a/go.sum +++ b/go.sum @@ -1,144 +1,16 @@ -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= +git.ailur.dev/ailur/fg-library/v2 v2.0.1 h1:ltPYXf/Om0hnMD8gr1K5bkYrfHqKPSbb0hxa0wtTnZ0= +git.ailur.dev/ailur/fg-library/v2 v2.0.1/go.mod h1:1jYbWhabGcIwp7CkhHqvRwC8eP+nHv5BrXPe9NX2HE8= +git.ailur.dev/ailur/fg-nucleus-library v1.0.2 h1:EWfeab+wJKaxx/Qg5TKpvZHicA0V/NilUv2g6W97rtg= +git.ailur.dev/ailur/fg-nucleus-library v1.0.2/go.mod h1:T2mdUiXlZqb917CkNB2vwujkD/QhJDpCHLRvKuskBpY= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= diff --git a/main.go b/main.go index 911393c..963ca5c 100644 --- a/main.go +++ b/main.go @@ -1,1463 +1,757 @@ package main import ( - "crypto/rand" - "database/sql" - "encoding/hex" - "encoding/json" + "bytes" + "git.ailur.dev/ailur/burgernotes/git.ailur.dev/ailur/burgernotes/protobuf" + "errors" - "fmt" - "github.com/catalinc/hashcash" - "log" + "io" "os" - "regexp" - "strconv" "strings" "time" - "github.com/gin-contrib/sessions" - "github.com/gin-contrib/sessions/cookie" - "github.com/gin-gonic/gin" - "github.com/mattn/go-sqlite3" - "github.com/spf13/viper" - "golang.org/x/crypto/scrypt" + "crypto/ed25519" + "encoding/json" + "io/fs" + "net/http" + "net/url" + + library "git.ailur.dev/ailur/fg-library/v2" + nucleusLibrary "git.ailur.dev/ailur/fg-nucleus-library" + "github.com/go-chi/chi/v5" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "google.golang.org/protobuf/proto" ) -var ( - conn *sql.DB - mem *sql.DB - host string - port int - secretKey string - maxStorage int64 - saltChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -) - -func randomChars(length int) (string, error) { - if length <= 0 { - return "", errors.New("salt length must be at least one") - } - - salt := make([]byte, length) - randomBytes := make([]byte, length) - _, err := rand.Read(randomBytes) - if err != nil { - return "", err - } - - for i := range salt { - salt[i] = saltChars[int(randomBytes[i])%len(saltChars)] - } - return string(salt), nil +var ServiceInformation = library.Service{ + Name: "burgernotes", + Permissions: library.Permissions{ + Authenticate: true, // This service does require authentication + Database: true, // This service does require database access + BlobStorage: true, // This service does require blob storage access + InterServiceCommunication: true, // This service does require inter-service communication + Resources: false, // This service is API-only, so it does not require resources + }, + ServiceID: uuid.MustParse("b0bee29e-00c4-4ead-a5d6-3f792ff25174"), } -func hash(password, salt string) (string, error) { - passwordBytes := []byte(password) - saltBytes := []byte(salt) - - derivedKey, err := scrypt.Key(passwordBytes, saltBytes, 32768, 8, 1, 64) - if err != nil { - return "", err - } - - hashString := fmt.Sprintf("scrypt:32768:8:1$%s$%s", salt, hex.EncodeToString(derivedKey)) - return hashString, nil -} - -func verifyHash(werkzeugHash, password string) (bool, error) { - parts := strings.Split(werkzeugHash, "$") - if len(parts) != 3 || parts[0] != "scrypt:32768:8:1" { - return false, errors.New("invalid hash format") - } - salt := parts[1] - - computedHash, err := hash(password, salt) - if err != nil { - return false, err - } - - return werkzeugHash == computedHash, nil -} - -func getUser(id int) (string, string, string, error) { - var created, username, password string - err := conn.QueryRow("SELECT created, username, password FROM users WHERE id = ? LIMIT 1", id).Scan(&created, &username, &password) - if err != nil { - return "", "", "", err - } - - return created, username, password, err -} - -func getNote(id int) (int, string, string, string, string, error) { - var creator int - var created, edited, content, title string - err := conn.QueryRow("SELECT creator, created, edited, content, title FROM notes WHERE id = ? LIMIT 1", id).Scan(&creator, &created, &edited, &content, &title) - if err != nil { - return 0, "", "", "", "", err - } - - return creator, created, edited, content, title, err -} - -func getSpace(id int) (int, error) { - var space int - err := conn.QueryRow("SELECT COALESCE(SUM(LENGTH(content) + LENGTH(title)), 0) FROM notes WHERE creator = ?", id).Scan(&space) - if err != nil { - return 0, err - } - return space, nil -} - -func getNoteCount(id int) (int, error) { - var count int - err := conn.QueryRow("SELECT COUNT(*) FROM notes WHERE creator = ?", id).Scan(&count) - if err != nil { - return 0, err - } - return count, nil -} - -func checkUsernameTaken(username string) (int, bool, error) { - var id int - err := conn.QueryRow("SELECT id FROM users WHERE lower(username) = ? LIMIT 1", strings.ToLower(username)).Scan(&id) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return 0, false, nil - } else { - return 0, true, err - } - } else { - return id, true, nil - } -} - -func getSession(session string) (int, int, error) { - var id int - var sessionId int - err := mem.QueryRow("SELECT sessionid, id FROM sessions WHERE session = ? LIMIT 1", session).Scan(&sessionId, &id) - if err != nil { - return 0, 0, err - } - return sessionId, id, err -} - -func getSessionFromId(sessionId int) (string, int, error) { - var id int - var session string - err := mem.QueryRow("SELECT session, id FROM sessions WHERE sessionid = ? LIMIT 1", sessionId).Scan(&session, &id) - if err != nil { - return "", 0, err - } - return session, id, err -} - -func generateDB() error { - schemaBytes, err := os.ReadFile("schema.sql") +func unmarshalProtobuf(r *http.Request, protobuf proto.Message) error { + var protobufData []byte + _, err := r.Body.Read(protobufData) if err != nil { return err } - _, err = conn.Exec(string(schemaBytes)) + + err = proto.Unmarshal(protobufData, protobuf) if err != nil { return err } - log.Println("[INFO] Generated database") + return nil } -func initDb() { - _, err := os.Stat("database.db") - if os.IsNotExist(err) { - err = generateDB() - if err != nil { - log.Fatalln("[FATAL] Unknown while generating database:", err) - } - } else { - log.Print("[PROMPT] Proceeding will overwrite the database. Proceed? (y/n): ") - var answer string - _, err := fmt.Scanln(&answer) - if err != nil { - log.Fatalln("[FATAL] Unknown while scanning input:", err) - } - if strings.ToLower(answer) == "y" { - err := generateDB() - if err != nil { - log.Fatalln("[FATAL] Unknown while generating database:", err) - return - } - } else if answer == ":3" { - log.Println("[:3] :3") - } else { - log.Println("[INFO] Stopped") - } +func logFunc(message string, messageType uint64, information library.ServiceInitializationInformation) { + // Log the message to the logger service + information.Outbox <- library.InterServiceMessage{ + ServiceID: ServiceInformation.ServiceID, + ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000002"), // Logger service + MessageType: messageType, + SentAt: time.Now(), + Message: message, } } -func migrateDb() { - _, err := os.Stat("database.db") - if os.IsNotExist(err) { - err = generateDB() - if err != nil { - log.Fatalln("[FATAL] Unknown while generating database:", err) - } - } else { - log.Println("[PROMPT] Proceeding will render the database unusable for older versions of Burgernotes. Proceed? (y/n): ") - var answer string - _, err := fmt.Scanln(&answer) - if err != nil { - log.Fatalln("[FATAL] Unknown while scanning input:", err) - } - if strings.ToLower(answer) == "y" { - _, err = conn.Exec("ALTER TABLE users DROP COLUMN versionTwoLegacyPassword") - if err != nil { - log.Println("[WARN] Unknown while migrating database (1/4):", err) - log.Println("[INFO] This is likely because your database is already migrated. This is not a problem, and Burgernotes does not need this removed - it is just for cleanup") - } - _, err = conn.Exec("CREATE TABLE oauth (id INTEGER NOT NULL, oauthProvider TEXT NOT NULL, encryptedPasswd TEXT NOT NULL)") - if err != nil { - log.Println("[WARN] Unknown while migrating database (2/4):", err) - log.Println("[INFO] This is likely because your database is already migrated. This is not a problem, but if it is not, it may cause issues with OAuth2") - } - _, err = conn.Exec("DROP TABLE sessions") - if err != nil { - log.Println("[WARN] Unknown while migrating database (3/4):", err) - log.Println("[INFO] This is likely because your database is already migrated. This is not a problem, and Burgernotes does not need this removed - it is just for cleanup") - } - _, err = conn.Exec("ALTER TABLE users ADD COLUMN migrated INTEGER NOT NULL DEFAULT 0") - if err != nil { - log.Println("[WARN] Unknown while migrating database (4/4):", err) - log.Println("[INFO] This is likely because your database is already migrated. This is not a problem, but if it is not, it may cause issues with migrating to Burgernotes 2.0") - } - } else if answer == ":3" { - log.Println("[:3] :3") - } else { - log.Println("[INFO] Stopped") - } - } -} - -func main() { - if _, err := os.Stat("config.ini"); err == nil { - log.Println("[INFO] Config loaded") - } else if os.IsNotExist(err) { - log.Fatalln("[FATAL] config.ini does not exist") - } else { - log.Fatalln("[FATAL] File is in quantum uncertainty:", err) +func askBlobService(body any, information library.ServiceInitializationInformation, context uint64) (library.InterServiceMessage, error) { + // Ask the blob storage service for the thing + information.Outbox <- library.InterServiceMessage{ + ServiceID: ServiceInformation.ServiceID, + ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000003"), // Blob storage service + MessageType: context, + SentAt: time.Now(), + Message: body, } - viper.SetConfigName("config") - viper.AddConfigPath("./") - viper.AutomaticEnv() - - err := viper.ReadInConfig() - if err != nil { - log.Fatalln("[FATAL] Error in config file:", err) - } - - host = viper.GetString("config.HOST") - port = viper.GetInt("config.PORT") - secretKey = viper.GetString("config.SECRET_KEY") - maxStorage = viper.GetInt64("config.MAX_STORAGE") - - if host == "" { - log.Fatalln("[FATAL] HOST is not set") - } - - if port == 0 { - log.Fatalln("[FATAL] PORT is not set") - } - - if secretKey == "" { - log.Fatalln("[FATAL] SECRET_KEY is not set") - } else if secretKey == "supersecretkey" { - log.Println("[WARN] SECRET_KEY is set to a default value. Please set it to another value.") - } - - if maxStorage == 0 { - log.Fatalln("[FATAL] MAX_STORAGE is not set") - } - - conn, err = sql.Open("sqlite3", "database.db") - if err != nil { - log.Fatalln("[FATAL] Cannot open database:", err) - } - defer func(conn *sql.DB) { - err := conn.Close() - if err != nil { - log.Println("[ERROR] Unknown in main() conn defer:", err) - } - }(conn) - - mem, err = sql.Open("sqlite3", "file:bgnsessiondb?cache=shared&mode=memory") - if err != nil { - log.Fatalln("[FATAL] Cannot open session database:", err) - } - defer func(mem *sql.DB) { - err := mem.Close() - if err != nil { - log.Println("[ERROR] Unknown in main() mem defer:", err) - } - }(mem) - - _, err = mem.Exec("CREATE TABLE sessions (sessionid INTEGER PRIMARY KEY AUTOINCREMENT, session TEXT NOT NULL, id INTEGER NOT NULL, device TEXT NOT NULL DEFAULT '?')") - if err != nil { - if err.Error() == "table sessions already exists" { - log.Println("[INFO] Session table already exists") - } else { - log.Fatalln("[FATAL] Cannot create session table:", err) - } - } - - _, err = mem.Exec("CREATE TABLE spent (hashcash TEXT NOT NULL, expires INTEGER NOT NULL)") - if err != nil { - if err.Error() == "table spent already exists" { - log.Println("[INFO] Spent table already exists") - } else { - log.Fatalln("[FATAL] Cannot create spent table:", err) - } - - } - - if len(os.Args) > 1 { - if os.Args[1] == "init_db" { - initDb() - os.Exit(0) - } else if os.Args[1] == "migrate_db" { - migrateDb() - os.Exit(0) - } - } - - gin.SetMode(gin.ReleaseMode) - router := gin.New() - store := cookie.NewStore([]byte(secretKey)) - router.Use(sessions.Sessions("session", store)) - - router.Use(func(c *gin.Context) { - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") - c.Writer.Header().Set("Access-Control-Allow-Headers", "*, Authorization") - c.Writer.Header().Set("Access-Control-Allow-Methods", "*") - - if c.Request.Method == "OPTIONS" { - c.AbortWithStatus(200) - return - } - - c.Next() - }) - - router.GET("/api/version", func(c *gin.Context) { - c.String(200, "Burgernotes Version 2.0 Beta 2") - }) - - router.GET("/api/versionjson", func(c *gin.Context) { - c.JSON(200, gin.H{"name": "Burgernotes", "versiontxt": "Version 2.0 Beta 2", "versionsem": "2.0.0b2", "versionnum": "200"}) - }) - - router.POST("/api/signup", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - username, ok := data["username"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - password, ok := data["password"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - stamp, ok := data["stamp"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - var spentStamp string - err = mem.QueryRow("SELECT hashcash FROM spent WHERE hashcash = ?", stamp).Scan(&spentStamp) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - _, err = mem.Exec("INSERT INTO spent (hashcash, expires) VALUES (?, ?)", stamp, time.Now().Unix()+86400) - if err != nil { - log.Println("[ERROR] Unknown in /api/signup spent Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SIGNUP-SPENTINSERT"}) - return - } - } else { - log.Println("[ERROR] Unknown in /api/signup spent QueryRow():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SIGNUP-SPENTSELECT"}) - return - } - } else { - c.JSON(409, gin.H{"error": "Stamp already spent"}) - return - } - - if strings.Split(stamp, ":")[3] != "signup" || strings.Split(stamp, ":")[4] != "I love Burgernotes!" { - c.JSON(400, gin.H{"error": "Invalid hashcash stamp"}) - return - } - - pow := hashcash.New(20, 16, "I love Burgernotes!") - ok = pow.Check(stamp) - if !ok { - c.JSON(400, gin.H{"error": "Invalid hashcash stamp"}) - return - } - - if username == "" || password == "" || len(username) > 20 || !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(username) { - c.JSON(422, gin.H{"error": "Invalid username or password"}) - return - } - - _, taken, err := checkUsernameTaken(username) - if err != nil { - log.Println("[ERROR] Unknown in /api/signup checkUsernameTaken():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SIGNUP-USERTAKEN"}) - return - } - if taken { - c.JSON(409, gin.H{"error": "Username is taken"}) - return - } - - salt, err := randomChars(16) - if err != nil { - log.Println("[ERROR] Unknown in /api/signup randomChars():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SIGNUP-SALT"}) - return - } - hashedPasswd, err := hash(password, salt) - if err != nil { - log.Println("[ERROR] Unknown in /api/signup hash():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SIGNUP-HASH"}) - return - } - - _, err = conn.Exec("INSERT INTO users (username, password, created, migrated) VALUES (?, ?, ?, 1)", username, hashedPasswd, strconv.FormatInt(time.Now().Unix(), 10)) - if err != nil { - log.Println("[ERROR] Unknown in /api/signup Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SIGNUP-DBINSERT"}) - return - } - - log.Println("[INFO] Added new user") - userid, taken, err := checkUsernameTaken(username) - if !taken { - log.Println("[CRITICAL] Something is very wrong! A user was created but could not be found in the database") - log.Println("[INFO] This should not be possible. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes with the error code: UNKNOWN-API-SIGNUP-POSTTAKEN") - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SIGNUP-POSTTAKEN"}) - return - } - - token, err := randomChars(512) - if err != nil { - log.Println("[ERROR] Unknown in /api/signup token randomChars():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SIGNUP-SESSIONSALT"}) - return - } - _, err = mem.Exec("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)", token, userid, c.Request.Header.Get("User-Agent")) - if err != nil { - log.Println("[ERROR] Unknown in /api/signup session Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SIGNUP-SESSIONINSERT"}) - return - } - - c.JSON(200, gin.H{"key": token}) - }) - - router.POST("/api/login", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - username, ok := data["username"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - password, ok := data["password"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - modern, ok := data["modern"].(bool) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - userid, taken, err := checkUsernameTaken(username) - if !taken { - c.JSON(401, gin.H{"error": "User does not exist"}) - return - } else if err != nil { - log.Println("[ERROR] Unknown in /api/login checkUsernameTaken():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LOGIN-USERTAKEN"}) - return - } - - var migrated int - err = conn.QueryRow("SELECT migrated FROM users WHERE id = ?", userid).Scan(&migrated) - if err != nil { - log.Println("[ERROR] Unknown in /api/login migrated QueryRow():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LOGIN-MIGRATED"}) - return - } - - _, _, hashedPasswd, err := getUser(userid) - if err != nil { - log.Println("[ERROR] Unknown in /api/login getUser():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LOGIN-GETUSER"}) - return - } - - correctPassword, err := verifyHash(hashedPasswd, password) - if err != nil { - if errors.Is(err, errors.New("invalid hash format")) { - c.JSON(422, gin.H{"error": "Invalid hash format"}) - return - } else { - log.Println("[ERROR] Unknown in /api/login verifyHash():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LOGIN-VERIFYHASH"}) - return - } - } else if !correctPassword { - if migrated == 0 { - c.JSON(401, gin.H{"error": "User has not migrated", "migrated": false}) - return - } else { - c.JSON(401, gin.H{"error": "Incorrect password", "migrated": true}) - return - } - } else { - if modern && migrated == 0 { - _, err := conn.Exec("UPDATE users SET migrated = 1 WHERE id = ?", userid) - if err != nil { - log.Println("[ERROR] Unknown in /api/login modern Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LOGIN-MODERN"}) - return - } - } - } - - token, err := randomChars(512) - if err != nil { - log.Println("[ERROR] Unknown in /api/login token randomChars():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LOGIN-SESSIONSALT"}) - return - } - - _, err = mem.Exec("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)", token, userid, c.Request.Header.Get("User-Agent")) - if err != nil { - log.Println("[ERROR] Unknown in /api/login session Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LOGIN-SESSIONINSERT"}) - return - } - - if migrated != 1 { - c.JSON(200, gin.H{"key": token, "migrated": false}) - } else { - c.JSON(200, gin.H{"key": token, "migrated": true}) - } - }) - - router.POST("/api/oauth/get", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - username, ok := data["username"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - oauthProvider, ok := data["oauthProvider"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - _, userid, err := checkUsernameTaken(username) - if err != nil { - c.JSON(404, gin.H{"error": "Username not found"}) - return - } - - var encryptedPasswd string - err = conn.QueryRow("SELECT encryptedPasswd FROM oauth WHERE id = ? AND oauthProvider = ?", userid, oauthProvider).Scan(&encryptedPasswd) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - c.JSON(404, gin.H{"error": "Entry not found"}) - } else { - log.Println("[ERROR] Unknown in /api/oauth/get select:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-OAUTH-GET-SELECT"}) - } - } - - c.JSON(200, gin.H{"password": encryptedPasswd}) - }) - - router.POST("/api/oauth/add", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - oauthProvider, ok := data["oauthProvider"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - encryptedPassword, ok := data["encryptedPassword"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - } - - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - _, err = conn.Exec("INSERT INTO oauth (id, oauthProvider, encryptedPasswd) VALUES (?, ?, ?)", userid, oauthProvider, encryptedPassword) - if err != nil { - if errors.Is(err, sqlite3.ErrConstraintUnique) { - c.JSON(409, gin.H{"error": "Entry already exists"}) - } else { - log.Println("[ERROR] Unknown in /api/oauth/add Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-OAUTH-ADD-EXEC"}) - } - } - - c.JSON(200, gin.H{"success": true}) - }) - - router.POST("/api/oauth/remove", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - oauthProvider, ok := data["oauthProvider"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - _, err = conn.Exec("DELETE FROM oauth WHERE userid = ? AND oauthProvider = ?", userid, oauthProvider) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - c.JSON(404, gin.H{"error": "Entry not found"}) - } else { - log.Println("[ERROR] Unknown in /api/oauth/add Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-OAUTH-REMOVE-EXEC"}) - } - } - - c.JSON(200, gin.H{"success": true}) - }) - - router.POST("/api/changepassword", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - newPassword, ok := data["newPassword"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - migrate, ok := data["migration"].(bool) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - salt, err := randomChars(16) - if err != nil { - log.Println("[ERROR] Unknown in /api/changepassword randomChars():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-CHANGEPASSWORD-SALT"}) - return - } - hashedPassword, err := hash(newPassword, salt) - if err != nil { - log.Println("[ERROR] Unknown in /api/changepassword hash():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-CHANGEPASSWORD-HASH"}) - return - } - - _, err = conn.Exec("UPDATE users SET password = ? WHERE id = ?", hashedPassword, userid) - if err != nil { - log.Println("[ERROR] Unknown in /api/changepassword Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-CHANGEPASSWORD-DBUPDATE"}) - return - } - - if migrate { - _, err = conn.Exec("UPDATE users SET migrated = 1 WHERE id = ?", userid) - if err != nil { - log.Println("[ERROR] Unknown in /api/changepassword migrate Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-CHANGEPASSWORD-MIGRATE"}) - return - } - } - - c.JSON(200, gin.H{"success": true}) - }) - - router.POST("/api/userinfo", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - created, username, _, err := getUser(userid) - if err != nil { - log.Println("[ERROR] Unknown in /api/userinfo getUser():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-USERINFO-GETUSER"}) - return - } - - space, err := getSpace(userid) - if err != nil { - log.Println("[ERROR] Unknown in /api/userinfo getSpace():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-USERINFO-GETSPACE"}) - return - } - - notecount, err := getNoteCount(userid) - if err != nil { - log.Println("[ERROR] Unknown in /api/userinfo getNoteCount():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-USERINFO-GETNOTECOUNT"}) - return - } - c.JSON(200, gin.H{"username": username, "id": userid, "created": created, "storageused": space, "storagemax": maxStorage, "notecount": notecount}) - }) - - router.POST("/api/loggedin", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - if userid > 0 { - c.JSON(200, gin.H{"loggedin": true}) - } else { - c.JSON(403, gin.H{"loggedin": false}) - } - }) - - router.POST("/api/listnotes", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - rows, err := conn.Query("SELECT id, title FROM notes WHERE creator = ? ORDER BY id DESC", userid) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - c.JSON(200, []map[string]interface{}{}) - return - } else { - log.Println("[ERROR] Unknown in /api/listnotes query:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LISTNOTES-DBQUERY"}) - return - } - } - defer func(rows *sql.Rows) { - err := rows.Close() - if err != nil { - log.Println("[ERROR] Unknown in /api/listnotes row defer:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LISTNOTES-ROWCLOSE"}) - return - } - }(rows) - - var notes []map[string]interface{} - for rows.Next() { - var id int - var title string - if err := rows.Scan(&id, &title); err != nil { - log.Println("[ERROR] Unknown in /api/listnotes row scan:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LISTNOTES-ROWSCAN"}) - return - } - notes = append(notes, map[string]interface{}{"id": id, "title": title}) - } - if err := rows.Err(); err != nil { - log.Println("[ERROR] Unknown in /api/listnotes row iteration:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LISTNOTES-ROWERR"}) - return - } - - c.JSON(200, notes) - }) - - router.POST("/api/exportnotes", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - rows, err := conn.Query("SELECT id, created, edited, title, content FROM notes WHERE creator = ? ORDER BY edited DESC", userid) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - c.JSON(200, []map[string]interface{}{}) - return - } else { - log.Println("[ERROR] Unknown in /api/exportnotes query:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-EXPORTNOTES-DBQUERY"}) - return - } - } - defer func(rows *sql.Rows) { - err := rows.Close() - if err != nil { - log.Println("[ERROR] Unknown in /api/exportnotes row defer:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-EXPORTNOTES-ROWCLOSE"}) - return - } - }(rows) - - var notes []map[string]interface{} - for rows.Next() { - var id int - var created, edited, title, content string - if err := rows.Scan(&id, &created, &edited, &title, &content); err != nil { - log.Println("[ERROR] Unknown in /api/exportnotes row scan:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-EXPORTNOTES-ROWSCAN"}) - return - } - notes = append(notes, map[string]interface{}{"id": id, "created": created, "edited": edited, "title": title, "content": content}) - } - if err := rows.Err(); err != nil { - log.Println("[ERROR] Unknown in /api/exportnotes row iteration:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-EXPORTNOTES-ROWERR"}) - return - } - - c.JSON(200, notes) - }) - - router.POST("/api/importnotes", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - notesStr, ok := data["notes"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - var notes []interface{} - err = json.Unmarshal([]byte(notesStr), ¬es) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - for _, note := range notes { - note := note.(map[string]interface{}) - _, err := conn.Exec("INSERT INTO notes (creator, created, edited, title, content) VALUES (?, ?, ?, ?, ?)", userid, note["created"], note["edited"], note["title"], note["content"]) - if err != nil { - log.Println("[ERROR] Unknown in /api/importnotes Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-IMPORTNOTES-DBINSERT"}) - return - } - } - - c.JSON(200, gin.H{"success": true}) - }) - - router.POST("/api/newnote", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - noteName, ok := data["noteName"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - space, err := getSpace(userid) - if int64(len(noteName)+space) > maxStorage { - c.JSON(403, gin.H{"error": "Storage limit reached"}) - return - } else { - _, err := conn.Exec("INSERT INTO notes (title, content, creator, created, edited) VALUES (?, ?, ?, ?, ?)", noteName, "", userid, strconv.FormatInt(time.Now().Unix(), 10), strconv.FormatInt(time.Now().Unix(), 10)) - if err != nil { - log.Println("[ERROR] Unknown in /api/newnote Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-NEWNOTE-DBINSERT"}) - return - } else { - c.JSON(200, gin.H{"success": true}) - } - } - }) - - router.POST("/api/readnote", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - noteIdFloat, ok := data["noteId"].(float64) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - noteId := int(noteIdFloat) - - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - creator, _, _, content, _, err := getNote(noteId) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - c.JSON(422, gin.H{"error": "Note not found"}) - return - } else { - log.Println("[ERROR] Unknown in /api/readnote getNote():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-READNOTE-GETNOTE"}) - return - } - } else { - if creator != userid { - c.JSON(422, gin.H{"error": "Note does not belong to user"}) - return - } else { - c.JSON(200, gin.H{"content": content}) - } - } - }) - - router.POST("/api/purgenotes", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - _, err = conn.Exec("DELETE FROM notes WHERE creator = ?", userid) - if err != nil { - log.Println("[ERROR] Unknown in /api/purgenotes Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-PURGENOTES-DBDELETE"}) - return - } else { - c.JSON(200, gin.H{"success": true}) - } - }) - - router.POST("/api/editnote", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - noteIdFloat, ok := data["noteId"].(float64) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - noteId := int(noteIdFloat) - content, ok := data["content"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - title, ok := data["title"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - creator, _, _, _, _, err := getNote(noteId) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - c.JSON(422, gin.H{"error": "Note not found"}) - return - } else { - log.Println("[ERROR] Unknown in /api/editnote getNote():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-EDITNOTE-GETNOTE"}) - return - } - } - - if creator != userid { - c.JSON(403, gin.H{"error": "Note does not belong to user"}) - return - } else { - _, err := conn.Exec("UPDATE notes SET content = ?, title = ?, edited = ? WHERE id = ?", content, title, strconv.FormatInt(time.Now().Unix(), 10), noteId) - if err != nil { - log.Println("[ERROR] Unknown in /api/editnote Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-EDITNOTE-DBUPDATE"}) - return - } else { - c.JSON(200, gin.H{"success": true}) - } - } - }) - - router.POST("/api/removenote", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - noteIdFloat, ok := data["noteId"].(float64) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - noteId := int(noteIdFloat) - - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - creator, _, _, _, _, err := getNote(noteId) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - c.JSON(422, gin.H{"error": "Note not found"}) - return - } else { - log.Println("[ERROR] Unknown in /api/removenote getNote():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-REMOVENOTE-GETNOTE"}) - return - } - } - - if creator != userid { - c.JSON(403, gin.H{"error": "Note does not belong to user"}) - return - } else { - _, err := conn.Exec("DELETE FROM notes WHERE id = ?", noteId) - if err != nil { - log.Println("[ERROR] Unknown in /api/removenote Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-REMOVENOTE-DBDELETE"}) - return - } else { - c.JSON(200, gin.H{"success": true}) - } - } - }) - - router.POST("/api/deleteaccount", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - _, err = conn.Exec("DELETE FROM notes WHERE creator = ?", userid) - if err != nil { - log.Println("[ERROR] Unknown in /api/deleteaccount notes Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-DELETEACCOUNT-NOTESDELETE"}) - return - } - - _, err = conn.Exec("DELETE FROM users WHERE id = ?", userid) - if err != nil { - log.Println("[ERROR] Unknown in /api/deleteaccount user Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-DELETEACCOUNT-USERDELETE"}) - return - } - - _, err = mem.Exec("DELETE FROM sessions WHERE id = ?", userid) - if err != nil { - log.Println("[ERROR] Unknown in /api/deleteaccount session Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-DELETEACCOUNT-SESSIONDELETE"}) - return - } - - c.JSON(200, gin.H{"success": true}) - }) - - router.POST("/api/sessions/list", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - rows, err := mem.Query("SELECT sessionid, session, device FROM sessions WHERE id = ? ORDER BY id DESC", userid) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - c.JSON(200, []map[string]interface{}{}) - return - } else { - log.Println("[ERROR] Unknown in /api/sessions/list query:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SESSIONS-LIST-DBQUERY"}) - return - } - } - defer func(rows *sql.Rows) { - err := rows.Close() - if err != nil { - log.Println("[ERROR] Unknown in /api/sessions/list row defer:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SESSIONS-LIST-ROWCLOSE"}) - return - } - }(rows) - - var sessionList []map[string]interface{} - for rows.Next() { - var sessionid int - var session, device string - if err := rows.Scan(&sessionid, &session, &device); err != nil { - log.Println("[ERROR] Unknown in /api/sessions/list row scan:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SESSIONS-LIST-ROWSCAN"}) - return - } - if session == token { - sessionList = append(sessionList, map[string]interface{}{"id": sessionid, "thisSession": true, "device": device}) - } else { - sessionList = append(sessionList, map[string]interface{}{"id": sessionid, "thisSession": false, "device": device}) - } - } - if err := rows.Err(); err != nil { - log.Println("[ERROR] Unknown in /api/sessions/list row iteration:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SESSIONS-LIST-ROWERR"}) - return - } - - c.JSON(200, sessionList) - }) - - router.POST("/api/sessions/remove", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token, ok := data["secretKey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - sessionIdFloat, ok := data["sessionId"].(float64) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - sessionId := int(sessionIdFloat) - - _, userid, err := getSession(token) - if err != nil { - c.JSON(401, gin.H{"error": "Invalid session"}) - return - } - - _, creator, err := getSessionFromId(sessionId) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - c.JSON(422, gin.H{"error": "Target session not found"}) - return - } else { - log.Println("[ERROR] Unknown in /api/sessions/remove getSession():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SESSIONS-REMOVE-GETSESSION"}) - return - } - } else { - if creator != userid { - c.JSON(403, gin.H{"error": "Session does not belong to user"}) - return - } else { - _, err := mem.Exec("DELETE FROM sessions WHERE sessionid = ?", sessionId) - if err != nil { - log.Println("[ERROR] Unknown in /api/sessions/remove Exec():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SESSIONS-REMOVE-DBDELETE"}) - return - } else { - c.JSON(200, gin.H{"success": true}) - } - } - } - }) - - router.POST("/api/listusers", func(c *gin.Context) { - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - masterToken, ok := data["masterkey"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - if masterToken == secretKey { - rows, err := conn.Query("SELECT id, username, created FROM users ORDER BY id DESC") - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - c.JSON(200, []map[string]interface{}{}) - return - } else { - log.Println("[ERROR] Unknown in /api/listusers query:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LISTUSERS-DBQUERY"}) - return - } - } - defer func(rows *sql.Rows) { - err := rows.Close() - if err != nil { - log.Println("[ERROR] Unknown in /api/listusers row defer:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LISTUSERS-ROWCLOSE"}) - return - } - }(rows) - - var users []map[string]interface{} - for rows.Next() { - var id int - var username, created string - if err := rows.Scan(&id, &username, &created); err != nil { - log.Println("[ERROR] Unknown in /api/listusers row scan:", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LISTUSERS-ROWSCAN"}) - return - } - space, err := getSpace(id) - if err != nil { - log.Println("[ERROR] Unknown in /api/listusers getSpace():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LISTUSERS-GETSPACE"}) - return - } - notes, err := getNoteCount(id) - if err != nil { - log.Println("[ERROR] Unknown in /api/listusers getNoteCount():", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-LISTUSERS-GETNOTECOUNT"}) - return - } - users = append(users, map[string]interface{}{"id": id, "username": username, "created": created, "space": space, "notes": notes}) - } - if err := rows.Err(); err != nil { - log.Println("[ERROR] Unknown in /api/listusers row iteration:", err) - } - } - }) - + // 3 second timeout + timeoutChan := make(chan struct{}) go func() { - for { - time.Sleep(time.Minute) - affected, err := mem.Exec("DELETE FROM spent WHERE expires < ?", time.Now().Unix()) + time.Sleep(3 * time.Second) + logFunc("Timeout while waiting for the quota from the blob storage service", 2, information) + close(timeoutChan) + }() + + // Wait for the response + select { + case response := <-information.Inbox: + return response, nil + case <-timeoutChan: + return library.InterServiceMessage{}, errors.New("timeout") + } +} + +func getQuotaOrUsed(userID uuid.UUID, information library.ServiceInitializationInformation, context uint64) (int64, error) { + response, err := askBlobService(userID, information, context) + if err != nil { + return 0, err + } else if response.MessageType != 0 { + return 0, response.Message.(error) + } else { + return response.Message.(int64), nil + } +} + +func getQuota(userID uuid.UUID, information library.ServiceInitializationInformation) (int64, error) { + return getQuotaOrUsed(userID, information, 3) +} + +func getUsed(userID uuid.UUID, information library.ServiceInitializationInformation) (int64, error) { + return getQuotaOrUsed(userID, information, 4) +} + +func deleteNote(userID uuid.UUID, noteID uuid.UUID, information library.ServiceInitializationInformation) error { + response, err := askBlobService(nucleusLibrary.File{ + User: userID, + Name: noteID.String(), + }, information, 2) + if err != nil { + return err + } + + if response.MessageType != 0 { + return response.Message.(error) + } else { + return nil + } +} + +func modifyNote(userID uuid.UUID, noteID uuid.UUID, data []byte, information library.ServiceInitializationInformation) error { + response, err := askBlobService(nucleusLibrary.File{ + User: userID, + Name: noteID.String(), + Bytes: data, + }, information, 0) + if err != nil { + return err + } + + if response.MessageType != 0 { + return response.Message.(error) + } else { + return nil + } +} + +func getNote(userID uuid.UUID, noteID uuid.UUID, information library.ServiceInitializationInformation) (*os.File, error) { + response, err := askBlobService(nucleusLibrary.File{ + User: userID, + Name: noteID.String(), + }, information, 1) + if err != nil { + return nil, err + } + + if response.MessageType != 0 { + return nil, response.Message.(error) + } else { + return response.Message.(*os.File), nil + } +} + +func renderProtobuf(statusCode int, w http.ResponseWriter, protobuf proto.Message, information library.ServiceInitializationInformation) { + w.WriteHeader(statusCode) + data, err := proto.Marshal(protobuf) + if err != nil { + logFunc(err.Error(), 2, information) + } + w.Header().Add("Content-Type", "application/x-protobuf") + _, err = w.Write(data) + if err != nil { + logFunc(err.Error(), 2, information) + } +} + +func verifyJWT(token string, publicKey ed25519.PublicKey) (jwt.MapClaims, error) { + parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + return publicKey, nil + }) + if err != nil { + return nil, err + } + + if !parsedToken.Valid { + return nil, errors.New("invalid token") + } + + claims, ok := parsedToken.Claims.(jwt.MapClaims) + if !ok { + return nil, errors.New("invalid token") + } + + // Check if the token expired + date, err := claims.GetExpirationTime() + if err != nil || date.Before(time.Now()) || claims["sub"] == nil || claims["isOpenID"] == nil || claims["isOpenID"].(bool) { + return claims, errors.New("invalid token") + } + + return claims, nil +} + +func getUsername(token string, oauthHostName string, publicKey ed25519.PublicKey) (string, string, error) { + // Verify the JWT + _, err := verifyJWT(token, publicKey) + if err != nil { + return "", "", err + } + + // Get the user's information + var responseData struct { + Username string `json:"username"` + Sub string `json:"sub"` + } + request, err := http.NewRequest("GET", oauthHostName+"/api/oauth/userinfo", nil) + request.Header.Set("Authorization", "Bearer "+token) + response, err := http.DefaultClient.Do(request) + if err != nil { + return "", "", err + } + + if response.StatusCode != 200 || response.Body == nil || response.Body == http.NoBody { + return "", "", errors.New("invalid response") + } + + err = json.NewDecoder(response.Body).Decode(&responseData) + if err != nil { + return "", "", err + } + + return responseData.Sub, responseData.Username, nil +} + +func Main(information library.ServiceInitializationInformation) *chi.Mux { + var conn library.Database + hostName := information.Configuration["hostName"].(string) + + // Initiate a connection to the database + // Call service ID 1 to get the database connection information + information.Outbox <- library.InterServiceMessage{ + ServiceID: ServiceInformation.ServiceID, + ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), // Service initialization service + MessageType: 1, // Request connection information + SentAt: time.Now(), + Message: nil, + } + + // Wait for the response + response := <-information.Inbox + if response.MessageType == 2 { + // This is the connection information + // Set up the database connection + conn = response.Message.(library.Database) + if conn.DBType == library.Sqlite { + // Create the users table + _, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BLOB NOT NULL UNIQUE, publicKey BLOB NOT NULL, USERNAME TEXT NOT NULL)") if err != nil { - log.Println("[ERROR] Unknown in spent cleanup Exec():", err) - } else { - affectedRows, err := affected.RowsAffected() - if err != nil { - log.Println("[ERROR] Unknown in spent cleanup RowsAffected():", err) - } else { - log.Println("[INFO] Spent cleanup complete, deleted " + strconv.FormatInt(affectedRows, 10) + " rows") - } + logFunc(err.Error(), 3, information) } + // Create the notes table + _, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS notes (id BLOB NOT NULL UNIQUE, userID BLOB NOT NULL, title BLOB NOT NULL, titleIV BLOB NOT NULL)") + if err != nil { + logFunc(err.Error(), 3, information) + } + } else { + // Create the users table + _, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BYTEA NOT NULL UNIQUE, publicKey BYTEA NOT NULL, USERNAME TEXT NOT NULL)") + if err != nil { + logFunc(err.Error(), 3, information) + } + // Create the notes table + _, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS notes (id BYTEA NOT NULL UNIQUE, userID BYTEA NOT NULL, title BYTEA NOT NULL, titleIV BYTEA NOT NULL)") + if err != nil { + logFunc(err.Error(), 3, information) + } + } + } else { + // This is an error message + // Log the error message to the logger service + logFunc(response.Message.(error).Error(), 3, information) + } + + // Ask the authentication service for the public key + information.Outbox <- library.InterServiceMessage{ + ServiceID: ServiceInformation.ServiceID, + ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service + MessageType: 2, // Request public key + SentAt: time.Now(), + Message: nil, + } + + var publicKey ed25519.PublicKey = nil + + // 3 second timeout + go func() { + time.Sleep(3 * time.Second) + if publicKey == nil { + logFunc("Timeout while waiting for the public key from the authentication service", 3, information) } }() - log.Println("[INFO] Server started") - log.Println("[INFO] Welcome to Burgernotes! Today we are running on IP " + host + " on port " + strconv.Itoa(port) + ".") - err = router.Run(host + ":" + strconv.Itoa(port)) - if err != nil { - log.Fatalln("[FATAL] Server failed to begin operations") + // Wait for the response + response = <-information.Inbox + if response.MessageType == 2 { + // This is the public key + publicKey = response.Message.(ed25519.PublicKey) + } else { + // This is an error message + // Log the error message to the logger service + logFunc(response.Message.(error).Error(), 3, information) } + + // Ask the authentication service for the OAuth host name + information.Outbox <- library.InterServiceMessage{ + ServiceID: ServiceInformation.ServiceID, + ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service + MessageType: 0, // Request OAuth host name + SentAt: time.Now(), + Message: nil, + } + + var oauthHostName string + + // 3 second timeout + go func() { + time.Sleep(3 * time.Second) + if oauthHostName == "" { + logFunc("Timeout while waiting for the OAuth host name from the authentication service", 3, information) + } + }() + + // Wait for the response + response = <-information.Inbox + if response.MessageType == 0 { + // This is the OAuth host name + oauthHostName = response.Message.(string) + } else { + // This is an error message + // Log the error message to the logger service + logFunc(response.Message.(error).Error(), 3, information) + } + + // Ask the authentication service to create a new OAuth2 client + urlPath, err := url.JoinPath(hostName, "/oauth") + if err != nil { + logFunc(err.Error(), 3, information) + } + + information.Outbox <- library.InterServiceMessage{ + ServiceID: ServiceInformation.ServiceID, + ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service + MessageType: 1, // Create OAuth2 client + SentAt: time.Now(), + Message: nucleusLibrary.OAuthInformation{ + Name: "Data Tracker", + RedirectUri: urlPath, + KeyShareUri: "", + Scopes: []string{"openid"}, + }, + } + + oauthResponse := nucleusLibrary.OAuthResponse{} + + // 3 second timeout + go func() { + time.Sleep(3 * time.Second) + if oauthResponse == (nucleusLibrary.OAuthResponse{}) { + logFunc("Timeout while waiting for the OAuth response from the authentication service", 3, information) + } + }() + + // Wait for the response + response = <-information.Inbox + switch response.MessageType { + case 0: + // Success, set the OAuth response + oauthResponse = response.Message.(nucleusLibrary.OAuthResponse) + logFunc("Initialized with App ID: "+oauthResponse.AppID, 0, information) + case 1: + // An error which is their fault + logFunc(response.Message.(error).Error(), 3, information) + case 2: + // An error which is our fault + logFunc(response.Message.(error).Error(), 3, information) + default: + // An unknown error + logFunc("Unknown error", 3, information) + } + + // Set up the router + router := chi.NewRouter() + + // Set up the static routes + staticDir, err := fs.Sub(information.ResourceDir, "static") + if err != nil { + logFunc(err.Error(), 3, information) + } else { + router.Handle("/bgn-static/*", http.StripPrefix("/bgn-static/", http.FileServerFS(staticDir))) + } + + // Set up the routes + router.Post("/api/notes/add", func(w http.ResponseWriter, r *http.Request) { + var requestData protobuf.Token + err := unmarshalProtobuf(r, &requestData) + + // Verify the JWT + claims, err := verifyJWT(requestData.Token, publicKey) + if err != nil { + renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information) + return + } + + // Generate a new note UUID + noteID := uuid.New() + + // Check if the user has reached their quota + quota, err := getQuota(uuid.MustParse(claims["sub"].(string)), information) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x02}}, information) + return + } + + used, err := getUsed(uuid.MustParse(claims["sub"].(string)), information) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x03}}, information) + return + } + + if used >= quota { + renderProtobuf(403, w, &protobuf.Error{Error: "Quota reached"}, information) + return + } + + // Try to insert the note into the database + _, err = conn.DB.Exec("INSERT INTO notes (id, userID) VALUES ($1, $2)", noteID, claims["sub"]) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x04}}, information) + } else { + noteIdBytes, err := noteID.MarshalBinary() + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x05}}, information) + } + renderProtobuf(200, w, &protobuf.NoteID{NoteId: noteIdBytes}, information) + } + }) + + router.Post("/api/notes/remove", func(w http.ResponseWriter, r *http.Request) { + var requestData protobuf.NoteRequest + err := unmarshalProtobuf(r, &requestData) + if err != nil { + renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information) + return + } + + // Verify the JWT + claims, err := verifyJWT(requestData.Token.String(), publicKey) + if err != nil { + renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information) + return + } + + // Try to remove the note from the database + _, err = conn.DB.Exec("DELETE FROM notes WHERE id = $1 AND userID = $2", requestData.NoteId, claims["sub"]) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x06}}, information) + } + + // If it's there, try to remove the note from the blob storage + err = deleteNote(uuid.MustParse(claims["sub"].(string)), uuid.Must(uuid.FromBytes(requestData.NoteId.GetNoteId())), information) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x07}}, information) + } + + w.WriteHeader(200) + }) + + router.Post("/api/notes/list", func(w http.ResponseWriter, r *http.Request) { + // Verify the JWT + var requestData protobuf.Token + err := unmarshalProtobuf(r, &requestData) + if err != nil { + renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information) + return + } + + claims, err := verifyJWT(requestData.Token, publicKey) + if err != nil { + renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information) + return + } + + // Try to get the notes from the database + rows, err := conn.DB.Query("SELECT id, title, titleIV FROM notes WHERE userID = $1", claims["sub"]) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x08}}, information) + return + } + + // Create the notes list + var notes protobuf.ApiNotesListResponse + + // Iterate through the rows + for rows.Next() { + var title, titleIV, noteID []byte + err = rows.Scan(¬eID, &title, &titleIV) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x09}}, information) + return + } + + // Append the note to the list + notes.Notes = append(notes.Notes, &protobuf.NoteMetadata{ + NoteId: &protobuf.NoteID{ + NoteId: noteID, + }, + Title: &protobuf.AESData{ + Data: title, + Iv: titleIV, + }, + }) + } + + // Render the notes list + renderProtobuf(200, w, ¬es, information) + }) + + router.Post("/api/notes/get", func(w http.ResponseWriter, r *http.Request) { + var requestData protobuf.NoteRequest + err := unmarshalProtobuf(r, &requestData) + if err != nil { + renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information) + return + } + + // Verify the JWT + claims, err := verifyJWT(requestData.Token.String(), publicKey) + if err != nil { + renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information) + return + } + + // Try to get the note from the database + var title, titleIV []byte + err = conn.DB.QueryRow("SELECT title, titleIV FROM notes WHERE id = $1 AND userID = $2", requestData.NoteId, claims["sub"]).Scan(&title, &titleIV) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x0A}}, information) + return + } + + // Get the note from the blob storage + noteFile, err := getNote(uuid.MustParse(claims["sub"].(string)), uuid.Must(uuid.FromBytes(requestData.NoteId.GetNoteId())), information) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x0B}}, information) + return + } + + // The IV is the first 16 bytes of the file + iv := make([]byte, 16) + _, err = noteFile.Read(iv) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x0C}}, information) + return + } + + // The rest of the file is the data + data, err := io.ReadAll(noteFile) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x0D}}, information) + return + } + + // Close the file + err = noteFile.Close() + if err != nil { + logFunc("Resource leak in /api/notes/get", 2, information) + } + + // Render the note + renderProtobuf(200, w, &protobuf.Note{ + Note: &protobuf.AESData{ + Data: data, + Iv: iv, + }, + Metadata: &protobuf.NoteMetadata{ + NoteId: requestData.NoteId, + Title: &protobuf.AESData{ + Data: title, + Iv: titleIV, + }, + }, + }, information) + }) + + router.Post("/api/notes/edit", func(w http.ResponseWriter, r *http.Request) { + var requestData protobuf.ApiNotesEditRequest + err := unmarshalProtobuf(r, &requestData) + if err != nil { + renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information) + return + } + + // Verify the JWT + claims, err := verifyJWT(requestData.Token.String(), publicKey) + if err != nil { + renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information) + return + } + + // Update the title + _, err = conn.DB.Exec("UPDATE notes SET title = $1, titleIV = $2 WHERE id = $3 AND userID = $4", requestData.Note.Metadata.Title.Data, requestData.Note.Metadata.Title.Iv, requestData.Note.Metadata.NoteId, claims["sub"]) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x0E}}, information) + return + } + + // Edit the note in the blob storage + err = modifyNote(uuid.MustParse(claims["sub"].(string)), uuid.Must(uuid.FromBytes(requestData.Note.Metadata.NoteId.GetNoteId())), bytes.Join([][]byte{requestData.Note.Note.Iv, requestData.Note.Note.Data}, nil), information) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x0F}}, information) + return + } + + w.WriteHeader(200) + }) + + router.Post("/api/notes/purge", func(w http.ResponseWriter, r *http.Request) { + var requestData protobuf.Token + err := unmarshalProtobuf(r, &requestData) + if err != nil { + renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information) + return + } + + // Verify the JWT + claims, err := verifyJWT(requestData.Token, publicKey) + if err != nil { + renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information) + return + } + + // Get the notes from the database + rows, err := conn.DB.Query("SELECT id FROM notes WHERE userID = $1", claims["sub"]) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x10}}, information) + return + } + + // Iterate through the rows + for rows.Next() { + var noteID []byte + err = rows.Scan(¬eID) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x11}}, information) + return + } + + // Try to remove the note from the blob storage + err = deleteNote(uuid.MustParse(claims["sub"].(string)), uuid.Must(uuid.FromBytes(noteID)), information) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x12}}, information) + return + } + } + + // Remove the notes from the database + _, err = conn.DB.Exec("DELETE FROM notes WHERE userID = $1", claims["sub"]) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x13}}, information) + return + } + }) + + router.Post("/api/signup", func(w http.ResponseWriter, r *http.Request) { + var requestData protobuf.ApiSignupRequest + err := unmarshalProtobuf(r, &requestData) + if err != nil { + renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information) + return + } + + // Verify the JWT + sub, username, err := getUsername(requestData.Token.String(), oauthHostName, publicKey) + if err != nil { + renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information) + return + } + + // Try to insert the user into the database + _, err = conn.DB.Exec("INSERT INTO users (id, publicKey, username) VALUES ($1, $2, $3)", sub, requestData.PublicKey, username) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE") { + renderProtobuf(409, w, &protobuf.Error{Error: "User already exists"}, information) + } else { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x01}}, information) + } + return + } + + w.WriteHeader(200) + }) + + router.Post("/api/delete", func(w http.ResponseWriter, r *http.Request) { + var requestData protobuf.Token + err := unmarshalProtobuf(r, &requestData) + if err != nil { + renderProtobuf(400, w, &protobuf.Error{Error: "Invalid request"}, information) + return + } + + // Verify the JWT + claims, err := verifyJWT(requestData.Token, publicKey) + if err != nil { + renderProtobuf(403, w, &protobuf.Error{Error: "Invalid token"}, information) + return + } + + // Try to remove the user from the database + _, err = conn.DB.Exec("DELETE FROM users WHERE id = $1", claims["sub"]) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x14}}, information) + return + } + + // Get the notes from the database + rows, err := conn.DB.Query("SELECT id FROM notes WHERE userID = $1", claims["sub"]) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x15}}, information) + return + } + + // Iterate through the rows + for rows.Next() { + var noteID []byte + err = rows.Scan(¬eID) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x16}}, information) + return + } + + // Try to remove the note from the blob storage + err = deleteNote(uuid.MustParse(claims["sub"].(string)), uuid.Must(uuid.FromBytes(noteID)), information) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x17}}, information) + return + } + } + + // Remove the notes from the database + _, err = conn.DB.Exec("DELETE FROM notes WHERE userID = $1", claims["sub"]) + if err != nil { + renderProtobuf(500, w, &protobuf.ServerError{ErrorCode: []byte{0x18}}, information) + return + } + + w.WriteHeader(200) + }) + + // TODO: Implement shared notes + + return router } diff --git a/main.proto b/main.proto new file mode 100644 index 0000000..c391138 --- /dev/null +++ b/main.proto @@ -0,0 +1,142 @@ +syntax = "proto3"; +package main; +option go_package = "git.ailur.dev/ailur/burgernotes/protobuf"; + +// Token is a string that represents an OAuth2 JWT token. +message Token { + string token = 1; +} + +// NoteID is a UUID that represents a note. +message NoteID { + bytes noteId = 1; +} + +// NoteID and Token together represent a request involving a note. +message NoteRequest { + NoteID noteId = 1; + Token token = 2; +} + +// AESData represents AES-encrypted data. +message AESData { + bytes data = 2; + bytes iv = 3; +} + +// NoteMetadata represents the metadata of a note. +message NoteMetadata { + NoteID noteId = 1; + AESData title = 2; +} + +// Note represents a note. +message Note { + NoteMetadata metadata = 1; + AESData note = 2; +} + +// /api/notes/list returns an array of notes. +message ApiNotesListResponse { + repeated NoteMetadata notes = 1; +} + +// /api/notes/edit accepts a note and a token. +message ApiNotesEditRequest { + Note note = 1; + Token token = 2; +} + +// /api/signup accepts a public key and a token. +message ApiSignupRequest { + bytes publicKey = 1; + Token token = 2; +} + +// /api/invite/prepare accepts an username and a token. +message ApiInvitePrepareRequest { + string username = 1; + Token token = 2; +} + +// /api/invite/prepare returns a ECDH key. +message ApiInvitePrepareResponse { + bytes ecdhExchange = 1; +} + +// /api/invite/send accepts a ECDH exchange, a AES-encrypted key and a NoteRequest. +message ApiInviteSendRequest { + bytes ecdhExchange = 1; + AESData key = 2; + NoteRequest noteRequest = 3; +} + +// Invitation represents an invitation to a note. +message Invitation { + string username = 1; + AESData key = 2; + NoteID noteId = 3; +} + +// /api/invite/check returns an array of invitations. +message ApiInviteCheckResponse { + repeated Invitation invitations = 1; +} + +// /api/invite/link accepts a NoteRequest, UNIX timestamp and a singleUse boolean. +message ApiInviteLinkRequest { + NoteRequest noteRequest = 1; + int64 timestamp = 2; + bool singleUse = 3; +} + +// /api/invite/link returns an invite code. +message ApiInviteLinkResponse { + bytes inviteCode = 1; +} + +// /api/invite/accept accepts an invite code and a token. +message ApiInviteAcceptRequest { + bytes inviteCode = 1; + Token token = 2; +} + +// /api/shared is a WebSocket which accepts an array of line numbers and a token. +message ApiSharedRequest { + repeated uint64 lines = 1; + Token token = 2; +} + +// User represents a user editing notes. +message UserLines { + string username = 1; + bytes uuid = 2; + repeated uint64 lines = 3; +} + +// /api/shared is a WebSocket which returns the array of line numbers for each user working on a note. +message ApiSharedResponse { + repeated UserLines users = 1; +} + +// /api/shared/edit accepts multiple lines, represented as an individual AESData, and a token. +message ApiSharedEditRequest { + repeated AESData lines = 1; + Token token = 2; +} + +// /api/shared/get returns the lines of a note. +message ApiSharedGetResponse { + repeated AESData lines = 1; + NoteMetadata metadata = 2; +} + +// Error represents an error. +message Error { + string error = 1; +} + +// ServerError represents a 500 error, with a hex error code. +message ServerError { + bytes errorCode = 1; +} \ No newline at end of file diff --git a/schema.sql b/schema.sql deleted file mode 100644 index 51022d1..0000000 --- a/schema.sql +++ /dev/null @@ -1,27 +0,0 @@ -DROP TABLE IF EXISTS users; -DROP TABLE IF EXISTS notes; -DROP TABLE IF EXISTS oauth; - -CREATE TABLE users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created TEXT NOT NULL, - username TEXT NOT NULL, - password TEXT NOT NULL, - migrated INTEGER NOT NULL DEFAULT 0 -); - -CREATE TABLE notes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - creator INTEGER NOT NULL, - created TEXT NOT NULL, - edited TEXT NOT NULL, - content TEXT NOT NULL, - title TEXT NOT NULL -); - -CREATE TABLE oauth ( - id INTEGER NOT NULL, - oauthProvider TEXT NOT NULL, - encryptedPasswd TEXT NOT NULL, - UNIQUE (id, oauthProvider) -) \ No newline at end of file