+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
+15
-1
app/README.md
+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
+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
+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
-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
-
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
-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
+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
+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
+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
-2
app/src/settings-page/settings.ts
+3
-3
app/src/signup-page/signup.css
+3
-3
app/src/signup-page/signup.css
+7
-34
app/src/signup-page/signup.html
+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
+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
-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
+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
+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
-
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
+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
+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
-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
+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
-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
+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
+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
+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
+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
});