Self-hosted, federated location sharing app and server that prioritizes user privacy and security
end-to-end-encryption location-sharing privacy self-hosted federated

Compare changes

Choose any two refs to compare.

+34
app/.gitignore
··· 1 + # dependencies (bun install) 2 + node_modules 3 + 4 + # output 5 + out 6 + dist 7 + *.tgz 8 + 9 + # code coverage 10 + coverage 11 + *.lcov 12 + 13 + # logs 14 + logs 15 + _.log 16 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 + 18 + # dotenv environment variable files 19 + .env 20 + .env.development.local 21 + .env.test.local 22 + .env.production.local 23 + .env.local 24 + 25 + # caches 26 + .eslintcache 27 + .cache 28 + *.tsbuildinfo 29 + 30 + # IntelliJ based IDEs 31 + .idea 32 + 33 + # Finder (MacOS) folder config 34 + .DS_Store
+15 -1
app/README.md
··· 1 - - in src-tauri/src/lib.rs, we directly create the webpage with home-page as it's starting point so that "logging in" (seeing if the user id is set) is checked before the webview window is even created. This is fine since we are only checking a single value 1 + # privacypin 2 + 3 + To install dependencies: 4 + 5 + ```bash 6 + bun install 7 + ``` 8 + 9 + To run: 10 + 11 + ```bash 12 + bun run 13 + ``` 14 + 15 + This project was created using `bun init` in bun v1.3.3. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
+26 -17
app/bun.lock
··· 5 5 "": { 6 6 "name": "privacypin", 7 7 "dependencies": { 8 - "@tauri-apps/api": "^2.9.0", 9 - "@tauri-apps/plugin-opener": "^2.5.2", 8 + "@tauri-apps/api": "^2", 9 + "@tauri-apps/plugin-opener": "^2", 10 10 "@tauri-apps/plugin-store": "^2.4.1", 11 11 "alpinejs": "^3.15.1", 12 12 }, 13 13 "devDependencies": { 14 - "@tauri-apps/cli": "^2.9.4", 14 + "@tauri-apps/cli": "^2", 15 15 "@types/alpinejs": "^3.13.11", 16 - "typescript": "~5.6.3", 17 - "vite": "^6.4.1", 16 + "@types/bun": "latest", 17 + "typescript": "~5.6.2", 18 + "vite": "^6.0.3", 18 19 }, 19 20 }, 20 21 }, ··· 117 118 118 119 "@tauri-apps/api": ["@tauri-apps/api@2.9.0", "", {}, "sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw=="], 119 120 120 - "@tauri-apps/cli": ["@tauri-apps/cli@2.9.4", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.4", "@tauri-apps/cli-darwin-x64": "2.9.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.4", "@tauri-apps/cli-linux-arm64-gnu": "2.9.4", "@tauri-apps/cli-linux-arm64-musl": "2.9.4", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.4", "@tauri-apps/cli-linux-x64-gnu": "2.9.4", "@tauri-apps/cli-linux-x64-musl": "2.9.4", "@tauri-apps/cli-win32-arm64-msvc": "2.9.4", "@tauri-apps/cli-win32-ia32-msvc": "2.9.4", "@tauri-apps/cli-win32-x64-msvc": "2.9.4" }, "bin": { "tauri": "tauri.js" } }, "sha512-pvylWC9QckrOS9ATWXIXcgu7g2hKK5xTL5ZQyZU/U0n9l88SEFGcWgLQNa8WZmd+wWIOWhkxOFcOl3i6ubDNNw=="], 121 + "@tauri-apps/cli": ["@tauri-apps/cli@2.9.3", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.3", "@tauri-apps/cli-darwin-x64": "2.9.3", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.3", "@tauri-apps/cli-linux-arm64-gnu": "2.9.3", "@tauri-apps/cli-linux-arm64-musl": "2.9.3", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.3", "@tauri-apps/cli-linux-x64-gnu": "2.9.3", "@tauri-apps/cli-linux-x64-musl": "2.9.3", "@tauri-apps/cli-win32-arm64-msvc": "2.9.3", "@tauri-apps/cli-win32-ia32-msvc": "2.9.3", "@tauri-apps/cli-win32-x64-msvc": "2.9.3" }, "bin": { "tauri": "tauri.js" } }, "sha512-BQ7iLUXTQcyG1PpzLWeVSmBCedYDpnA/6Cm/kRFGtqjTf/eVUlyYO5S2ee07tLum3nWwDBWTGFZeruO8yEukfA=="], 121 122 122 - "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9rHkMVtbMhe0AliVbrGpzMahOBg3rwV46JYRELxR9SN6iu1dvPOaMaiC4cP6M/aD1424ziXnnMdYU06RAH8oIw=="], 123 + "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W8FQXZXQmQ0Fmj9UJXNrm2mLdIaLLriKVY7o/FzmizyIKTPIvHjfZALTNybbpTQRbJvKoGHLrW1DNzAWVDWJYg=="], 123 124 124 - "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-VT9ymNuT06f5TLjCZW2hfSxbVtZDhORk7CDUDYiq5TiSYQdxkl8MVBy0CCFFcOk4QAkUmqmVUA9r3YZ/N/vPRQ=="], 125 + "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-zDwu40rlshijt3TU6aRvzPUyVpapsx1sNfOlreDMTaMelQLHl6YoQzSRpLHYwrHrhimxyX2uDqnKIiuGel0Lhg=="], 125 126 126 - "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.9.4", "", { "os": "linux", "cpu": "arm" }, "sha512-tTWkEPig+2z3Rk0zqZYfjUYcgD+aSm72wdrIhdYobxbQZOBw0zfn50YtWv+av7bm0SHvv75f0l7JuwgZM1HFow=="], 127 + "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.9.3", "", { "os": "linux", "cpu": "arm" }, "sha512-+Oc2OfcTRwYtW93VJqd/HOk77buORwC9IToj/qsEvM7bTMq6Kda4alpZprzwrCHYANSw+zD8PgjJdljTpe4p+g=="], 127 128 128 - "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-ql6vJ611qoqRYHxkKPnb2vHa27U+YRKRmIpLMMBeZnfFtZ938eao7402AQCH1mO2+/8ioUhbpy9R/ZcLTXVmkg=="], 129 + "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.9.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-59GqU/J1n9wFyAtleoQOaU0oVIo+kwQynEw4meFDoKRXszKGor6lTsbsS3r0QKLSPbc0o/yYGJhqqCtkYjb/eg=="], 129 130 130 - "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-vg7yNn7ICTi6hRrcA/6ff2UpZQP7un3xe3SEld5QM0prgridbKAiXGaCKr3BnUBx/rGXegQlD/wiLcWdiiraSw=="], 131 + "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.9.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-fzvG+jEn5/iYGNH6Z2IRMheYFC4pJdXa19BR9fFm6Bdn2cuajRLDKdUcEME/DCtwqclphXtFZTrT4oezY5vI/A=="], 131 132 132 - "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.9.4", "", { "os": "linux", "cpu": "none" }, "sha512-l8L+3VxNk6yv5T/Z/gv5ysngmIpsai40B9p6NQQyqYqxImqYX37pqREoEBl1YwG7szGnDibpWhidPrWKR59OJA=="], 133 + "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.9.3", "", { "os": "linux", "cpu": "none" }, "sha512-qV8DZXI/fZwawk6T3Th1g6smiNC2KeQTk7XFgKvqZ6btC01z3UTsQmNGvI602zwm3Ld1TBZb4+rEWu2QmQimmw=="], 133 134 134 - "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-PepPhCXc/xVvE3foykNho46OmCyx47E/aG676vKTVp+mqin5d+IBqDL6wDKiGNT5OTTxKEyNlCQ81Xs2BQhhqA=="], 135 + "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tquyEONCNRfqEBWEe4eAHnxFN5yY5lFkCuD4w79XLIovUxVftQ684+xLp7zkhntkt4y20SMj2AgJa/+MOlx4Kg=="], 135 136 136 - "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zcd1QVffh5tZs1u1SCKUV/V7RRynebgYUNWHuV0FsIF1MjnULUChEXhAhug7usCDq4GZReMJOoXa6rukEozWIw=="], 137 + "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-v2cBIB/6ji8DL+aiL5QUykU3ZO8OoJGyx50/qv2HQVzkf85KdaYSis3D/oVRemN/pcDz+vyCnnL3XnzFnDl4JQ=="], 137 138 138 - "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-/7ZhnP6PY04bEob23q8MH/EoDISdmR1wuNm0k9d5HV7TDMd2GGCDa8dPXA4vJuglJKXIfXqxFmZ4L+J+MO42+w=="], 139 + "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.9.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZGvBy7nvrHPbE0HeKp/ioaiw8bNgAHxWnb7JRZ4/G0A+oFj0SeSFxl9k5uU6FKnM7bHM23Gd1oeaDex9g5Fceg=="], 139 140 140 - "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.9.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-1LmAfaC4Cq+3O1Ir1ksdhczhdtFSTIV51tbAGtbV/mr348O+M52A/xwCCXQank0OcdBxy5BctqkMtuZnQvA8uQ=="], 141 + "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.9.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-UsgIwOnpCoY9NK9/65QiwgmWVIE80LE7SwRYVblGtmlY9RYfsYvpbItwsovA/AcHMTiO+OCvS/q9yLeqS3m6Sg=="], 141 142 142 - "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg=="], 143 + "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.3", "", { "os": "win32", "cpu": "x64" }, "sha512-fmw7NrrHE5m49idCvJAx9T9bsupjdJ0a3p3DPCNCZRGANU6R1tA1L+KTlVuUtdAldX2NqU/9UPo2SCslYKgJHQ=="], 143 144 144 145 "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="], 145 146 ··· 147 148 148 149 "@types/alpinejs": ["@types/alpinejs@3.13.11", "", {}, "sha512-3KhGkDixCPiLdL3Z/ok1GxHwLxEWqQOKJccgaQL01wc0EVM2tCTaqlC3NIedmxAXkVzt/V6VTM8qPgnOHKJ1MA=="], 149 150 151 + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], 152 + 150 153 "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 151 154 155 + "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], 156 + 152 157 "@vue/reactivity": ["@vue/reactivity@3.1.5", "", { "dependencies": { "@vue/shared": "3.1.5" } }, "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg=="], 153 158 154 159 "@vue/shared": ["@vue/shared@3.1.5", "", {}, "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="], 155 160 156 161 "alpinejs": ["alpinejs@3.15.1", "", { "dependencies": { "@vue/reactivity": "~3.1.1" } }, "sha512-HLO1TtiE92VajFHtLLPK8BWaK1YepV/uj31UrfoGnQ00lyFOJZ+oVY3F0DghPAwvg8sLU79pmjGQSytERa2gEg=="], 162 + 163 + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], 157 164 158 165 "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 159 166 ··· 176 183 "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], 177 184 178 185 "typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], 186 + 187 + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 179 188 180 189 "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], 181 190 }
+23 -22
app/package.json
··· 1 1 { 2 - "name": "privacypin", 3 - "private": true, 4 - "version": "0.1.0", 5 - "type": "module", 6 - "scripts": { 7 - "dev": "vite", 8 - "build": "tsc && vite build", 9 - "preview": "vite preview", 10 - "tauri": "WEBKIT_DISABLE_DMABUF_RENDERER=1 tauri" 11 - }, 12 - "dependencies": { 13 - "@tauri-apps/api": "^2.9.0", 14 - "@tauri-apps/plugin-opener": "^2.5.2", 15 - "@tauri-apps/plugin-store": "^2.4.1", 16 - "alpinejs": "^3.15.1" 17 - }, 18 - "devDependencies": { 19 - "@tauri-apps/cli": "^2.9.4", 20 - "@types/alpinejs": "^3.13.11", 21 - "typescript": "~5.6.3", 22 - "vite": "^6.4.1" 23 - } 2 + "name": "privacypin", 3 + "private": true, 4 + "version": "0.1.0", 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite", 8 + "build": "tsc && vite build", 9 + "preview": "vite preview", 10 + "tauri": "WEBKIT_DISABLE_DMABUF_RENDERER=1 tauri" 11 + }, 12 + "dependencies": { 13 + "@tauri-apps/api": "^2", 14 + "@tauri-apps/plugin-opener": "^2", 15 + "@tauri-apps/plugin-store": "^2.4.1", 16 + "alpinejs": "^3.15.1" 17 + }, 18 + "devDependencies": { 19 + "@tauri-apps/cli": "^2", 20 + "@types/alpinejs": "^3.13.11", 21 + "typescript": "~5.6.2", 22 + "vite": "^6.0.3", 23 + "@types/bun": "latest" 24 + } 24 25 }
-6
app/src/assets/ellipsis-vertical.svg
··· 1 - <?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> 2 - <svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 - <path d="M12 16C12.5523 16 13 16.4477 13 17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17C11 16.4477 11.4477 16 12 16Z" stroke="#464455" stroke-linecap="round" stroke-linejoin="round"/> 4 - <path d="M12 6C12.5523 6 13 6.44772 13 7C13 7.55228 12.5523 8 12 8C11.4477 8 11 7.55228 11 7C11 6.44772 11.4477 6 12 6Z" stroke="#464455" stroke-linecap="round" stroke-linejoin="round"/> 5 - <path d="M12 11C12.5523 11 13 11.4477 13 12C13 12.5523 12.5523 13 12 13C11.4477 13 11 12.5523 11 12C11 11.4477 11.4477 11 12 11Z" stroke="#464455" stroke-linecap="round" stroke-linejoin="round"/> 6 - </svg>
-4
app/src/assets/paperplane.svg
··· 1 - <?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> 2 - <svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 - <path d="M11 13L15.4564 8.5M11 13L6.38202 9.57695C5.7407 9.07229 5.94107 8.06115 6.72742 7.834L20 4L17.117 15.9189C16.9651 16.6489 16.0892 16.9637 15.5 16.5L13.5 15M11 13V18L13.5 15M11 13L13.5 15M7 20L9 18M4 19L8.5 14.5M4 15L6.5 12.5" stroke="#464455" stroke-linecap="round" stroke-linejoin="round"/> 4 - </svg>
-5
app/src/assets/pin-location.svg
··· 1 - <?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> 2 - <svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 - <path d="M17 10C17 11.7279 15.0424 14.9907 13.577 17.3543C12.8967 18.4514 12.5566 19 12 19C11.4434 19 11.1033 18.4514 10.423 17.3543C8.95763 14.9907 7 11.7279 7 10C7 7.23858 9.23858 5 12 5C14.7614 5 17 7.23858 17 10Z" stroke="#464455" stroke-linecap="round" stroke-linejoin="round"/> 4 - <path d="M14.5 10C14.5 11.3807 13.3807 12.5 12 12.5C10.6193 12.5 9.5 11.3807 9.5 10C9.5 8.61929 10.6193 7.5 12 7.5C13.3807 7.5 14.5 8.61929 14.5 10Z" stroke="#464455" stroke-linecap="round" stroke-linejoin="round"/> 5 - </svg>
+4
app/src/assets/pin.svg
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 + <path fill-rule="evenodd" clip-rule="evenodd" d="M3.37892 10.2236L8 16L12.6211 10.2236C13.5137 9.10788 14 7.72154 14 6.29266V6C14 2.68629 11.3137 0 8 0C4.68629 0 2 2.68629 2 6V6.29266C2 7.72154 2.4863 9.10788 3.37892 10.2236ZM8 8C9.10457 8 10 7.10457 10 6C10 4.89543 9.10457 4 8 4C6.89543 4 6 4.89543 6 6C6 7.10457 6.89543 8 8 8Z" fill="#000000"/> 4 + </svg>
+13
app/src/assets/qr.svg
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + 3 + <svg fill="#000000" width="800px" height="800px" viewBox="0 -0.09 122.88 122.88" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="enable-background:new 0 0 122.88 122.7" xml:space="preserve"> 4 + 5 + <style type="text/css">.st0{fill-rule:evenodd;clip-rule:evenodd;}</style> 6 + 7 + <g> 8 + 9 + <path class="st0" d="M0.18,0h44.63v44.45H0.18V0L0.18,0z M111.5,111.5h11.38v11.2H111.5V111.5L111.5,111.5z M89.63,111.48h11.38 v10.67H89.63h-0.01H78.25v-21.82h11.02V89.27h11.21V67.22h11.38v10.84h10.84v11.2h-10.84v11.2h-11.21h-0.17H89.63V111.48 L89.63,111.48z M55.84,89.09h11.02v-11.2H56.2v-11.2h10.66v-11.2H56.02v11.2H44.63v-11.2h11.2V22.23h11.38v33.25h11.02v11.2h10.84 v-11.2h11.38v11.2H89.63v11.2H78.25v22.05H67.22v22.23H55.84V89.09L55.84,89.09z M111.31,55.48h11.38v11.2h-11.38V55.48 L111.31,55.48z M22.41,55.48h11.38v11.2H22.41V55.48L22.41,55.48z M0.18,55.48h11.38v11.2H0.18V55.48L0.18,55.48z M55.84,0h11.38 v11.2H55.84V0L55.84,0z M0,78.06h44.63v44.45H0V78.06L0,78.06z M10.84,88.86h22.95v22.86H10.84V88.86L10.84,88.86z M78.06,0h44.63 v44.45H78.06V0L78.06,0z M88.91,10.8h22.95v22.86H88.91V10.8L88.91,10.8z M11.02,10.8h22.95v22.86H11.02V10.8L11.02,10.8z"/> 10 + 11 + </g> 12 + 13 + </svg>
-4
app/src/assets/scan-qr.svg
··· 1 - <?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> 2 - <svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 - <path d="M7.55556 4H5C4.44771 4 4 4.44772 4 5V7.55556M16.4444 4H19C19.5523 4 20 4.44772 20 5V7.55556M20 16.4444V19C20 19.5523 19.5523 20 19 20H16.4444M7.55556 20H5C4.44771 20 4 19.5523 4 19V16.4444M5.77778 12.8889H6.66667M8.44444 12.8889H9.33333M5.77778 11H10.1111C10.6634 11 11.1111 10.5523 11.1111 10V5.77778M12.8889 5.77778V11.1111M16.4444 11H18.2222M14.6667 11H15.1111M13.7778 12.8889H15.1111M17 12.8889H18.2222M18.2222 15H15.5556M15.5556 16.8889V18.2222M13.7778 15V18.2222M12 18.2222V12.8889H11.1111M10.2222 14.6667V18.2222M18.2222 17.7778V17.7778C18.2222 17.5323 18.0232 17.3333 17.7778 17.3333V17.3333C17.5323 17.3333 17.3333 17.5323 17.3333 17.7778V17.7778C17.3333 18.0232 17.5323 18.2222 17.7778 18.2222V18.2222C18.0232 18.2222 18.2222 18.0232 18.2222 17.7778ZM18.2222 6.77778V8.33333C18.2222 8.88562 17.7745 9.33333 17.2222 9.33333H15.6667C15.1144 9.33333 14.6667 8.88562 14.6667 8.33333V6.77778C14.6667 6.22549 15.1144 5.77778 15.6667 5.77778H17.2222C17.7745 5.77778 18.2222 6.22549 18.2222 6.77778ZM6.77778 9.33333H8.33333C8.88562 9.33333 9.33333 8.88562 9.33333 8.33333V6.77778C9.33333 6.22549 8.88562 5.77778 8.33333 5.77778H6.77778C6.22549 5.77778 5.77778 6.22549 5.77778 6.77778V8.33333C5.77778 8.88562 6.22549 9.33333 6.77778 9.33333ZM7.44444 18.2222H6.77778C6.22549 18.2222 5.77778 17.7745 5.77778 17.2222V15.6667C5.77778 15.1144 6.22549 14.6667 6.77778 14.6667H7.44444C7.99673 14.6667 8.44444 15.1144 8.44444 15.6667V17.2222C8.44444 17.7745 7.99673 18.2222 7.44444 18.2222Z" stroke="#464455" stroke-linecap="round" stroke-linejoin="round"/> 4 - </svg>
-5
app/src/assets/setting.svg
··· 1 - <?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> 2 - <svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 - <path d="M10.4 5.6C10.4 4.84575 10.4 4.46863 10.6343 4.23431C10.8686 4 11.2458 4 12 4C12.7542 4 13.1314 4 13.3657 4.23431C13.6 4.46863 13.6 4.84575 13.6 5.6V6.6319C13.9725 6.74275 14.3287 6.8913 14.6642 7.07314L15.3942 6.34315C15.9275 5.80982 16.1942 5.54315 16.5256 5.54315C16.8569 5.54315 17.1236 5.80982 17.6569 6.34315C18.1903 6.87649 18.4569 7.14315 18.4569 7.47452C18.4569 7.80589 18.1903 8.07256 17.6569 8.60589L16.9269 9.33591C17.1087 9.67142 17.2573 10.0276 17.3681 10.4H18.4C19.1542 10.4 19.5314 10.4 19.7657 10.6343C20 10.8686 20 11.2458 20 12C20 12.7542 20 13.1314 19.7657 13.3657C19.5314 13.6 19.1542 13.6 18.4 13.6H17.3681C17.2573 13.9724 17.1087 14.3286 16.9269 14.6641L17.6569 15.3941C18.1902 15.9275 18.4569 16.1941 18.4569 16.5255C18.4569 16.8569 18.1902 17.1235 17.6569 17.6569C17.1236 18.1902 16.8569 18.4569 16.5255 18.4569C16.1942 18.4569 15.9275 18.1902 15.3942 17.6569L14.6642 16.9269C14.3286 17.1087 13.9724 17.2573 13.6 17.3681V18.4C13.6 19.1542 13.6 19.5314 13.3657 19.7657C13.1314 20 12.7542 20 12 20C11.2458 20 10.8686 20 10.6343 19.7657C10.4 19.5314 10.4 19.1542 10.4 18.4V17.3681C10.0276 17.2573 9.67142 17.1087 9.33591 16.9269L8.60598 17.6569C8.07265 18.1902 7.80598 18.4569 7.47461 18.4569C7.14324 18.4569 6.87657 18.1902 6.34324 17.6569C5.80991 17.1235 5.54324 16.8569 5.54324 16.5255C5.54324 16.1941 5.80991 15.9275 6.34324 15.3941L7.07314 14.6642C6.8913 14.3287 6.74275 13.9725 6.6319 13.6H5.6C4.84575 13.6 4.46863 13.6 4.23431 13.3657C4 13.1314 4 12.7542 4 12C4 11.2458 4 10.8686 4.23431 10.6343C4.46863 10.4 4.84575 10.4 5.6 10.4H6.6319C6.74275 10.0276 6.8913 9.67135 7.07312 9.33581L6.3432 8.60589C5.80987 8.07256 5.5432 7.80589 5.5432 7.47452C5.5432 7.14315 5.80987 6.87648 6.3432 6.34315C6.87654 5.80982 7.1432 5.54315 7.47457 5.54315C7.80594 5.54315 8.07261 5.80982 8.60594 6.34315L9.33588 7.07308C9.6714 6.89128 10.0276 6.74274 10.4 6.6319V5.6Z" stroke="#464455" stroke-linecap="round" stroke-linejoin="round"/> 4 - <path d="M14.4 12C14.4 13.3255 13.3255 14.4 12 14.4C10.6745 14.4 9.6 13.3255 9.6 12C9.6 10.6745 10.6745 9.6 12 9.6C13.3255 9.6 14.4 10.6745 14.4 12Z" stroke="#464455" stroke-linecap="round" stroke-linejoin="round"/> 5 - </svg>
-4
app/src/assets/user+.svg
··· 1 - <?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> 2 - <svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 - <path d="M20 3V5M20 5V7M20 5H22M20 5H18M16 8C16 10.2091 14.2091 12 12 12C9.79086 12 8 10.2091 8 8C8 5.79086 9.79086 4 12 4C14.2091 4 16 5.79086 16 8ZM9.31765 14H14.6824C15.1649 14 15.4061 14 15.6219 14.0461C16.3688 14.2056 17.0147 14.7661 17.3765 15.569C17.4811 15.8009 17.5574 16.0765 17.71 16.6278C17.8933 17.2901 17.985 17.6213 17.9974 17.8884C18.0411 18.8308 17.5318 19.6817 16.7756 19.9297C16.5613 20 16.2714 20 15.6916 20H8.30844C7.72864 20 7.43875 20 7.22441 19.9297C6.46818 19.6817 5.95888 18.8308 6.00261 17.8884C6.01501 17.6213 6.10668 17.2901 6.29003 16.6278C6.44262 16.0765 6.51891 15.8009 6.62346 15.569C6.9853 14.7661 7.63116 14.2056 8.37806 14.0461C8.59387 14 8.83513 14 9.31765 14Z" stroke="#464455" stroke-linecap="round" stroke-linejoin="round"/> 4 - </svg>
-24
app/src/home-page/home.css
··· 1 - .container-sl { 2 - display: flex; 3 - justify-content: space-between; /* This will align the elements on opposite sides of the container */ 4 - } 5 - 6 - .element-al, 7 - .element-ar { 8 - flex: 1; /* Make the elements take up equal width */ 9 - } 10 - 11 - .element-al { 12 - text-align: left; 13 - } 14 - 15 - .element-ar { 16 - text-align: right; 17 - } 18 - 19 1 body { 20 2 font-family: sans-serif; 21 3 background: #f9fafb; ··· 27 9 .app { 28 10 width: 100%; 29 11 background: #f9fafb; 30 - } 31 - 32 - .svg-icon { 33 - width: 25px; 34 - height: 25px; 35 - margin: auto; 36 12 } 37 13 38 14 header {
+12 -33
app/src/home-page/home.html
··· 12 12 <header> 13 13 <h1>PrivacyPin</h1> 14 14 <div> 15 - <!-- <@azom.dev> somehow the "+" emoji does not display in the code for me, but it's temporary anyways --> 16 - <!-- <@kishka.cc> we will need to replace these with svgs, as it's the font that messes up the emoji --> 17 - <button class="icon-btn" @click="updateServer()"> 18 - <img 19 - class="svg-icon" 20 - src="/src/assets/paperplane.svg" 21 - alt="Paperplane Flying Icon" 22 - /> 23 - </button> 24 - <button class="icon-btn" @click="addFriend()"> 25 - <img 26 - class="svg-icon" 27 - src="/src/assets/user+.svg" 28 - alt="Friend Add Icon" 29 - /> 30 - </button> 31 - <button class="icon-btn" @click="openSettings()"> 32 - <img 33 - class="svg-icon" 34 - src="/src/assets/setting.svg" 35 - alt="Settings Icon" 36 - /> 15 + <!-- somehow the "+" emoji does not display in the code for me, but it's temporary anyways --> 16 + <button class="icon-btn" @click="addFriend()">โž•</button> 17 + <button class="icon-btn" @click="goto('settings')"> 18 + โš™๏ธ 37 19 </button> 38 20 </div> 39 21 </header> ··· 65 47 @click="viewLocation(friend.id)" 66 48 > 67 49 <img 68 - class="svg-icon" 69 - src="/src/assets/pin-location.svg" 50 + src="/src/assets/pin.svg" 70 51 alt="Pin Icon" 71 52 />View 72 53 </button> 73 - <a 54 + <span 74 55 class="menu-icon" 75 - style="margin-bottom: auto" 76 56 @click="friendOptions(friend.id)" 77 - > 78 - <img 79 - class="svg-icon" 80 - src="/src/assets/ellipsis-vertical.svg" 81 - alt="Options menu" 82 - /> 83 - </a> 57 + ></span> 58 + <img 59 + class="menu-icon" 60 + src="/src/assets/three-dots.svg" 61 + alt="Pin Icon" 62 + /> 84 63 </div> 85 64 </div> 86 65 </template>
+10 -8
app/src/home-page/home.ts
··· 1 1 import Alpine from "alpinejs"; 2 - import { goto } from "../utils/tools.ts"; 3 2 import { Store } from "../utils/store.ts"; 4 - import * as api from "../utils/api.ts"; 3 + import { post } from "../utils/api.ts"; 5 4 6 5 Alpine.data("homePageState", () => ({ 7 6 friends: [ ··· 23 22 alert(`Options for friend id ${friend_id}`); 24 23 }, 25 24 26 - async updateServer() { 27 - await api.sendPings("123", "3.14159N 3.14159W"); 28 - }, 29 - 30 25 addFriend() { 31 26 alert("Add friend functionality would open here"); 32 27 }, 33 28 34 29 openSettings() { 35 - goto("settings"); 30 + alert("Settings would open here"); 36 31 }, 37 32 38 33 async generateSignupKey() { 39 - this.newSignupKey = await api.generateSignupKey(); 34 + const res = await post("generate-signup-key", undefined); 35 + this.newSignupKey = res; 36 + console.log(res); 40 37 }, 41 38 42 39 isAdmin() { 43 40 return Store.get("is_admin"); 41 + }, 42 + 43 + goto(newLocation: string) { 44 + window.location.href = 45 + "/src/" + newLocation + "-page/" + newLocation + ".html"; 44 46 }, 45 47 })); 46 48
+3 -3
app/src/settings-page/settings.css
··· 96 96 background: #1d4ed8; 97 97 } 98 98 99 - .btn-secondary { 99 + .btn-qr { 100 100 background: white; 101 101 gap: 0.5rem; 102 102 border: 1px solid #d1d5db; 103 103 } 104 104 105 - .btn-secondary:hover { 105 + .btn-qr:hover { 106 106 background: #f3f4f6; 107 107 } 108 108 109 - .btn-secondary img { 109 + .btn-qr img { 110 110 width: 16px; 111 111 height: 16px; 112 112 }
+4 -2
app/src/settings-page/settings.html
··· 8 8 9 9 <body> 10 10 <div class="card"> 11 + <!-- x-data connects this element to the settingsPageState Alpine component, enabling its data (serverAddress and signupKey) and functions (signup and scanQR) to work within it :) --> 12 + <!-- TODO: make this a form instead? --> 11 13 <div class="actions" x-data="settingsPageState"> 12 14 <h3>Settings</h3> 13 15 14 - <button class="btn-primary" @click="goto('home')"> 16 + <button class="btn-secondary" @click="goto('home')"> 15 17 Back to Home 16 18 </button> 17 19 18 - <button class="btn-secondary" @click="await debugLogout()"> 20 + <button class="btn-secondary" @click="resetStore()"> 19 21 Signout 20 22 </button> 21 23 </div>
+3 -2
app/src/settings-page/settings.ts
··· 3 3 import { goto } from "../utils/tools.ts"; 4 4 5 5 Alpine.data("settingsPageState", () => ({ 6 - async debugLogout() { 7 - await Store.reset(); 6 + resetStore() { 7 + Store.reset(); 8 + alert("Store reset"); 8 9 goto("signup"); 9 10 }, 10 11 goto(newLocation: string) {
+3 -3
app/src/signup-page/signup.css
··· 34 34 } 35 35 36 36 .icon-circle img { 37 - width: 48px; 38 - height: 48px; 37 + width: 32px; 38 + height: 32px; 39 39 } 40 40 41 41 h1 { ··· 107 107 } 108 108 109 109 .btn-qr img { 110 - width: 20px; 110 + width: 16px; 111 111 height: 16px; 112 112 } 113 113
+7 -34
app/src/signup-page/signup.html
··· 10 10 <div class="card"> 11 11 <div class="header"> 12 12 <div class="icon-circle"> 13 - <img src="/src/assets/pin-location.svg" alt="Pin Icon" /> 13 + <img src="/src/assets/pin.svg" alt="Pin Icon" /> 14 14 </div> 15 15 <h1>PrivacyPin</h1> 16 16 <p>Connect with a server to start sharing</p> ··· 21 21 <div class="actions" x-data="signupPageState"> 22 22 <div> 23 23 <label for="server">Server Address</label> 24 - <input 25 - id="server" 26 - type="url" 27 - placeholder="https://your-server.com" 28 - x-model="serverAddress" 29 - required 30 - /> 24 + <input id="server" type="url" placeholder="https://your-server.com" x-model="serverAddress" required /> 31 25 </div> 32 26 33 27 <div> 34 28 <label for="key">Signup Key</label> 35 - <input 36 - id="key" 37 - type="password" 38 - placeholder="Enter your signup key" 39 - x-model="signupKey" 40 - required 41 - /> 29 + <input id="key" type="password" placeholder="Enter your signup key" x-model="signupKey" required /> 42 30 </div> 43 31 44 - <p class="hint"> 45 - Scan a QR code to automatically fill both server address and 46 - signup key 47 - </p> 48 - <button 49 - type="button" 50 - x-bind:disabled="isDoingStuff" 51 - class="btn-qr" 52 - @click="await scanQR()" 53 - > 54 - <img src="/src/assets/scan-qr.svg" alt="QR Icon" /> 32 + <p class="hint">Scan a QR code to automatically fill both server address and signup key</p> 33 + <button type="button" x-bind:disabled="isDoingStuff" class="btn-qr" @click="await scanQR()"> 34 + <img src="/src/assets/qr.svg" alt="QR Icon" /> 55 35 Scan QR Code 56 36 </button> 57 37 58 - <button 59 - class="btn-primary" 60 - x-bind:disabled="isDoingStuff" 61 - @click="await signup()" 62 - > 63 - <span x-show="isDoingStuff">Connecting...</span> 64 - <span x-show="!isDoingStuff">Connect</span> 65 - </button> 38 + <button class="btn-primary" x-bind:disabled="isDoingStuff" @click="await signup()"><span x-show="isDoingStuff">Connecting...</span> <span x-show="!isDoingStuff">Connect</span></button> 66 39 </div> 67 40 </div> 68 41 </body>
+5 -4
app/src/signup-page/signup.ts
··· 1 1 import Alpine from "alpinejs"; 2 2 import { createAccount } from "../utils/api.ts"; 3 + import { Store } from "../utils/store.ts"; 3 4 4 5 Alpine.data("signupPageState", () => ({ 5 6 serverAddress: "", ··· 8 9 9 10 async signup() { 10 11 this.isDoingStuff = true; 11 - await new Promise((resolve) => setTimeout(resolve, 1000)); // temp 12 + await new Promise((resolve) => setTimeout(resolve, 2000)); // temp 12 13 try { 13 - await createAccount(this.serverAddress, this.signupKey); 14 + const res = await createAccount(this.serverAddress, this.signupKey); 15 + Store.set("is_admin", res.is_admin); 16 + Store.set("user_id", res.user_id); 14 17 window.location.href = "/src/home-page/home.html"; 15 18 } catch (e) { 16 - const err = e instanceof Error ? e.message : e; 17 - alert(`Sign-up failed: ${err}`); 18 19 this.isDoingStuff = false; 19 20 } 20 21 },
-53
app/src/types.d.ts
··· 1 - // THIS FILE IS TEMPORARY UNTIL WE CAN HAVE A TYPESCRIPT VERSION THAT INCLUDE THE BASE64 <-> UINT8ARRAY CONVERSION STUFF 2 - 3 - declare global { 4 - interface Uint8Array { 5 - /** 6 - * Converts this `Uint8Array` to a Base64 or Base64URL encoded string. 7 - * 8 - * @param options Optional configuration: 9 - * - `alphabet`: Selects between `"base64"` (default) and `"base64url"` alphabets. 10 - * - `omitPadding`: If true, omits the trailing `=` padding characters. 11 - * 12 - * @returns The Base64-encoded representation of the byte array. 13 - * 14 - * @example 15 - * ```ts 16 - * const bytes = new Uint8Array([72, 101, 108, 108, 111]); 17 - * console.log(bytes.toBase64()); // "SGVsbG8=" 18 - * ``` 19 - */ 20 - toBase64(options?: { alphabet?: "base64" | "base64url"; omitPadding?: boolean }): string; 21 - } 22 - 23 - interface Uint8ArrayConstructor { 24 - /** 25 - * Creates a `Uint8Array` from a Base64 or Base64URL encoded string. 26 - * 27 - * @param base64 The input string to decode. 28 - * @param options Optional configuration: 29 - * - `alphabet`: Selects between `"base64"` (default) and `"base64url"` alphabets. 30 - * - `lastChunkHandling`: Controls how to handle incomplete input: 31 - * - `"strict"` (default): Throws an error if input is not valid Base64. 32 - * - `"loose"`: Tolerates missing padding or invalid trailing characters. 33 - * - `"stop-before-partial"`: Ignores an incomplete trailing chunk. 34 - * 35 - * @returns A new `Uint8Array` containing the decoded bytes. 36 - * 37 - * @example 38 - * ```ts 39 - * const bytes = Uint8Array.fromBase64("SGVsbG8="); 40 - * console.log(new TextDecoder().decode(bytes)); // "Hello" 41 - * ``` 42 - */ 43 - fromBase64( 44 - base64: string, 45 - options?: { 46 - alphabet?: "base64" | "base64url"; 47 - lastChunkHandling?: "loose" | "strict" | "stop-before-partial"; 48 - }, 49 - ): Uint8Array; 50 - } 51 - } 52 - 53 - export {};
+80 -67
app/src/utils/api.ts
··· 1 1 import { Store } from "./store.ts"; 2 2 3 3 function bufToBase64(buf: ArrayBuffer): string { 4 - return new Uint8Array(buf).toBase64(); 4 + return btoa(String.fromCharCode(...new Uint8Array(buf))); 5 5 } 6 6 7 - /** 8 - * This function can throw an error 9 - */ 10 - export async function createAccount(server_url: string, signup_key: string): Promise<{ user_id: string; is_admin: boolean }> { 11 - const keyPair = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]); 12 - const pubKeyRaw = await crypto.subtle.exportKey("raw", keyPair.publicKey); 13 - const privKeyRaw = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey); 14 - const pub_key_b64 = bufToBase64(pubKeyRaw); 7 + function strToBytes(str: string): Uint8Array { 8 + return new TextEncoder().encode(str); 9 + } 15 10 16 - const response = await fetch(server_url + "/create-account", { 17 - method: "POST", 18 - headers: { "Content-Type": "application/json" }, 19 - body: JSON.stringify({ signup_key, pub_key_b64 }), 20 - }); 11 + export async function createAccount( 12 + server_url: string, 13 + signup_key: string, 14 + ): Promise<{ user_id: string; is_admin: boolean }> { 15 + try { 16 + await Store.set("server_url", server_url); 17 + const keyPair = await crypto.subtle.generateKey("Ed25519", true, [ 18 + "sign", 19 + "verify", 20 + ]); 21 + const pubKeyRaw = await crypto.subtle.exportKey("raw", keyPair.publicKey); 22 + const privKeyRaw = await crypto.subtle.exportKey( 23 + "pkcs8", 24 + keyPair.privateKey, 25 + ); 26 + const pub_key_b64 = bufToBase64(pubKeyRaw); 21 27 22 - if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); 23 - const json = await response.json(); 28 + const response = await fetch(server_url + "/create-account", { 29 + method: "POST", 30 + headers: { "Content-Type": "application/json" }, 31 + body: JSON.stringify({ signup_key, pub_key_b64 }), 32 + }); 24 33 25 - // TODO validate data? 34 + if (!response.ok) throw new Error(await response.text()); 35 + const json = await response.json(); 26 36 27 - await Store.set("server_url", server_url); 28 - await Store.set("user_id", json.user_id); 29 - await Store.set("is_admin", json.is_admin); 30 - await Store.set("priv_key", bufToBase64(privKeyRaw)); 37 + await Store.set("user_id", json.user_id); 38 + await Store.set("priv_key", bufToBase64(privKeyRaw)); 31 39 32 - return json; 40 + return json; 41 + } catch (err) { 42 + alert(`${err}`); 43 + throw err; 44 + } 33 45 } 34 46 35 - // this api is laughably vulnerable to a replay attack currently, but not later with key chaining 36 - export async function generateSignupKey(): Promise<string> { 37 - return await post("generate-signup-key"); 38 - } 47 + export async function post( 48 + endpoint: string, 49 + data: object | string | undefined, 50 + ): Promise<any> { 51 + try { 52 + const user_id = await Store.get("user_id"); 53 + const server_url = await Store.get("server_url"); 54 + console.log(`Exhibit B: ${server_url}`); 55 + const privKey_b64 = await Store.get("priv_key"); 39 56 40 - export async function requestFriendRequest(friend_id: string): Promise<void> { 41 - await post("request-friend-request", friend_id); 42 - } 57 + if (!user_id || !privKey_b64) throw new Error("Missing user credentials"); 43 58 44 - export async function isFriendRequestAccepted(friend_id: string): Promise<boolean> { 45 - const res = await post("is-friend-request-accepted", friend_id); 46 - return res === "true"; 47 - } 59 + // Prepare request body bytes 60 + let bodyStr = ""; 61 + if (typeof data === "object") bodyStr = JSON.stringify(data); 62 + else if (typeof data === "string") bodyStr = data; 63 + const bodyBytes = strToBytes(bodyStr); 48 64 49 - export async function sendPings(friend_id: string, ping: string): Promise<void> { 50 - // later, accept a list of friend ids, but anyways this specific api won't stay in typescript for long since it needs to be run in the background 51 - await post("send-pings", JSON.stringify([{ receiver_id: friend_id, encrypted_ping: ping }])); 52 - } 65 + // Import private key and sign 66 + const privKeyBytes = Uint8Array.from(atob(privKey_b64), (c) => 67 + c.charCodeAt(0), 68 + ); 53 69 54 - export async function getPings(friend_id: string): Promise<string[]> { 55 - const res = await post("get-pings", friend_id); 56 - return JSON.parse(res); 57 - } 70 + const privKey = await crypto.subtle.importKey( 71 + "pkcs8", 72 + privKeyBytes.buffer, 73 + { name: "Ed25519" }, 74 + false, 75 + ["sign"], 76 + ); 58 77 59 - /** 60 - * This function can throw an error 61 - */ 62 - async function post(endpoint: string, body: string | undefined = undefined) { 63 - const user_id = await Store.get("user_id"); 64 - const server_url = await Store.get("server_url"); 65 - const privKey_b64 = await Store.get("priv_key"); 78 + const signature = await crypto.subtle.sign("Ed25519", privKey, bodyBytes); 79 + const signature_b64 = bufToBase64(signature); 66 80 67 - const bodyBytes = new TextEncoder().encode(body === undefined ? "" : body); 68 - 69 - const privKeyBytes = Uint8Array.fromBase64(privKey_b64); 70 - 71 - const privKey = await crypto.subtle.importKey("pkcs8", privKeyBytes.buffer, "Ed25519", false, ["sign"]); 72 - 73 - const signature = await crypto.subtle.sign("Ed25519", privKey, bodyBytes); 74 - const signature_b64 = bufToBase64(signature); 81 + // Encode header JSON to base64 82 + const authJson = JSON.stringify({ user_id, signature: signature_b64 }); 83 + const authHeader = btoa(authJson); 75 84 76 - const headers = { 77 - "x-auth": JSON.stringify({ user_id, signature: signature_b64 }), 78 - "Content-Type": "application/json", // TODO: not always json tho, but does it matter? 79 - }; 85 + const headers: Record<string, string> = { 86 + "x-auth": authHeader, 87 + }; 88 + if (typeof data === "object") headers["Content-Type"] = "application/json"; 80 89 81 - const res = await fetch(`${server_url}/${endpoint}`, { 82 - method: "POST", 83 - headers, 84 - body: bodyBytes, // TODO: do we need to send bodyBytes instead to match server side auth? 85 - }); 90 + const res = await fetch(`${server_url}/${endpoint}`, { 91 + method: "POST", 92 + headers, 93 + body: bodyStr.length > 0 ? bodyStr : undefined, 94 + }); 86 95 87 - if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`); 88 - return await res.text(); 96 + if (!res.ok) throw new Error(await res.text()); 97 + return await res.text(); 98 + } catch (err) { 99 + alert(`${err}`); 100 + throw err; 101 + } 89 102 }
+44
app/src/utils/tools.ts
··· 2 2 window.location.href = 3 3 "/src/" + newLocation + "-page/" + newLocation + ".html"; 4 4 } 5 + 6 + /* 7 + 8 + Use this type of function to toggle dark mode. It CAN be modified to your needs. copy the function, and fix the end comment(be sure to put this in the alpine section) 9 + 10 + toggleDarkMode() { 11 + /* 12 + This toggles darkmode for 'body' in the css file | use for only document types 13 + document.body.classList.toggle("dark-theme"); 14 + 15 + this toggles darkmode for '.app' in the css file | use if it isn't a document type 16 + toggleStyle("app", "dark-theme"); 17 + 18 + * / 19 + 20 + document.body.classList.toggle("dark-theme"); 21 + toggleStyle("header", "dark-theme"); 22 + toggleStyle([".app", ".friend-card", ".content"], "dark-theme"); 23 + }, 24 + */ 25 + 26 + export function toggleStyle(classNames: string | string[], newClass: string) { 27 + if (typeof classNames === "string") { 28 + for ( 29 + let i = 0; 30 + i < document.getElementsByClassName(classNames).length; 31 + i++ 32 + ) { 33 + document.getElementsByClassName(classNames)[i].classList.toggle(newClass); 34 + } 35 + } else { 36 + for (let i = 0; i < classNames.length; i++) { 37 + for ( 38 + let j = 0; 39 + j < document.getElementsByClassName(classNames[i]).length; 40 + j++ 41 + ) { 42 + document 43 + .getElementsByClassName(classNames[i]) 44 + [j].classList.toggle(newClass); 45 + } 46 + } 47 + } 48 + }
+4 -4
app/src-tauri/src/lib.rs
··· 1 - use std::path::PathBuf; 2 - use tauri::{WebviewUrl, WebviewWindowBuilder}; 3 - use tauri_plugin_store::StoreBuilder; 4 - 5 1 // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ 6 2 #[tauri::command] 7 3 fn greet(name: &str) -> String { 8 4 format!("Hello, {}! You've been greeted from Rust!", name) 9 5 } 6 + 7 + use std::path::PathBuf; 8 + use tauri::{WebviewUrl, WebviewWindowBuilder}; 9 + use tauri_plugin_store::StoreBuilder; 10 10 11 11 #[cfg_attr(mobile, tauri::mobile_entry_point)] 12 12 pub fn run() {
+60 -190
server/Cargo.lock
··· 3 3 version = 4 4 4 5 5 [[package]] 6 - name = "aho-corasick" 7 - version = "1.1.4" 8 - source = "registry+https://github.com/rust-lang/crates.io-index" 9 - checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 10 - dependencies = [ 11 - "memchr", 12 - ] 13 - 14 - [[package]] 15 6 name = "atomic-waker" 16 7 version = "1.1.2" 17 8 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 19 10 20 11 [[package]] 21 12 name = "axum" 22 - version = "0.8.8" 13 + version = "0.8.6" 23 14 source = "registry+https://github.com/rust-lang/crates.io-index" 24 - checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" 15 + checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" 25 16 dependencies = [ 26 17 "axum-core", 27 18 "bytes", ··· 52 43 53 44 [[package]] 54 45 name = "axum-core" 55 - version = "0.5.6" 46 + version = "0.5.5" 56 47 source = "registry+https://github.com/rust-lang/crates.io-index" 57 - checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" 48 + checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" 58 49 dependencies = [ 59 50 "bytes", 60 51 "futures-core", ··· 92 83 93 84 [[package]] 94 85 name = "bytes" 95 - version = "1.11.0" 86 + version = "1.10.1" 96 87 source = "registry+https://github.com/rust-lang/crates.io-index" 97 - checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 88 + checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 98 89 99 90 [[package]] 100 91 name = "cfg-if" ··· 119 110 120 111 [[package]] 121 112 name = "crypto-common" 122 - version = "0.2.0-rc.8" 113 + version = "0.2.0-rc.5" 123 114 source = "registry+https://github.com/rust-lang/crates.io-index" 124 - checksum = "e6165b8029cdc3e765b74d3548f85999ee799d5124877ce45c2c85ca78e4d4aa" 115 + checksum = "919bd05924682a5480aec713596b9e2aabed3a0a6022fab6847f85a99e5f190a" 125 116 dependencies = [ 126 117 "hybrid-array", 127 118 ] 128 119 129 120 [[package]] 130 121 name = "curve25519-dalek" 131 - version = "5.0.0-pre.3" 122 + version = "5.0.0-pre.1" 132 123 source = "registry+https://github.com/rust-lang/crates.io-index" 133 - checksum = "92419e1cdc506051ffd30713ad09d0ec6a24bba9197e12989de389e35b19c77a" 124 + checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7" 134 125 dependencies = [ 135 126 "cfg-if", 136 127 "cpufeatures", ··· 155 146 156 147 [[package]] 157 148 name = "der" 158 - version = "0.8.0-rc.10" 149 + version = "0.8.0-rc.9" 159 150 source = "registry+https://github.com/rust-lang/crates.io-index" 160 - checksum = "02c1d73e9668ea6b6a28172aa55f3ebec38507131ce179051c8033b5c6037653" 151 + checksum = "e9d8dd2f26c86b27a2a8ea2767ec7f9df7a89516e4794e54ac01ee618dda3aa4" 161 152 dependencies = [ 162 153 "const-oid", 163 154 ] 164 155 165 156 [[package]] 166 157 name = "digest" 167 - version = "0.11.0-rc.5" 158 + version = "0.11.0-rc.4" 168 159 source = "registry+https://github.com/rust-lang/crates.io-index" 169 - checksum = "ebf9423bafb058e4142194330c52273c343f8a5beb7176d052f0e73b17dd35b9" 160 + checksum = "ea390c940e465846d64775e55e3115d5dc934acb953de6f6e6360bc232fe2bf7" 170 161 dependencies = [ 171 162 "block-buffer", 172 163 "crypto-common", ··· 184 175 185 176 [[package]] 186 177 name = "ed25519-dalek" 187 - version = "3.0.0-pre.3" 178 + version = "3.0.0-pre.1" 188 179 source = "registry+https://github.com/rust-lang/crates.io-index" 189 - checksum = "5d6d275a4ffdfc16e98fbcb5f5417214a06957c7cdc6eb2815c2dc50dce1c1dd" 180 + checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1" 190 181 dependencies = [ 191 182 "curve25519-dalek", 192 183 "ed25519", ··· 196 187 ] 197 188 198 189 [[package]] 199 - name = "errno" 200 - version = "0.3.14" 190 + name = "fiat-crypto" 191 + version = "0.3.0" 201 192 source = "registry+https://github.com/rust-lang/crates.io-index" 202 - checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 203 - dependencies = [ 204 - "libc", 205 - "windows-sys 0.61.2", 206 - ] 193 + checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" 207 194 208 195 [[package]] 209 - name = "fiat-crypto" 210 - version = "0.3.0" 196 + name = "fnv" 197 + version = "1.0.7" 211 198 source = "registry+https://github.com/rust-lang/crates.io-index" 212 - checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" 199 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 213 200 214 201 [[package]] 215 202 name = "form_urlencoded" ··· 266 253 267 254 [[package]] 268 255 name = "http" 269 - version = "1.4.0" 256 + version = "1.3.1" 270 257 source = "registry+https://github.com/rust-lang/crates.io-index" 271 - checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" 258 + checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 272 259 dependencies = [ 273 260 "bytes", 261 + "fnv", 274 262 "itoa", 275 263 ] 276 264 ··· 320 308 321 309 [[package]] 322 310 name = "hyper" 323 - version = "1.8.1" 311 + version = "1.7.0" 324 312 source = "registry+https://github.com/rust-lang/crates.io-index" 325 - checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 313 + checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" 326 314 dependencies = [ 327 315 "atomic-waker", 328 316 "bytes", ··· 341 329 342 330 [[package]] 343 331 name = "hyper-util" 344 - version = "0.1.19" 332 + version = "0.1.17" 345 333 source = "registry+https://github.com/rust-lang/crates.io-index" 346 - checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" 334 + checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" 347 335 dependencies = [ 348 336 "bytes", 349 337 "futures-core", ··· 357 345 358 346 [[package]] 359 347 name = "itoa" 360 - version = "1.0.17" 348 + version = "1.0.15" 361 349 source = "registry+https://github.com/rust-lang/crates.io-index" 362 - checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 363 - 364 - [[package]] 365 - name = "lazy_static" 366 - version = "1.5.0" 367 - source = "registry+https://github.com/rust-lang/crates.io-index" 368 - checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 350 + checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 369 351 370 352 [[package]] 371 353 name = "libc" 372 - version = "0.2.178" 354 + version = "0.2.177" 373 355 source = "registry+https://github.com/rust-lang/crates.io-index" 374 - checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" 356 + checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 375 357 376 358 [[package]] 377 359 name = "lock_api" ··· 384 366 385 367 [[package]] 386 368 name = "log" 387 - version = "0.4.29" 369 + version = "0.4.28" 388 370 source = "registry+https://github.com/rust-lang/crates.io-index" 389 - checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 390 - 391 - [[package]] 392 - name = "matchers" 393 - version = "0.2.0" 394 - source = "registry+https://github.com/rust-lang/crates.io-index" 395 - checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" 396 - dependencies = [ 397 - "regex-automata", 398 - ] 371 + checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 399 372 400 373 [[package]] 401 374 name = "matchit" ··· 417 390 418 391 [[package]] 419 392 name = "mio" 420 - version = "1.1.1" 393 + version = "1.1.0" 421 394 source = "registry+https://github.com/rust-lang/crates.io-index" 422 - checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" 395 + checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" 423 396 dependencies = [ 424 397 "libc", 425 398 "wasi", ··· 433 406 checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" 434 407 dependencies = [ 435 408 "rand", 436 - ] 437 - 438 - [[package]] 439 - name = "nu-ansi-term" 440 - version = "0.50.3" 441 - source = "registry+https://github.com/rust-lang/crates.io-index" 442 - checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 443 - dependencies = [ 444 - "windows-sys 0.61.2", 445 409 ] 446 410 447 411 [[package]] ··· 512 476 513 477 [[package]] 514 478 name = "proc-macro2" 515 - version = "1.0.104" 479 + version = "1.0.103" 516 480 source = "registry+https://github.com/rust-lang/crates.io-index" 517 - checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" 481 + checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 518 482 dependencies = [ 519 483 "unicode-ident", 520 484 ] ··· 568 532 ] 569 533 570 534 [[package]] 571 - name = "regex-automata" 572 - version = "0.4.13" 573 - source = "registry+https://github.com/rust-lang/crates.io-index" 574 - checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 575 - dependencies = [ 576 - "aho-corasick", 577 - "memchr", 578 - "regex-syntax", 579 - ] 580 - 581 - [[package]] 582 - name = "regex-syntax" 583 - version = "0.8.8" 584 - source = "registry+https://github.com/rust-lang/crates.io-index" 585 - checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 586 - 587 - [[package]] 588 535 name = "rust-server" 589 536 version = "0.1.0" 590 537 dependencies = [ ··· 596 543 "serde_json", 597 544 "tokio", 598 545 "tower-http", 599 - "tracing", 600 - "tracing-subscriber", 601 546 ] 602 547 603 548 [[package]] ··· 611 556 612 557 [[package]] 613 558 name = "ryu" 614 - version = "1.0.22" 559 + version = "1.0.20" 615 560 source = "registry+https://github.com/rust-lang/crates.io-index" 616 - checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" 561 + checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 617 562 618 563 [[package]] 619 564 name = "scopeguard" ··· 659 604 660 605 [[package]] 661 606 name = "serde_json" 662 - version = "1.0.148" 607 + version = "1.0.145" 663 608 source = "registry+https://github.com/rust-lang/crates.io-index" 664 - checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" 609 + checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 665 610 dependencies = [ 666 611 "itoa", 667 612 "memchr", 613 + "ryu", 668 614 "serde", 669 615 "serde_core", 670 - "zmij", 671 616 ] 672 617 673 618 [[package]] ··· 705 650 ] 706 651 707 652 [[package]] 708 - name = "sharded-slab" 709 - version = "0.1.7" 710 - source = "registry+https://github.com/rust-lang/crates.io-index" 711 - checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 712 - dependencies = [ 713 - "lazy_static", 714 - ] 715 - 716 - [[package]] 717 653 name = "signal-hook-registry" 718 - version = "1.4.8" 654 + version = "1.4.6" 719 655 source = "registry+https://github.com/rust-lang/crates.io-index" 720 - checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 656 + checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 721 657 dependencies = [ 722 - "errno", 723 658 "libc", 724 659 ] 725 660 726 661 [[package]] 727 662 name = "signature" 728 - version = "3.0.0-rc.6" 663 + version = "3.0.0-rc.5" 729 664 source = "registry+https://github.com/rust-lang/crates.io-index" 730 - checksum = "597a96996ccff7dfa16f052bd995b4cecc72af22c35138738dc029f0ead6608d" 665 + checksum = "2a0251c9d6468f4ba853b6352b190fb7c1e405087779917c238445eb03993826" 731 666 732 667 [[package]] 733 668 name = "smallvec" ··· 762 697 763 698 [[package]] 764 699 name = "syn" 765 - version = "2.0.112" 700 + version = "2.0.109" 766 701 source = "registry+https://github.com/rust-lang/crates.io-index" 767 - checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" 702 + checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" 768 703 dependencies = [ 769 704 "proc-macro2", 770 705 "quote", ··· 778 713 checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 779 714 780 715 [[package]] 781 - name = "thread_local" 782 - version = "1.1.9" 783 - source = "registry+https://github.com/rust-lang/crates.io-index" 784 - checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 785 - dependencies = [ 786 - "cfg-if", 787 - ] 788 - 789 - [[package]] 790 716 name = "tokio" 791 717 version = "1.48.0" 792 718 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 832 758 833 759 [[package]] 834 760 name = "tower-http" 835 - version = "0.6.8" 761 + version = "0.6.6" 836 762 source = "registry+https://github.com/rust-lang/crates.io-index" 837 - checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 763 + checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 838 764 dependencies = [ 839 765 "bitflags", 840 766 "bytes", 841 767 "http", 842 - "http-body", 843 768 "pin-project-lite", 844 769 "tower-layer", 845 770 "tower-service", 846 - "tracing", 847 771 ] 848 772 849 773 [[package]] ··· 860 784 861 785 [[package]] 862 786 name = "tracing" 863 - version = "0.1.44" 787 + version = "0.1.41" 864 788 source = "registry+https://github.com/rust-lang/crates.io-index" 865 - checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 789 + checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 866 790 dependencies = [ 867 791 "log", 868 792 "pin-project-lite", 869 - "tracing-attributes", 870 793 "tracing-core", 871 794 ] 872 795 873 796 [[package]] 874 - name = "tracing-attributes" 875 - version = "0.1.31" 876 - source = "registry+https://github.com/rust-lang/crates.io-index" 877 - checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 878 - dependencies = [ 879 - "proc-macro2", 880 - "quote", 881 - "syn", 882 - ] 883 - 884 - [[package]] 885 797 name = "tracing-core" 886 - version = "0.1.36" 887 - source = "registry+https://github.com/rust-lang/crates.io-index" 888 - checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 889 - dependencies = [ 890 - "once_cell", 891 - "valuable", 892 - ] 893 - 894 - [[package]] 895 - name = "tracing-log" 896 - version = "0.2.0" 897 - source = "registry+https://github.com/rust-lang/crates.io-index" 898 - checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 899 - dependencies = [ 900 - "log", 901 - "once_cell", 902 - "tracing-core", 903 - ] 904 - 905 - [[package]] 906 - name = "tracing-subscriber" 907 - version = "0.3.22" 798 + version = "0.1.34" 908 799 source = "registry+https://github.com/rust-lang/crates.io-index" 909 - checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" 800 + checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 910 801 dependencies = [ 911 - "matchers", 912 - "nu-ansi-term", 913 802 "once_cell", 914 - "regex-automata", 915 - "sharded-slab", 916 - "smallvec", 917 - "thread_local", 918 - "tracing", 919 - "tracing-core", 920 - "tracing-log", 921 803 ] 922 804 923 805 [[package]] ··· 931 813 version = "1.0.22" 932 814 source = "registry+https://github.com/rust-lang/crates.io-index" 933 815 checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 934 - 935 - [[package]] 936 - name = "valuable" 937 - version = "0.1.1" 938 - source = "registry+https://github.com/rust-lang/crates.io-index" 939 - checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 940 816 941 817 [[package]] 942 818 name = "wasi" ··· 1035 911 1036 912 [[package]] 1037 913 name = "zerocopy" 1038 - version = "0.8.31" 914 + version = "0.8.27" 1039 915 source = "registry+https://github.com/rust-lang/crates.io-index" 1040 - checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" 916 + checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" 1041 917 dependencies = [ 1042 918 "zerocopy-derive", 1043 919 ] 1044 920 1045 921 [[package]] 1046 922 name = "zerocopy-derive" 1047 - version = "0.8.31" 923 + version = "0.8.27" 1048 924 source = "registry+https://github.com/rust-lang/crates.io-index" 1049 - checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" 925 + checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" 1050 926 dependencies = [ 1051 927 "proc-macro2", 1052 928 "quote", ··· 1058 934 version = "1.8.2" 1059 935 source = "registry+https://github.com/rust-lang/crates.io-index" 1060 936 checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 1061 - 1062 - [[package]] 1063 - name = "zmij" 1064 - version = "1.0.7" 1065 - source = "registry+https://github.com/rust-lang/crates.io-index" 1066 - checksum = "de9211a9f64b825911bdf0240f58b7a8dac217fe260fc61f080a07f61372fbd5"
+1 -3
server/Cargo.toml
··· 11 11 serde = { version = "1.0.228", features = ["derive"] } 12 12 serde_json = "1.0.145" 13 13 tokio = { version = "1.48.0", features = ["full"] } 14 - tower-http = {version="0.6.6", features=["cors", "trace"]} 15 - tracing = "0.1.44" 16 - tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } 14 + tower-http = {version="0.6.6", features=["cors"]}
-93
server/src/auth.rs
··· 1 - use axum::{body::Body, extract::State, http::Request, middleware::Next}; 2 - use base64::{Engine, prelude::BASE64_STANDARD}; 3 - use ed25519_dalek::Signature; 4 - 5 - use crate::{ 6 - ReqBail, SrvErr, 7 - types::{AppState, AuthData}, 8 - }; 9 - 10 - pub async fn auth_test( 11 - State(state): State<AppState>, 12 - req: Request<Body>, 13 - next: Next, 14 - ) -> Result<axum::response::Response, SrvErr> { 15 - let endpoint = req.uri().path().to_owned(); 16 - 17 - if endpoint != "/create-account" { 18 - // CURSED STUFF BEGIN 19 - let (parts, body) = req.into_parts(); 20 - let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); 21 - let new_body = Body::from(body_bytes.clone()); 22 - let mut req = Request::from_parts(parts, new_body); 23 - // CURSED STUFF END 24 - 25 - let auth_header = req 26 - .headers() 27 - .get("x-auth") 28 - .and_then(|v| v.to_str().ok()) 29 - .ok_or(SrvErr!("missing x-auth header"))?; 30 - 31 - let auth_data: AuthData = serde_json::from_str(auth_header) 32 - .map_err(|e| SrvErr!("failed to parse x-auth JSON", e))?; 33 - 34 - let users = state.users.lock().await; 35 - let user_id = auth_data.user_id; 36 - let user = users 37 - .iter() 38 - .find(|u| u.id == user_id) 39 - .ok_or(SrvErr!("User not found"))?; 40 - let verifying_key = user.pub_key.clone(); 41 - 42 - // NOTE (key chaining): 43 - // Do NOT drop the `users` lock until after both steps are complete: 44 - // 1) verify the request using the current stored key 45 - // 2) update the stored key to the next key from this request 46 - // 47 - // If we unlock in between, a replay/duplicate of the same request can race: 48 - // 49 - // - Request A reads pk_i and starts verifying 50 - // - Attacker replays A (same signature) while A is still verifying 51 - // - Replay gets verified against pk_i and proceeds to update pk -> pk_attacker 52 - // - Request A finishes and updates pk again, but the replay has already 53 - // been accepted and may have advanced the key to an attacker-chosen value 54 - // 55 - // Keeping the lock across "verify + update" makes the transition atomic. 56 - drop(users); 57 - 58 - //////////////////////////////////// 59 - //////////////////////////////////// unsure 60 - //////////////////////////////////// 61 - 62 - let sig_vec = BASE64_STANDARD 63 - .decode(&auth_data.signature) 64 - .map_err(|e| SrvErr!("base64 decode fail", e))?; 65 - let sig_bytes: [u8; 64] = sig_vec 66 - .try_into() 67 - .map_err(|e| SrvErr!("invalid signature length", e))?; 68 - let signature = Signature::from_bytes(&sig_bytes); 69 - 70 - if let Err(err) = verifying_key.verify_strict(&body_bytes, &signature) { 71 - panic!("Signature verification failed: {err}"); 72 - } 73 - 74 - //////////////////////////////////// 75 - //////////////////////////////////// 76 - //////////////////////////////////// 77 - 78 - // TODO: Make the endpoints as enums at some point 79 - if endpoint == "/generate-signup-key" { 80 - let admin_id = state.admin_id.lock().await; 81 - if admin_id.as_ref() != Some(&user_id) { 82 - ReqBail!("not allowed: admin only"); 83 - } 84 - } 85 - 86 - req.extensions_mut().insert(user_id); // pass user_id to the actual request handler, whatever it is, in handlers.rs 87 - *req.body_mut() = Body::from(body_bytes); 88 - 89 - return Ok(next.run(req).await); 90 - } 91 - 92 - return Ok(next.run(req).await); 93 - }
+52 -36
server/src/handlers.rs
··· 3 3 use ed25519_dalek::VerifyingKey; 4 4 use nanoid::nanoid; 5 5 6 - use crate::{ReqBail, SrvErr, types::*}; 6 + use crate::types::*; 7 + 8 + macro_rules! my_err { 9 + ($msg:expr) => { 10 + Err(MyErr($msg)) 11 + }; 12 + } 7 13 8 14 pub async fn create_user( 9 15 State(state): State<AppState>, // TODO: some time ago, I change this (and all other handlers) to State(state): State<Arc<AppState>> no idea if I actually need it 10 16 Json(payload): Json<CreateUserRequest>, 11 - ) -> Result<Json<CreateAccountResponse>, SrvErr> { 17 + ) -> Result<Json<CreateAccountResponse>, MyErr> { 12 18 let key_used = { state.signup_keys.lock().await.remove(&payload.signup_key) }; 13 19 14 20 if !key_used { 15 - ReqBail!("Signup key was not there"); 21 + return my_err!("Signup key was not there"); 16 22 } 17 23 18 24 // todo check 19 25 let pub_key_bytes = match BASE64_STANDARD.decode(&payload.pub_key_b64) { 20 26 Ok(b) => b, 21 - Err(_) => ReqBail!("Invalid base64 public key"), 27 + Err(_) => return my_err!("Invalid base64 public key"), 22 28 }; 23 29 24 30 // todo check 25 - let pub_key_arr: [u8; 32] = pub_key_bytes 26 - .as_slice() 27 - .try_into() 28 - .map_err(|_| SrvErr!("Invalid pubkey length"))?; 29 - let pub_key = VerifyingKey::from_bytes(&pub_key_arr) 30 - .map_err(|e| SrvErr!("Invalid public key bytes", e))?; 31 + let pub_key = match VerifyingKey::from_bytes( 32 + &pub_key_bytes 33 + .try_into() 34 + .map_err(|_| MyErr("Invalid pubkey length".into()))?, 35 + ) { 36 + Ok(pk) => pk, 37 + Err(_) => return my_err!("Invalid public key bytes"), 38 + }; 31 39 32 40 let user_id = nanoid!(5); 33 41 let mut is_admin = false; ··· 46 54 return Ok(Json(CreateAccountResponse { user_id, is_admin })); 47 55 } 48 56 49 - pub async fn generate_signup_key(State(state): State<AppState>) -> Result<String, SrvErr> { 57 + pub async fn generate_signup_key(State(state): State<AppState>) -> Result<String, MyErr> { 50 58 let new_signup_key = nanoid!(5); 51 59 let mut signup_keys = state.signup_keys.lock().await; 52 60 ··· 56 64 return Ok(new_signup_key); 57 65 } 58 66 59 - pub async fn request_friend_request( 67 + pub async fn create_friend_request( 60 68 State(state): State<AppState>, 61 69 Extension(user_id): Extension<String>, 62 - friend_id: String, 63 - ) -> Result<(), SrvErr> { 64 - if friend_id == user_id { 65 - ReqBail!("Cannot friend yourself"); 70 + accepter_id: String, 71 + ) -> Result<(), MyErr> { 72 + if accepter_id == user_id { 73 + return my_err!("Cannot friend yourself"); 66 74 } 67 75 68 76 let mut friend_requests = state.friend_requests.lock().await; 69 - let link = Link::new(friend_id, user_id); 77 + let link = Link::new(accepter_id, user_id); 78 + if friend_requests.contains(&link) { 79 + return my_err!("Friend request already exists"); 80 + } 81 + friend_requests.insert(link); 82 + return Ok(()); 83 + } 70 84 71 - // if we remove sucessfully the link, it means a request already existed 72 - // so we are making the friendship official 73 - let friend_request_accepted = friend_requests.remove(&link); 74 - if friend_request_accepted { 75 - drop(friend_requests); 85 + pub async fn accept_friend_request( 86 + State(state): State<AppState>, 87 + Extension(user_id): Extension<String>, 88 + sender_id: String, 89 + ) -> Result<(), MyErr> { 90 + let link = Link::new(user_id, sender_id); 76 91 77 - let mut pings_state = state.positions.lock().await; 78 - pings_state.insert(link.clone(), RingBuffer::new(state.ring_buffer_cap)); 79 - drop(pings_state); 92 + let friend_request_accepted = { state.friend_requests.lock().await.remove(&link) }; 80 93 81 - let mut links = state.links.lock().await; 82 - links.insert(link); 83 - drop(links); 84 - } else { 85 - friend_requests.insert(link); 86 - drop(friend_requests); 94 + if !friend_request_accepted { 95 + return my_err!("Friend request not found"); 87 96 } 88 97 98 + let mut pings_state = state.positions.lock().await; 99 + pings_state.insert(link.clone(), RingBuffer::new(state.ring_buffer_cap)); 100 + drop(pings_state); 101 + 102 + let mut links = state.links.lock().await; 103 + links.insert(link); 104 + 89 105 return Ok(()); 90 106 } 91 107 ··· 93 109 State(state): State<AppState>, 94 110 Extension(user_id): Extension<String>, 95 111 friend_id: String, 96 - ) -> Result<PlainBool, SrvErr> { 112 + ) -> Result<PlainBool, MyErr> { 97 113 let link = Link::new(friend_id, user_id); 98 114 let links = state.links.lock().await; 99 115 let accepted = links.contains(&link); ··· 104 120 State(state): State<AppState>, 105 121 Extension(user_id): Extension<String>, 106 122 Json(pings): Json<Vec<PingPayload>>, 107 - ) -> Result<(), SrvErr> { 123 + ) -> Result<(), MyErr> { 108 124 let links = state.links.lock().await; 109 125 for ping in &pings { 110 126 let link = Link::new(user_id.clone(), ping.receiver_id.clone()); 111 127 if !links.contains(&link) { 112 - ReqBail!("Ping receiver is not linked to sender"); 128 + return my_err!("Ping receiver is not linked to sender"); 113 129 } 114 130 } 115 131 drop(links); ··· 131 147 State(state): State<AppState>, 132 148 Extension(user_id): Extension<String>, 133 149 sender_id: String, 134 - ) -> Result<EncryptedPingVec, SrvErr> { 150 + ) -> Result<EncryptedPingVec, MyErr> { 135 151 let link = Link::new(user_id, sender_id); 136 152 let links = state.links.lock().await; 137 153 138 154 if !links.contains(&link) { 139 - ReqBail!("No link exists between these users"); 155 + return my_err!("No link exists between these users"); 140 156 } 141 157 drop(links); 142 158
-208
server/src/log.rs
··· 1 - use std::{ 2 - sync::atomic::{AtomicU64, Ordering}, 3 - time::Instant, 4 - }; 5 - 6 - use axum::{ 7 - body::{Body, Bytes, to_bytes}, 8 - http::{HeaderMap, Method, Request}, 9 - middleware::Next, 10 - response::Response, 11 - }; 12 - use base64::{Engine, prelude::BASE64_STANDARD}; 13 - use serde_json::Value; 14 - 15 - static REQ_SEQ: AtomicU64 = AtomicU64::new(1); 16 - 17 - fn format_x_auth(headers: &HeaderMap) -> Option<String> { 18 - let v = headers.get("x-auth")?; 19 - let s = v.to_str().ok()?.to_string(); 20 - 21 - const MAX: usize = 120; 22 - if s.len() > MAX { 23 - Some(format!("{}โ€ฆ (len={})", &s[..MAX], s.len())) 24 - } else { 25 - Some(s) 26 - } 27 - } 28 - 29 - fn status_emoji(status: axum::http::StatusCode) -> &'static str { 30 - if status.is_success() { 31 - "โœ…" 32 - } else if status.is_redirection() { 33 - "โ†ช" 34 - } else if status.is_client_error() { 35 - "โš " 36 - } else if status.is_server_error() { 37 - "โŒ" 38 - } else { 39 - "โ„น" 40 - } 41 - } 42 - 43 - /// Convert JSON into a "key: value" style display. 44 - /// - Objects: `key: value` (nested objects/arrays are indented) 45 - /// - Arrays: `- item` (nested indented) 46 - /// If not JSON: UTF-8 text, else base64. 47 - fn body_as_kv(bytes: &Bytes) -> String { 48 - if bytes.is_empty() { 49 - return "<empty>".to_string(); 50 - } 51 - 52 - if let Ok(v) = serde_json::from_slice::<Value>(bytes) { 53 - let mut out = String::new(); 54 - write_value(&mut out, &v, 0); 55 - return out.trim_end().to_string(); 56 - } 57 - 58 - match std::str::from_utf8(bytes) { 59 - Ok(s) => s.to_string(), 60 - Err(_) => format!("<non-utf8; base64>\n{}", BASE64_STANDARD.encode(bytes)), 61 - } 62 - } 63 - 64 - fn write_value(out: &mut String, v: &Value, indent: usize) { 65 - match v { 66 - Value::Object(map) => { 67 - for (k, val) in map { 68 - write_key_value(out, k, val, indent); 69 - } 70 - } 71 - Value::Array(arr) => { 72 - for item in arr { 73 - write_array_item(out, item, indent); 74 - } 75 - } 76 - _ => { 77 - // Root primitive 78 - out.push_str(&indent_str(indent)); 79 - out.push_str(&format_primitive(v)); 80 - out.push('\n'); 81 - } 82 - } 83 - } 84 - 85 - fn write_key_value(out: &mut String, key: &str, val: &Value, indent: usize) { 86 - let pad = indent_str(indent); 87 - 88 - match val { 89 - Value::Object(_) | Value::Array(_) => { 90 - out.push_str(&pad); 91 - out.push_str(key); 92 - out.push_str(":\n"); 93 - write_value(out, val, indent + 2); 94 - } 95 - _ => { 96 - out.push_str(&pad); 97 - out.push_str(key); 98 - out.push_str(": "); 99 - out.push_str(&format_primitive(val)); 100 - out.push('\n'); 101 - } 102 - } 103 - } 104 - 105 - fn write_array_item(out: &mut String, item: &Value, indent: usize) { 106 - let pad = indent_str(indent); 107 - 108 - match item { 109 - Value::Object(_) | Value::Array(_) => { 110 - out.push_str(&pad); 111 - out.push_str("-\n"); 112 - write_value(out, item, indent + 2); 113 - } 114 - _ => { 115 - out.push_str(&pad); 116 - out.push_str("- "); 117 - out.push_str(&format_primitive(item)); 118 - out.push('\n'); 119 - } 120 - } 121 - } 122 - 123 - fn format_primitive(v: &Value) -> String { 124 - match v { 125 - Value::String(s) => s.clone(), 126 - Value::Number(n) => n.to_string(), 127 - Value::Bool(b) => b.to_string(), 128 - Value::Null => "null".to_string(), 129 - // Shouldnโ€™t happen here (we route these elsewhere), but safe fallback 130 - Value::Object(_) | Value::Array(_) => "<complex>".to_string(), 131 - } 132 - } 133 - 134 - fn indent_str(spaces: usize) -> String { 135 - " ".repeat(spaces) 136 - } 137 - 138 - fn indent_block(s: &str, spaces: usize) -> String { 139 - let pad = " ".repeat(spaces); 140 - s.lines() 141 - .map(|line| format!("{pad}{line}\n")) 142 - .collect::<String>() 143 - .trim_end_matches('\n') 144 - .to_string() 145 - } 146 - 147 - pub async fn log_req_res_bodies(req: Request<Body>, next: Next) -> Response { 148 - // Avoid noisy CORS preflight logs 149 - if req.method() == Method::OPTIONS { 150 - return next.run(req).await; 151 - } 152 - 153 - let id = REQ_SEQ.fetch_add(1, Ordering::Relaxed); 154 - let start = Instant::now(); 155 - 156 - let method = req.method().clone(); 157 - let uri = req.uri().clone(); 158 - let req_headers = req.headers().clone(); 159 - 160 - // Read + restore request body 161 - let (req_parts, req_body) = req.into_parts(); 162 - let req_bytes = to_bytes(req_body, usize::MAX).await.unwrap(); 163 - let req = Request::from_parts(req_parts, Body::from(req_bytes.clone())); 164 - 165 - // Run handler 166 - let res = next.run(req).await; 167 - 168 - let status = res.status(); 169 - let res_headers = res.headers().clone(); 170 - 171 - // Read + restore response body 172 - let (res_parts, res_body) = res.into_parts(); 173 - let res_bytes = to_bytes(res_body, usize::MAX).await.unwrap(); 174 - let res = Response::from_parts(res_parts, Body::from(res_bytes.clone())); 175 - 176 - let ms = start.elapsed().as_millis(); 177 - let sep = "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•"; 178 - 179 - let mut out = String::new(); 180 - out.push('\n'); 181 - out.push_str(sep); 182 - out.push('\n'); 183 - out.push_str(&format!( 184 - "๐Ÿšฆ #{id} {method} {uri} {} {status} โฑ {ms}ms\n", 185 - status_emoji(status) 186 - )); 187 - 188 - out.push('\n'); 189 - 190 - out.push_str("๐Ÿ“ฅ Request\n"); 191 - if let Some(xauth) = format_x_auth(&req_headers) { 192 - out.push_str(&format!(" ๐Ÿ” x-auth: {xauth}\n")); 193 - } 194 - out.push_str(&indent_block(&body_as_kv(&req_bytes), 2)); 195 - out.push('\n'); 196 - 197 - out.push_str("๐Ÿ“ค Response\n"); 198 - out.push_str(&indent_block(&body_as_kv(&res_bytes), 2)); 199 - out.push('\n'); 200 - 201 - out.push_str(sep); 202 - out.push('\n'); 203 - 204 - tracing::info!("{out}"); 205 - 206 - let _ = res_headers; 207 - res 208 - }
+86 -19
server/src/main.rs
··· 1 1 use std::{collections::HashMap, sync::Arc}; 2 2 3 - use axum::{Router, routing::post}; 3 + use axum::{ 4 + Router, 5 + body::{Body, to_bytes}, 6 + extract::{Request, State}, 7 + middleware::Next, 8 + response::IntoResponse, 9 + routing::post, 10 + }; 11 + use base64::{Engine, prelude::BASE64_STANDARD}; 12 + use ed25519_dalek::Signature; 4 13 use nanoid::nanoid; 14 + use serde::Deserialize; 5 15 use std::collections::HashSet; 6 16 use tokio::sync::Mutex; 7 - use tower_http::cors::CorsLayer; 8 - use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 17 + use tower_http::cors::{Any, CorsLayer}; 9 18 10 - mod auth; 11 19 mod handlers; 12 - mod log; 13 20 mod types; 14 21 15 22 use handlers::*; 16 23 use types::*; 17 24 18 - use crate::auth::auth_test; 19 - use crate::log::log_req_res_bodies; 20 - 21 25 #[tokio::main] 22 26 async fn main() { 23 - tracing_subscriber::registry() 24 - .with( 25 - tracing_subscriber::EnvFilter::try_from_default_env() 26 - .unwrap_or_else(|_| "info,tower_http=info,axum=info".into()), 27 - ) 28 - .with(tracing_subscriber::fmt::layer()) 29 - .init(); 30 - 31 27 // TODO: should this be inside an Arc? 32 28 let state = AppState { 33 29 users: Arc::new(Mutex::new(Vec::new())), ··· 50 46 .route("/", post(|| async { "You just sent a POST to /" })) // for testing 51 47 .route("/create-account", post(create_user)) 52 48 .route("/generate-signup-key", post(generate_signup_key)) 53 - .route("/request-friend-request", post(request_friend_request)) 49 + .route("/create-friend-request", post(create_friend_request)) 50 + .route("/accept-friend-request", post(accept_friend_request)) 54 51 .route( 55 52 "/is-friend-request-accepted", 56 53 post(is_friend_request_accepted), ··· 59 56 .route("/get-pings", post(get_pings)) 60 57 .with_state(state.clone()) 61 58 .layer(axum::middleware::from_fn_with_state(state, auth_test)) 62 - .layer(axum::middleware::from_fn(log_req_res_bodies)) 63 59 .layer(CorsLayer::permissive()); 64 60 65 61 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); 66 62 axum::serve(listener, app).await.unwrap(); 67 63 } 68 64 69 - // TODO: potential security risk of returning error messages directly to the user. nice for debugging tho :p 65 + async fn auth_test(State(state): State<AppState>, req: Request, next: Next) -> impl IntoResponse { 66 + let endpoint = req.uri().path().to_owned(); 67 + if endpoint != "/create-account" { 68 + // CURSED STUFF BEGIN 69 + let (parts, body) = req.into_parts(); 70 + let body_bytes = to_bytes(body, usize::MAX).await.unwrap(); 71 + let new_body = Body::from(body_bytes.clone()); 72 + let mut req = Request::from_parts(parts, new_body); 73 + // CURSED STUFF END 74 + 75 + let auth_header = req 76 + .headers() 77 + .get("x-auth") 78 + .and_then(|v| v.to_str().ok()) 79 + .unwrap_or_else(|| panic!("missing x-auth header")); 80 + 81 + let decoded_auth = BASE64_STANDARD 82 + .decode(auth_header) 83 + .unwrap_or_else(|_| panic!("invalid base64 in x-auth header")); 84 + 85 + let auth_str = String::from_utf8(decoded_auth) 86 + .unwrap_or_else(|_| panic!("invalid utf8 in x-auth header")); 87 + 88 + let auth_data: Auth = serde_json::from_str(&auth_str) 89 + .unwrap_or_else(|e| panic!("failed to parse x-auth JSON: {e}")); 90 + 91 + let users = state.users.lock().await; 92 + let user_id = auth_data.user_id; 93 + let user = users 94 + .iter() 95 + .find(|u| u.id == user_id) 96 + .unwrap_or_else(|| panic!("User not found")); 97 + let verifying_key = user.pub_key.clone(); 98 + drop(users); 99 + 100 + //////////////////////////////////// 101 + //////////////////////////////////// unsure 102 + //////////////////////////////////// 103 + 104 + let sig_vec = BASE64_STANDARD.decode(&auth_data.signature).unwrap(); 105 + let sig_bytes: [u8; 64] = sig_vec.try_into().expect("invalid signature length"); 106 + let signature = Signature::from_bytes(&sig_bytes); 107 + 108 + if let Err(err) = verifying_key.verify_strict(&body_bytes, &signature) { 109 + panic!("Signature verification failed: {err}"); 110 + } 111 + 112 + //////////////////////////////////// 113 + //////////////////////////////////// 114 + //////////////////////////////////// 115 + 116 + // TODO: Maybe make the endpoints an enum 117 + if endpoint == "/generate-signup-key" { 118 + let admin_id = state.admin_id.lock().await; 119 + if admin_id.as_ref() != Some(&user_id) { 120 + todo!("not allowed") 121 + } 122 + } 123 + 124 + req.extensions_mut().insert(user_id); 125 + 126 + return next.run(req).await; 127 + } 128 + 129 + return next.run(req).await; 130 + } 131 + 132 + #[derive(Debug, Deserialize, Clone)] 133 + struct Auth { 134 + user_id: String, 135 + signature: String, 136 + }
+6 -57
server/src/types.rs
··· 22 22 pub ring_buffer_cap: usize, 23 23 } 24 24 25 - #[derive(Debug, Deserialize, Clone)] 26 - pub struct AuthData { 27 - pub user_id: String, 28 - pub signature: String, 29 - } 30 - 31 25 pub struct RingBuffer { 32 26 pub ring: Box<[Option<EncryptedPing>]>, 33 27 pub idx: usize, ··· 86 80 pub id: String, 87 81 pub pub_key: VerifyingKey, 88 82 } 83 + // pub struct User(pub String); 89 84 90 85 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 91 86 pub struct Link(String, String); ··· 121 116 122 117 impl IntoResponse for EncryptedPingVec { 123 118 fn into_response(self) -> Response { 124 - axum::Json(self.0).into_response() 119 + axum::Json(self.0).into_response() // TODO: check if this is correct 125 120 } 126 121 } 127 122 128 - #[derive(Debug)] 129 - pub struct SrvErr { 130 - pub msg: String, 131 - pub cause: Option<String>, 132 - } 123 + pub struct MyErr(pub &'static str); 133 124 134 - impl IntoResponse for SrvErr { 125 + impl IntoResponse for MyErr { 135 126 fn into_response(self) -> Response { 136 - // Log once here (this runs only for real errors) 137 - match &self.cause { 138 - Some(c) => eprintln!("[ERR] {} | cause: {}", self.msg, c), 139 - None => eprintln!("[ERR] {}", self.msg), 140 - } 141 - 142 - let body = if cfg!(debug_assertions) { 143 - match &self.cause { 144 - Some(c) => format!("{} | cause: {}", self.msg, c), 145 - None => self.msg.clone(), 146 - } 147 - } else { 148 - self.msg.clone() 149 - }; 150 - 151 - (StatusCode::INTERNAL_SERVER_ERROR, body).into_response() 127 + let body = format!(r#"{{"error":"{}"}}"#, self.0); // example: {"error":"something went wrong"} 128 + return (StatusCode::INTERNAL_SERVER_ERROR, body).into_response(); 152 129 } 153 130 } 154 - 155 - /// Central policy: what gets logged, what gets returned. 156 - pub fn mk_srv_err(msg: impl Into<String>, cause: Option<String>) -> SrvErr { 157 - SrvErr { 158 - msg: msg.into(), 159 - cause, 160 - } 161 - } 162 - 163 - #[macro_export] 164 - macro_rules! SrvErr { 165 - ($msg:expr) => { 166 - $crate::mk_srv_err($msg, None) 167 - }; 168 - ($msg:expr, $err:expr) => { 169 - $crate::mk_srv_err($msg, Some(format!("{:?}", $err))) 170 - }; 171 - } 172 - 173 - #[macro_export] 174 - macro_rules! ReqBail { 175 - ($msg:expr) => {{ 176 - return Err($crate::SrvErr!($msg)); 177 - }}; 178 - ($msg:expr, $err:expr) => {{ 179 - return Err($crate::SrvErr!($msg, $err)); 180 - }}; 181 - }
+47
server/test/autils.ts
··· 1 + import { expect } from "bun:test"; 2 + 3 + export const URL = "http://127.0.0.1:3000"; 4 + 5 + export async function generateUser(signup_key: string | undefined, should_be_admin: boolean = false): Promise<string> { 6 + if (signup_key === undefined) { 7 + throw new Error("signup_key was not provided or captured from server output"); 8 + } 9 + 10 + const res = await fetch(`${URL}/create-account`, { 11 + method: "POST", 12 + body: signup_key, 13 + }); 14 + const json = await res.json(); 15 + expect(res.status).toBe(200); 16 + expect(json).toEqual({ 17 + user_id: expect.any(String), 18 + is_admin: should_be_admin, 19 + }); 20 + 21 + return json.user_id; 22 + } 23 + 24 + export async function post(endpoint: string, user_id: string, data: Object | string | undefined): Promise<any> { 25 + const headers: Record<string, string> = { 26 + "x-auth": JSON.stringify({ user_id }), 27 + }; 28 + 29 + let stringified_data: string | undefined; 30 + 31 + if (typeof data === "object") { 32 + stringified_data = JSON.stringify(data); 33 + headers["Content-Type"] = "application/json"; 34 + } else { 35 + stringified_data = data; 36 + } 37 + 38 + const res = await fetch(`${URL}/${endpoint}`, { 39 + method: "POST", 40 + headers, 41 + body: stringified_data, 42 + }); 43 + 44 + expect(res.status).toBe(200); 45 + 46 + return await res.text(); 47 + }
+12 -2
server/test/test.test.ts
··· 1 1 import { SERVER_DIR, startOrRestartServer, stopServer } from "./srv.ts"; 2 2 import { rm } from "node:fs/promises"; 3 - import { describe, test, expect, beforeAll, afterAll, beforeEach } from "bun:test"; 3 + import { 4 + describe, 5 + test, 6 + expect, 7 + beforeAll, 8 + afterAll, 9 + beforeEach, 10 + } from "bun:test"; 4 11 import { generateUser, post, URL } from "./utils.ts"; 5 12 6 13 console.log(`SERVER_DIR: ${SERVER_DIR}`); ··· 47 54 48 55 const res3 = await post("get-pings", user, admin.user_id); 49 56 50 - expect(JSON.parse(res3)).toEqual(["this is definitely encrypted trust #2", "this is definitely encrypted trust #1"]); 57 + expect(JSON.parse(res3)).toEqual([ 58 + "this is definitely encrypted trust #2", 59 + "this is definitely encrypted trust #1", 60 + ]); 51 61 }); 52 62 });