-34
app/.gitignore
-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
+1
-15
app/README.md
+1
-15
app/README.md
···
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.
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
+17
-26
app/bun.lock
+17
-26
app/bun.lock
···
5
5
"": {
6
6
"name": "privacypin",
7
7
"dependencies": {
8
-
"@tauri-apps/api": "^2",
9
-
"@tauri-apps/plugin-opener": "^2",
8
+
"@tauri-apps/api": "^2.9.0",
9
+
"@tauri-apps/plugin-opener": "^2.5.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",
14
+
"@tauri-apps/cli": "^2.9.4",
15
15
"@types/alpinejs": "^3.13.11",
16
-
"@types/bun": "latest",
17
-
"typescript": "~5.6.2",
18
-
"vite": "^6.0.3",
16
+
"typescript": "~5.6.3",
17
+
"vite": "^6.4.1",
19
18
},
20
19
},
21
20
},
···
118
117
119
118
"@tauri-apps/api": ["@tauri-apps/api@2.9.0", "", {}, "sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw=="],
120
119
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=="],
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=="],
122
121
123
-
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W8FQXZXQmQ0Fmj9UJXNrm2mLdIaLLriKVY7o/FzmizyIKTPIvHjfZALTNybbpTQRbJvKoGHLrW1DNzAWVDWJYg=="],
122
+
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9rHkMVtbMhe0AliVbrGpzMahOBg3rwV46JYRELxR9SN6iu1dvPOaMaiC4cP6M/aD1424ziXnnMdYU06RAH8oIw=="],
124
123
125
-
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-zDwu40rlshijt3TU6aRvzPUyVpapsx1sNfOlreDMTaMelQLHl6YoQzSRpLHYwrHrhimxyX2uDqnKIiuGel0Lhg=="],
124
+
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-VT9ymNuT06f5TLjCZW2hfSxbVtZDhORk7CDUDYiq5TiSYQdxkl8MVBy0CCFFcOk4QAkUmqmVUA9r3YZ/N/vPRQ=="],
126
125
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=="],
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=="],
128
127
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=="],
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=="],
130
129
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=="],
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=="],
132
131
133
-
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.9.3", "", { "os": "linux", "cpu": "none" }, "sha512-qV8DZXI/fZwawk6T3Th1g6smiNC2KeQTk7XFgKvqZ6btC01z3UTsQmNGvI602zwm3Ld1TBZb4+rEWu2QmQimmw=="],
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=="],
134
133
135
-
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tquyEONCNRfqEBWEe4eAHnxFN5yY5lFkCuD4w79XLIovUxVftQ684+xLp7zkhntkt4y20SMj2AgJa/+MOlx4Kg=="],
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=="],
136
135
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=="],
136
+
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zcd1QVffh5tZs1u1SCKUV/V7RRynebgYUNWHuV0FsIF1MjnULUChEXhAhug7usCDq4GZReMJOoXa6rukEozWIw=="],
138
137
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=="],
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=="],
140
139
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=="],
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=="],
142
141
143
-
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.3", "", { "os": "win32", "cpu": "x64" }, "sha512-fmw7NrrHE5m49idCvJAx9T9bsupjdJ0a3p3DPCNCZRGANU6R1tA1L+KTlVuUtdAldX2NqU/9UPo2SCslYKgJHQ=="],
142
+
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg=="],
144
143
145
144
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
146
145
···
148
147
149
148
"@types/alpinejs": ["@types/alpinejs@3.13.11", "", {}, "sha512-3KhGkDixCPiLdL3Z/ok1GxHwLxEWqQOKJccgaQL01wc0EVM2tCTaqlC3NIedmxAXkVzt/V6VTM8qPgnOHKJ1MA=="],
150
149
151
-
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
152
-
153
150
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
154
151
155
-
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
156
-
157
152
"@vue/reactivity": ["@vue/reactivity@3.1.5", "", { "dependencies": { "@vue/shared": "3.1.5" } }, "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg=="],
158
153
159
154
"@vue/shared": ["@vue/shared@3.1.5", "", {}, "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="],
160
155
161
156
"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=="],
164
157
165
158
"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=="],
166
159
···
183
176
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
184
177
185
178
"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=="],
188
179
189
180
"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=="],
190
181
}
+22
-23
app/package.json
+22
-23
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",
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
-
}
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
+
}
25
24
}
+6
app/src/assets/ellipsis-vertical.svg
+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
+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
+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
-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
-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
+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
+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
+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
+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
+
1
19
body {
2
20
font-family: sans-serif;
3
21
background: #f9fafb;
···
9
27
.app {
10
28
width: 100%;
11
29
background: #f9fafb;
30
+
}
31
+
32
+
.svg-icon {
33
+
width: 25px;
34
+
height: 25px;
35
+
margin: auto;
12
36
}
13
37
14
38
header {
+33
-12
app/src/home-page/home.html
+33
-12
app/src/home-page/home.html
···
12
12
<header>
13
13
<h1>PrivacyPin</h1>
14
14
<div>
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
-
โ๏ธ
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
+
/>
19
37
</button>
20
38
</div>
21
39
</header>
···
47
65
@click="viewLocation(friend.id)"
48
66
>
49
67
<img
50
-
src="/src/assets/pin.svg"
68
+
class="svg-icon"
69
+
src="/src/assets/pin-location.svg"
51
70
alt="Pin Icon"
52
71
/>View
53
72
</button>
54
-
<span
73
+
<a
55
74
class="menu-icon"
75
+
style="margin-bottom: auto"
56
76
@click="friendOptions(friend.id)"
57
-
></span>
58
-
<img
59
-
class="menu-icon"
60
-
src="/src/assets/three-dots.svg"
61
-
alt="Pin Icon"
62
-
/>
77
+
>
78
+
<img
79
+
class="svg-icon"
80
+
src="/src/assets/ellipsis-vertical.svg"
81
+
alt="Options menu"
82
+
/>
83
+
</a>
63
84
</div>
64
85
</div>
65
86
</template>
+8
-10
app/src/home-page/home.ts
+8
-10
app/src/home-page/home.ts
···
1
1
import Alpine from "alpinejs";
2
+
import { goto } from "../utils/tools.ts";
2
3
import { Store } from "../utils/store.ts";
3
-
import { post } from "../utils/api.ts";
4
+
import * as api from "../utils/api.ts";
4
5
5
6
Alpine.data("homePageState", () => ({
6
7
friends: [
···
22
23
alert(`Options for friend id ${friend_id}`);
23
24
},
24
25
26
+
async updateServer() {
27
+
await api.sendPings("123", "3.14159N 3.14159W");
28
+
},
29
+
25
30
addFriend() {
26
31
alert("Add friend functionality would open here");
27
32
},
28
33
29
34
openSettings() {
30
-
alert("Settings would open here");
35
+
goto("settings");
31
36
},
32
37
33
38
async generateSignupKey() {
34
-
const res = await post("generate-signup-key", undefined);
35
-
this.newSignupKey = res;
36
-
console.log(res);
39
+
this.newSignupKey = await api.generateSignupKey();
37
40
},
38
41
39
42
isAdmin() {
40
43
return Store.get("is_admin");
41
-
},
42
-
43
-
goto(newLocation: string) {
44
-
window.location.href =
45
-
"/src/" + newLocation + "-page/" + newLocation + ".html";
46
44
},
47
45
}));
48
46
+3
-3
app/src/settings-page/settings.css
+3
-3
app/src/settings-page/settings.css
···
96
96
background: #1d4ed8;
97
97
}
98
98
99
-
.btn-qr {
99
+
.btn-secondary {
100
100
background: white;
101
101
gap: 0.5rem;
102
102
border: 1px solid #d1d5db;
103
103
}
104
104
105
-
.btn-qr:hover {
105
+
.btn-secondary:hover {
106
106
background: #f3f4f6;
107
107
}
108
108
109
-
.btn-qr img {
109
+
.btn-secondary img {
110
110
width: 16px;
111
111
height: 16px;
112
112
}
+2
-4
app/src/settings-page/settings.html
+2
-4
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? -->
13
11
<div class="actions" x-data="settingsPageState">
14
12
<h3>Settings</h3>
15
13
16
-
<button class="btn-secondary" @click="goto('home')">
14
+
<button class="btn-primary" @click="goto('home')">
17
15
Back to Home
18
16
</button>
19
17
20
-
<button class="btn-secondary" @click="resetStore()">
18
+
<button class="btn-secondary" @click="await debugLogout()">
21
19
Signout
22
20
</button>
23
21
</div>
+2
-3
app/src/settings-page/settings.ts
+2
-3
app/src/settings-page/settings.ts
+3
-3
app/src/signup-page/signup.css
+3
-3
app/src/signup-page/signup.css
+34
-7
app/src/signup-page/signup.html
+34
-7
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.svg" alt="Pin Icon" />
13
+
<img src="/src/assets/pin-location.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 id="server" type="url" placeholder="https://your-server.com" x-model="serverAddress" required />
24
+
<input
25
+
id="server"
26
+
type="url"
27
+
placeholder="https://your-server.com"
28
+
x-model="serverAddress"
29
+
required
30
+
/>
25
31
</div>
26
32
27
33
<div>
28
34
<label for="key">Signup Key</label>
29
-
<input id="key" type="password" placeholder="Enter your signup key" x-model="signupKey" required />
35
+
<input
36
+
id="key"
37
+
type="password"
38
+
placeholder="Enter your signup key"
39
+
x-model="signupKey"
40
+
required
41
+
/>
30
42
</div>
31
43
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" />
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" />
35
55
Scan QR Code
36
56
</button>
37
57
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>
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>
39
66
</div>
40
67
</div>
41
68
</body>
+4
-5
app/src/signup-page/signup.ts
+4
-5
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";
4
3
5
4
Alpine.data("signupPageState", () => ({
6
5
serverAddress: "",
···
9
8
10
9
async signup() {
11
10
this.isDoingStuff = true;
12
-
await new Promise((resolve) => setTimeout(resolve, 2000)); // temp
11
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // temp
13
12
try {
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);
13
+
await createAccount(this.serverAddress, this.signupKey);
17
14
window.location.href = "/src/home-page/home.html";
18
15
} catch (e) {
16
+
const err = e instanceof Error ? e.message : e;
17
+
alert(`Sign-up failed: ${err}`);
19
18
this.isDoingStuff = false;
20
19
}
21
20
},
+53
app/src/types.d.ts
+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 {};
+67
-80
app/src/utils/api.ts
+67
-80
app/src/utils/api.ts
···
1
1
import { Store } from "./store.ts";
2
2
3
3
function bufToBase64(buf: ArrayBuffer): string {
4
-
return btoa(String.fromCharCode(...new Uint8Array(buf)));
4
+
return new Uint8Array(buf).toBase64();
5
5
}
6
6
7
-
function strToBytes(str: string): Uint8Array {
8
-
return new TextEncoder().encode(str);
9
-
}
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);
10
15
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);
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
+
});
27
21
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
-
});
22
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
23
+
const json = await response.json();
24
+
25
+
// TODO validate data?
26
+
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));
31
+
32
+
return json;
33
+
}
33
34
34
-
if (!response.ok) throw new Error(await response.text());
35
-
const json = await response.json();
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
+
}
36
39
37
-
await Store.set("user_id", json.user_id);
38
-
await Store.set("priv_key", bufToBase64(privKeyRaw));
40
+
export async function requestFriendRequest(friend_id: string): Promise<void> {
41
+
await post("request-friend-request", friend_id);
42
+
}
39
43
40
-
return json;
41
-
} catch (err) {
42
-
alert(`${err}`);
43
-
throw err;
44
-
}
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";
45
47
}
46
48
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");
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
+
}
56
53
57
-
if (!user_id || !privKey_b64) throw new Error("Missing user credentials");
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
+
}
58
58
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);
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");
64
66
65
-
// Import private key and sign
66
-
const privKeyBytes = Uint8Array.from(atob(privKey_b64), (c) =>
67
-
c.charCodeAt(0),
68
-
);
67
+
const bodyBytes = new TextEncoder().encode(body === undefined ? "" : body);
69
68
70
-
const privKey = await crypto.subtle.importKey(
71
-
"pkcs8",
72
-
privKeyBytes.buffer,
73
-
{ name: "Ed25519" },
74
-
false,
75
-
["sign"],
76
-
);
69
+
const privKeyBytes = Uint8Array.fromBase64(privKey_b64);
77
70
78
-
const signature = await crypto.subtle.sign("Ed25519", privKey, bodyBytes);
79
-
const signature_b64 = bufToBase64(signature);
71
+
const privKey = await crypto.subtle.importKey("pkcs8", privKeyBytes.buffer, "Ed25519", false, ["sign"]);
80
72
81
-
// Encode header JSON to base64
82
-
const authJson = JSON.stringify({ user_id, signature: signature_b64 });
83
-
const authHeader = btoa(authJson);
73
+
const signature = await crypto.subtle.sign("Ed25519", privKey, bodyBytes);
74
+
const signature_b64 = bufToBase64(signature);
84
75
85
-
const headers: Record<string, string> = {
86
-
"x-auth": authHeader,
87
-
};
88
-
if (typeof data === "object") headers["Content-Type"] = "application/json";
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
+
};
89
80
90
-
const res = await fetch(`${server_url}/${endpoint}`, {
91
-
method: "POST",
92
-
headers,
93
-
body: bodyStr.length > 0 ? bodyStr : undefined,
94
-
});
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
+
});
95
86
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
-
}
87
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
88
+
return await res.text();
102
89
}
-44
app/src/utils/tools.ts
-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
+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
+
1
5
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
2
6
#[tauri::command]
3
7
fn greet(name: &str) -> String {
4
8
format!("Hello, {}! You've been greeted from Rust!", name)
5
9
}
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() {
+190
-60
server/Cargo.lock
+190
-60
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]]
6
15
name = "atomic-waker"
7
16
version = "1.1.2"
8
17
source = "registry+https://github.com/rust-lang/crates.io-index"
···
10
19
11
20
[[package]]
12
21
name = "axum"
13
-
version = "0.8.6"
22
+
version = "0.8.8"
14
23
source = "registry+https://github.com/rust-lang/crates.io-index"
15
-
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
24
+
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
16
25
dependencies = [
17
26
"axum-core",
18
27
"bytes",
···
43
52
44
53
[[package]]
45
54
name = "axum-core"
46
-
version = "0.5.5"
55
+
version = "0.5.6"
47
56
source = "registry+https://github.com/rust-lang/crates.io-index"
48
-
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
57
+
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
49
58
dependencies = [
50
59
"bytes",
51
60
"futures-core",
···
83
92
84
93
[[package]]
85
94
name = "bytes"
86
-
version = "1.10.1"
95
+
version = "1.11.0"
87
96
source = "registry+https://github.com/rust-lang/crates.io-index"
88
-
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
97
+
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
89
98
90
99
[[package]]
91
100
name = "cfg-if"
···
110
119
111
120
[[package]]
112
121
name = "crypto-common"
113
-
version = "0.2.0-rc.5"
122
+
version = "0.2.0-rc.8"
114
123
source = "registry+https://github.com/rust-lang/crates.io-index"
115
-
checksum = "919bd05924682a5480aec713596b9e2aabed3a0a6022fab6847f85a99e5f190a"
124
+
checksum = "e6165b8029cdc3e765b74d3548f85999ee799d5124877ce45c2c85ca78e4d4aa"
116
125
dependencies = [
117
126
"hybrid-array",
118
127
]
119
128
120
129
[[package]]
121
130
name = "curve25519-dalek"
122
-
version = "5.0.0-pre.1"
131
+
version = "5.0.0-pre.3"
123
132
source = "registry+https://github.com/rust-lang/crates.io-index"
124
-
checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7"
133
+
checksum = "92419e1cdc506051ffd30713ad09d0ec6a24bba9197e12989de389e35b19c77a"
125
134
dependencies = [
126
135
"cfg-if",
127
136
"cpufeatures",
···
146
155
147
156
[[package]]
148
157
name = "der"
149
-
version = "0.8.0-rc.9"
158
+
version = "0.8.0-rc.10"
150
159
source = "registry+https://github.com/rust-lang/crates.io-index"
151
-
checksum = "e9d8dd2f26c86b27a2a8ea2767ec7f9df7a89516e4794e54ac01ee618dda3aa4"
160
+
checksum = "02c1d73e9668ea6b6a28172aa55f3ebec38507131ce179051c8033b5c6037653"
152
161
dependencies = [
153
162
"const-oid",
154
163
]
155
164
156
165
[[package]]
157
166
name = "digest"
158
-
version = "0.11.0-rc.4"
167
+
version = "0.11.0-rc.5"
159
168
source = "registry+https://github.com/rust-lang/crates.io-index"
160
-
checksum = "ea390c940e465846d64775e55e3115d5dc934acb953de6f6e6360bc232fe2bf7"
169
+
checksum = "ebf9423bafb058e4142194330c52273c343f8a5beb7176d052f0e73b17dd35b9"
161
170
dependencies = [
162
171
"block-buffer",
163
172
"crypto-common",
···
175
184
176
185
[[package]]
177
186
name = "ed25519-dalek"
178
-
version = "3.0.0-pre.1"
187
+
version = "3.0.0-pre.3"
179
188
source = "registry+https://github.com/rust-lang/crates.io-index"
180
-
checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1"
189
+
checksum = "5d6d275a4ffdfc16e98fbcb5f5417214a06957c7cdc6eb2815c2dc50dce1c1dd"
181
190
dependencies = [
182
191
"curve25519-dalek",
183
192
"ed25519",
···
187
196
]
188
197
189
198
[[package]]
199
+
name = "errno"
200
+
version = "0.3.14"
201
+
source = "registry+https://github.com/rust-lang/crates.io-index"
202
+
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
203
+
dependencies = [
204
+
"libc",
205
+
"windows-sys 0.61.2",
206
+
]
207
+
208
+
[[package]]
190
209
name = "fiat-crypto"
191
210
version = "0.3.0"
192
211
source = "registry+https://github.com/rust-lang/crates.io-index"
193
212
checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
194
213
195
214
[[package]]
196
-
name = "fnv"
197
-
version = "1.0.7"
198
-
source = "registry+https://github.com/rust-lang/crates.io-index"
199
-
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
200
-
201
-
[[package]]
202
215
name = "form_urlencoded"
203
216
version = "1.2.2"
204
217
source = "registry+https://github.com/rust-lang/crates.io-index"
···
253
266
254
267
[[package]]
255
268
name = "http"
256
-
version = "1.3.1"
269
+
version = "1.4.0"
257
270
source = "registry+https://github.com/rust-lang/crates.io-index"
258
-
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
271
+
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
259
272
dependencies = [
260
273
"bytes",
261
-
"fnv",
262
274
"itoa",
263
275
]
264
276
···
308
320
309
321
[[package]]
310
322
name = "hyper"
311
-
version = "1.7.0"
323
+
version = "1.8.1"
312
324
source = "registry+https://github.com/rust-lang/crates.io-index"
313
-
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
325
+
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
314
326
dependencies = [
315
327
"atomic-waker",
316
328
"bytes",
···
329
341
330
342
[[package]]
331
343
name = "hyper-util"
332
-
version = "0.1.17"
344
+
version = "0.1.19"
333
345
source = "registry+https://github.com/rust-lang/crates.io-index"
334
-
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
346
+
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
335
347
dependencies = [
336
348
"bytes",
337
349
"futures-core",
···
345
357
346
358
[[package]]
347
359
name = "itoa"
348
-
version = "1.0.15"
360
+
version = "1.0.17"
349
361
source = "registry+https://github.com/rust-lang/crates.io-index"
350
-
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
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"
351
369
352
370
[[package]]
353
371
name = "libc"
354
-
version = "0.2.177"
372
+
version = "0.2.178"
355
373
source = "registry+https://github.com/rust-lang/crates.io-index"
356
-
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
374
+
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
357
375
358
376
[[package]]
359
377
name = "lock_api"
···
366
384
367
385
[[package]]
368
386
name = "log"
369
-
version = "0.4.28"
387
+
version = "0.4.29"
370
388
source = "registry+https://github.com/rust-lang/crates.io-index"
371
-
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
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
+
]
372
399
373
400
[[package]]
374
401
name = "matchit"
···
390
417
391
418
[[package]]
392
419
name = "mio"
393
-
version = "1.1.0"
420
+
version = "1.1.1"
394
421
source = "registry+https://github.com/rust-lang/crates.io-index"
395
-
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
422
+
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
396
423
dependencies = [
397
424
"libc",
398
425
"wasi",
···
406
433
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
407
434
dependencies = [
408
435
"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",
409
445
]
410
446
411
447
[[package]]
···
476
512
477
513
[[package]]
478
514
name = "proc-macro2"
479
-
version = "1.0.103"
515
+
version = "1.0.104"
480
516
source = "registry+https://github.com/rust-lang/crates.io-index"
481
-
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
517
+
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
482
518
dependencies = [
483
519
"unicode-ident",
484
520
]
···
532
568
]
533
569
534
570
[[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]]
535
588
name = "rust-server"
536
589
version = "0.1.0"
537
590
dependencies = [
···
543
596
"serde_json",
544
597
"tokio",
545
598
"tower-http",
599
+
"tracing",
600
+
"tracing-subscriber",
546
601
]
547
602
548
603
[[package]]
···
556
611
557
612
[[package]]
558
613
name = "ryu"
559
-
version = "1.0.20"
614
+
version = "1.0.22"
560
615
source = "registry+https://github.com/rust-lang/crates.io-index"
561
-
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
616
+
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
562
617
563
618
[[package]]
564
619
name = "scopeguard"
···
604
659
605
660
[[package]]
606
661
name = "serde_json"
607
-
version = "1.0.145"
662
+
version = "1.0.148"
608
663
source = "registry+https://github.com/rust-lang/crates.io-index"
609
-
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
664
+
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
610
665
dependencies = [
611
666
"itoa",
612
667
"memchr",
613
-
"ryu",
614
668
"serde",
615
669
"serde_core",
670
+
"zmij",
616
671
]
617
672
618
673
[[package]]
···
650
705
]
651
706
652
707
[[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]]
653
717
name = "signal-hook-registry"
654
-
version = "1.4.6"
718
+
version = "1.4.8"
655
719
source = "registry+https://github.com/rust-lang/crates.io-index"
656
-
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
720
+
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
657
721
dependencies = [
722
+
"errno",
658
723
"libc",
659
724
]
660
725
661
726
[[package]]
662
727
name = "signature"
663
-
version = "3.0.0-rc.5"
728
+
version = "3.0.0-rc.6"
664
729
source = "registry+https://github.com/rust-lang/crates.io-index"
665
-
checksum = "2a0251c9d6468f4ba853b6352b190fb7c1e405087779917c238445eb03993826"
730
+
checksum = "597a96996ccff7dfa16f052bd995b4cecc72af22c35138738dc029f0ead6608d"
666
731
667
732
[[package]]
668
733
name = "smallvec"
···
697
762
698
763
[[package]]
699
764
name = "syn"
700
-
version = "2.0.109"
765
+
version = "2.0.112"
701
766
source = "registry+https://github.com/rust-lang/crates.io-index"
702
-
checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f"
767
+
checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4"
703
768
dependencies = [
704
769
"proc-macro2",
705
770
"quote",
···
713
778
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
714
779
715
780
[[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]]
716
790
name = "tokio"
717
791
version = "1.48.0"
718
792
source = "registry+https://github.com/rust-lang/crates.io-index"
···
758
832
759
833
[[package]]
760
834
name = "tower-http"
761
-
version = "0.6.6"
835
+
version = "0.6.8"
762
836
source = "registry+https://github.com/rust-lang/crates.io-index"
763
-
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
837
+
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
764
838
dependencies = [
765
839
"bitflags",
766
840
"bytes",
767
841
"http",
842
+
"http-body",
768
843
"pin-project-lite",
769
844
"tower-layer",
770
845
"tower-service",
846
+
"tracing",
771
847
]
772
848
773
849
[[package]]
···
784
860
785
861
[[package]]
786
862
name = "tracing"
787
-
version = "0.1.41"
863
+
version = "0.1.44"
788
864
source = "registry+https://github.com/rust-lang/crates.io-index"
789
-
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
865
+
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
790
866
dependencies = [
791
867
"log",
792
868
"pin-project-lite",
869
+
"tracing-attributes",
793
870
"tracing-core",
794
871
]
795
872
796
873
[[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]]
797
885
name = "tracing-core"
798
-
version = "0.1.34"
886
+
version = "0.1.36"
799
887
source = "registry+https://github.com/rust-lang/crates.io-index"
800
-
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
888
+
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
801
889
dependencies = [
802
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"
908
+
source = "registry+https://github.com/rust-lang/crates.io-index"
909
+
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
910
+
dependencies = [
911
+
"matchers",
912
+
"nu-ansi-term",
913
+
"once_cell",
914
+
"regex-automata",
915
+
"sharded-slab",
916
+
"smallvec",
917
+
"thread_local",
918
+
"tracing",
919
+
"tracing-core",
920
+
"tracing-log",
803
921
]
804
922
805
923
[[package]]
···
813
931
version = "1.0.22"
814
932
source = "registry+https://github.com/rust-lang/crates.io-index"
815
933
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"
816
940
817
941
[[package]]
818
942
name = "wasi"
···
911
1035
912
1036
[[package]]
913
1037
name = "zerocopy"
914
-
version = "0.8.27"
1038
+
version = "0.8.31"
915
1039
source = "registry+https://github.com/rust-lang/crates.io-index"
916
-
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
1040
+
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
917
1041
dependencies = [
918
1042
"zerocopy-derive",
919
1043
]
920
1044
921
1045
[[package]]
922
1046
name = "zerocopy-derive"
923
-
version = "0.8.27"
1047
+
version = "0.8.31"
924
1048
source = "registry+https://github.com/rust-lang/crates.io-index"
925
-
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
1049
+
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
926
1050
dependencies = [
927
1051
"proc-macro2",
928
1052
"quote",
···
934
1058
version = "1.8.2"
935
1059
source = "registry+https://github.com/rust-lang/crates.io-index"
936
1060
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"
+3
-1
server/Cargo.toml
+3
-1
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"]}
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"] }
+93
server/src/auth.rs
+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
+
}
+36
-52
server/src/handlers.rs
+36
-52
server/src/handlers.rs
···
3
3
use ed25519_dalek::VerifyingKey;
4
4
use nanoid::nanoid;
5
5
6
-
use crate::types::*;
7
-
8
-
macro_rules! my_err {
9
-
($msg:expr) => {
10
-
Err(MyErr($msg))
11
-
};
12
-
}
6
+
use crate::{ReqBail, SrvErr, types::*};
13
7
14
8
pub async fn create_user(
15
9
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
16
10
Json(payload): Json<CreateUserRequest>,
17
-
) -> Result<Json<CreateAccountResponse>, MyErr> {
11
+
) -> Result<Json<CreateAccountResponse>, SrvErr> {
18
12
let key_used = { state.signup_keys.lock().await.remove(&payload.signup_key) };
19
13
20
14
if !key_used {
21
-
return my_err!("Signup key was not there");
15
+
ReqBail!("Signup key was not there");
22
16
}
23
17
24
18
// todo check
25
19
let pub_key_bytes = match BASE64_STANDARD.decode(&payload.pub_key_b64) {
26
20
Ok(b) => b,
27
-
Err(_) => return my_err!("Invalid base64 public key"),
21
+
Err(_) => ReqBail!("Invalid base64 public key"),
28
22
};
29
23
30
24
// todo check
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
-
};
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))?;
39
31
40
32
let user_id = nanoid!(5);
41
33
let mut is_admin = false;
···
54
46
return Ok(Json(CreateAccountResponse { user_id, is_admin }));
55
47
}
56
48
57
-
pub async fn generate_signup_key(State(state): State<AppState>) -> Result<String, MyErr> {
49
+
pub async fn generate_signup_key(State(state): State<AppState>) -> Result<String, SrvErr> {
58
50
let new_signup_key = nanoid!(5);
59
51
let mut signup_keys = state.signup_keys.lock().await;
60
52
···
64
56
return Ok(new_signup_key);
65
57
}
66
58
67
-
pub async fn create_friend_request(
59
+
pub async fn request_friend_request(
68
60
State(state): State<AppState>,
69
61
Extension(user_id): Extension<String>,
70
-
accepter_id: String,
71
-
) -> Result<(), MyErr> {
72
-
if accepter_id == user_id {
73
-
return my_err!("Cannot friend yourself");
62
+
friend_id: String,
63
+
) -> Result<(), SrvErr> {
64
+
if friend_id == user_id {
65
+
ReqBail!("Cannot friend yourself");
74
66
}
75
67
76
68
let mut friend_requests = state.friend_requests.lock().await;
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
-
}
69
+
let link = Link::new(friend_id, user_id);
84
70
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);
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);
91
76
92
-
let friend_request_accepted = { state.friend_requests.lock().await.remove(&link) };
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);
93
80
94
-
if !friend_request_accepted {
95
-
return my_err!("Friend request not found");
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);
96
87
}
97
88
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
-
105
89
return Ok(());
106
90
}
107
91
···
109
93
State(state): State<AppState>,
110
94
Extension(user_id): Extension<String>,
111
95
friend_id: String,
112
-
) -> Result<PlainBool, MyErr> {
96
+
) -> Result<PlainBool, SrvErr> {
113
97
let link = Link::new(friend_id, user_id);
114
98
let links = state.links.lock().await;
115
99
let accepted = links.contains(&link);
···
120
104
State(state): State<AppState>,
121
105
Extension(user_id): Extension<String>,
122
106
Json(pings): Json<Vec<PingPayload>>,
123
-
) -> Result<(), MyErr> {
107
+
) -> Result<(), SrvErr> {
124
108
let links = state.links.lock().await;
125
109
for ping in &pings {
126
110
let link = Link::new(user_id.clone(), ping.receiver_id.clone());
127
111
if !links.contains(&link) {
128
-
return my_err!("Ping receiver is not linked to sender");
112
+
ReqBail!("Ping receiver is not linked to sender");
129
113
}
130
114
}
131
115
drop(links);
···
147
131
State(state): State<AppState>,
148
132
Extension(user_id): Extension<String>,
149
133
sender_id: String,
150
-
) -> Result<EncryptedPingVec, MyErr> {
134
+
) -> Result<EncryptedPingVec, SrvErr> {
151
135
let link = Link::new(user_id, sender_id);
152
136
let links = state.links.lock().await;
153
137
154
138
if !links.contains(&link) {
155
-
return my_err!("No link exists between these users");
139
+
ReqBail!("No link exists between these users");
156
140
}
157
141
drop(links);
158
142
+208
server/src/log.rs
+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
+
}
+19
-86
server/src/main.rs
+19
-86
server/src/main.rs
···
1
1
use std::{collections::HashMap, sync::Arc};
2
2
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;
3
+
use axum::{Router, routing::post};
13
4
use nanoid::nanoid;
14
-
use serde::Deserialize;
15
5
use std::collections::HashSet;
16
6
use tokio::sync::Mutex;
17
-
use tower_http::cors::{Any, CorsLayer};
7
+
use tower_http::cors::CorsLayer;
8
+
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
18
9
10
+
mod auth;
19
11
mod handlers;
12
+
mod log;
20
13
mod types;
21
14
22
15
use handlers::*;
23
16
use types::*;
24
17
18
+
use crate::auth::auth_test;
19
+
use crate::log::log_req_res_bodies;
20
+
25
21
#[tokio::main]
26
22
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
+
27
31
// TODO: should this be inside an Arc?
28
32
let state = AppState {
29
33
users: Arc::new(Mutex::new(Vec::new())),
···
46
50
.route("/", post(|| async { "You just sent a POST to /" })) // for testing
47
51
.route("/create-account", post(create_user))
48
52
.route("/generate-signup-key", post(generate_signup_key))
49
-
.route("/create-friend-request", post(create_friend_request))
50
-
.route("/accept-friend-request", post(accept_friend_request))
53
+
.route("/request-friend-request", post(request_friend_request))
51
54
.route(
52
55
"/is-friend-request-accepted",
53
56
post(is_friend_request_accepted),
···
56
59
.route("/get-pings", post(get_pings))
57
60
.with_state(state.clone())
58
61
.layer(axum::middleware::from_fn_with_state(state, auth_test))
62
+
.layer(axum::middleware::from_fn(log_req_res_bodies))
59
63
.layer(CorsLayer::permissive());
60
64
61
65
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
62
66
axum::serve(listener, app).await.unwrap();
63
67
}
64
68
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
-
}
69
+
// TODO: potential security risk of returning error messages directly to the user. nice for debugging tho :p
+57
-6
server/src/types.rs
+57
-6
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
+
25
31
pub struct RingBuffer {
26
32
pub ring: Box<[Option<EncryptedPing>]>,
27
33
pub idx: usize,
···
80
86
pub id: String,
81
87
pub pub_key: VerifyingKey,
82
88
}
83
-
// pub struct User(pub String);
84
89
85
90
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
86
91
pub struct Link(String, String);
···
116
121
117
122
impl IntoResponse for EncryptedPingVec {
118
123
fn into_response(self) -> Response {
119
-
axum::Json(self.0).into_response() // TODO: check if this is correct
124
+
axum::Json(self.0).into_response()
120
125
}
121
126
}
122
127
123
-
pub struct MyErr(pub &'static str);
128
+
#[derive(Debug)]
129
+
pub struct SrvErr {
130
+
pub msg: String,
131
+
pub cause: Option<String>,
132
+
}
124
133
125
-
impl IntoResponse for MyErr {
134
+
impl IntoResponse for SrvErr {
126
135
fn into_response(self) -> Response {
127
-
let body = format!(r#"{{"error":"{}"}}"#, self.0); // example: {"error":"something went wrong"}
128
-
return (StatusCode::INTERNAL_SERVER_ERROR, body).into_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()
129
152
}
130
153
}
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
-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
-
}
+2
-12
server/test/test.test.ts
+2
-12
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 {
4
-
describe,
5
-
test,
6
-
expect,
7
-
beforeAll,
8
-
afterAll,
9
-
beforeEach,
10
-
} from "bun:test";
3
+
import { describe, test, expect, beforeAll, afterAll, beforeEach } from "bun:test";
11
4
import { generateUser, post, URL } from "./utils.ts";
12
5
13
6
console.log(`SERVER_DIR: ${SERVER_DIR}`);
···
54
47
55
48
const res3 = await post("get-pings", user, admin.user_id);
56
49
57
-
expect(JSON.parse(res3)).toEqual([
58
-
"this is definitely encrypted trust #2",
59
-
"this is definitely encrypted trust #1",
60
-
]);
50
+
expect(JSON.parse(res3)).toEqual(["this is definitely encrypted trust #2", "this is definitely encrypted trust #1"]);
61
51
});
62
52
});