+26
-2
.gitignore
+26
-2
.gitignore
···
1
+
# custom
2
output/
3
server/target
4
+
5
+
# Logs
6
+
logs
7
+
*.log
8
+
npm-debug.log*
9
+
yarn-debug.log*
10
+
yarn-error.log*
11
+
pnpm-debug.log*
12
+
lerna-debug.log*
13
+
14
+
node_modules
15
+
dist
16
+
dist-ssr
17
+
*.local
18
+
19
+
# Editor directories and files
20
+
.vscode/*
21
+
!.vscode/extensions.json
22
+
.idea
23
+
.DS_Store
24
+
*.suo
25
+
*.ntvs*
26
+
*.njsproj
27
+
*.sln
28
+
*.sw?
-24
app/.gitignore
-24
app/.gitignore
···
1
-
# Logs
2
-
logs
3
-
*.log
4
-
npm-debug.log*
5
-
yarn-debug.log*
6
-
yarn-error.log*
7
-
pnpm-debug.log*
8
-
lerna-debug.log*
9
-
10
-
node_modules
11
-
dist
12
-
dist-ssr
13
-
*.local
14
-
15
-
# Editor directories and files
16
-
.vscode/*
17
-
!.vscode/extensions.json
18
-
.idea
19
-
.DS_Store
20
-
*.suo
21
-
*.ntvs*
22
-
*.njsproj
23
-
*.sln
24
-
*.sw?
···
+1
app/README.md
+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
+18
-17
app/bun.lock
+18
-17
app/bun.lock
···
1
{
2
"lockfileVersion": 1,
3
"workspaces": {
4
"": {
5
"name": "privacypin",
6
"dependencies": {
7
-
"@tauri-apps/api": "^2",
8
-
"@tauri-apps/plugin-opener": "^2",
9
"@tauri-apps/plugin-store": "^2.4.1",
10
"alpinejs": "^3.15.1",
11
},
12
"devDependencies": {
13
-
"@tauri-apps/cli": "^2",
14
"@types/alpinejs": "^3.13.11",
15
-
"typescript": "~5.6.2",
16
-
"vite": "^6.0.3",
17
},
18
},
19
},
···
116
117
"@tauri-apps/api": ["@tauri-apps/api@2.9.0", "", {}, "sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw=="],
118
119
-
"@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
121
-
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W8FQXZXQmQ0Fmj9UJXNrm2mLdIaLLriKVY7o/FzmizyIKTPIvHjfZALTNybbpTQRbJvKoGHLrW1DNzAWVDWJYg=="],
122
123
-
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-zDwu40rlshijt3TU6aRvzPUyVpapsx1sNfOlreDMTaMelQLHl6YoQzSRpLHYwrHrhimxyX2uDqnKIiuGel0Lhg=="],
124
125
-
"@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
127
-
"@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
129
-
"@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
131
-
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.9.3", "", { "os": "linux", "cpu": "none" }, "sha512-qV8DZXI/fZwawk6T3Th1g6smiNC2KeQTk7XFgKvqZ6btC01z3UTsQmNGvI602zwm3Ld1TBZb4+rEWu2QmQimmw=="],
132
133
-
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tquyEONCNRfqEBWEe4eAHnxFN5yY5lFkCuD4w79XLIovUxVftQ684+xLp7zkhntkt4y20SMj2AgJa/+MOlx4Kg=="],
134
135
-
"@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
137
-
"@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
139
-
"@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
141
-
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.3", "", { "os": "win32", "cpu": "x64" }, "sha512-fmw7NrrHE5m49idCvJAx9T9bsupjdJ0a3p3DPCNCZRGANU6R1tA1L+KTlVuUtdAldX2NqU/9UPo2SCslYKgJHQ=="],
142
143
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
144
···
1
{
2
"lockfileVersion": 1,
3
+
"configVersion": 0,
4
"workspaces": {
5
"": {
6
"name": "privacypin",
7
"dependencies": {
8
+
"@tauri-apps/api": "^2.9.0",
9
+
"@tauri-apps/plugin-opener": "^2.5.2",
10
"@tauri-apps/plugin-store": "^2.4.1",
11
"alpinejs": "^3.15.1",
12
},
13
"devDependencies": {
14
+
"@tauri-apps/cli": "^2.9.4",
15
"@types/alpinejs": "^3.13.11",
16
+
"typescript": "~5.6.3",
17
+
"vite": "^6.4.1",
18
},
19
},
20
},
···
117
118
"@tauri-apps/api": ["@tauri-apps/api@2.9.0", "", {}, "sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw=="],
119
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
122
+
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9rHkMVtbMhe0AliVbrGpzMahOBg3rwV46JYRELxR9SN6iu1dvPOaMaiC4cP6M/aD1424ziXnnMdYU06RAH8oIw=="],
123
124
+
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-VT9ymNuT06f5TLjCZW2hfSxbVtZDhORk7CDUDYiq5TiSYQdxkl8MVBy0CCFFcOk4QAkUmqmVUA9r3YZ/N/vPRQ=="],
125
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
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
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
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
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
136
+
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zcd1QVffh5tZs1u1SCKUV/V7RRynebgYUNWHuV0FsIF1MjnULUChEXhAhug7usCDq4GZReMJOoXa6rukEozWIw=="],
137
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
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
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
144
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
145
-58
app/index copy.html
-58
app/index copy.html
···
1
-
<!doctype html>
2
-
<html lang="en">
3
-
<head>
4
-
<meta charset="UTF-8" />
5
-
<title>Position Share โ Signup</title>
6
-
<script type="module" src="/src/main.ts"></script>
7
-
<link rel="stylesheet" href="/src/styles.css" />
8
-
</head>
9
-
10
-
<body>
11
-
<div class="card">
12
-
<div class="header">
13
-
<div class="icon-circle">
14
-
<img src="./src/assets/pin.svg" alt="Pin Icon" />
15
-
</div>
16
-
<h1>Position Share</h1>
17
-
<p>Connect with your server to start sharing</p>
18
-
</div>
19
-
20
-
<!-- x-data connects this element to the signupState Alpine component, enabling its data (serverAddress and signupKey) and functions (signup and scanQR) to work within it :) -->
21
-
<div class="actions" x-data="signupState">
22
-
<div>
23
-
<label for="server">Server Address</label>
24
-
<input id="server" type="url" placeholder="https://your-server.com" x-model="serverAddress" required />
25
-
</div>
26
-
27
-
<div>
28
-
<label for="key">Signup Key</label>
29
-
<input id="key" type="password" placeholder="Enter your signup key" x-model="signupKey" required />
30
-
</div>
31
-
32
-
<p class="hint">Scan a QR code to automatically fill both server address and signup key</p>
33
-
<button type="button" class="btn-qr" @click="scanQR">
34
-
<img src="./src/assets/qr.svg" alt="QR Icon" />
35
-
Scan QR Code
36
-
</button>
37
-
38
-
<button class="btn-primary" @click="signup">Connect</button>
39
-
</div>
40
-
</div>
41
-
42
-
<script>
43
-
function signupState() {
44
-
return {
45
-
serverAddress: "",
46
-
signupKey: "",
47
-
// we have the functions within a bigger function because this is how we can access the variables we define (by using this.variableWeWant)
48
-
signup() {
49
-
alert(this.serverAddress);
50
-
},
51
-
scanQR() {
52
-
alert(this.signupKey);
53
-
},
54
-
};
55
-
}
56
-
</script>
57
-
</body>
58
-
</html>
···
-61
app/index.html
-61
app/index.html
···
1
-
<!doctype html>
2
-
<html lang="en">
3
-
<head>
4
-
<meta charset="UTF-8" />
5
-
<script type="module">
6
-
import Alpine from "alpinejs";
7
-
window.Alpine = Alpine;
8
-
Alpine.start();
9
-
</script>
10
-
<link rel="stylesheet" href="/src/styles.css" />
11
-
</head>
12
-
13
-
<body>
14
-
<div class="card">
15
-
<div class="header">
16
-
<div class="icon-circle">
17
-
<img src="./src/assets/pin.svg" alt="Pin Icon" />
18
-
</div>
19
-
<h1>Position Share</h1>
20
-
<p>Connect with your server to start sharing</p>
21
-
</div>
22
-
23
-
<!-- x-data connects this element to the signupState Alpine component, enabling its data (serverAddress and signupKey) and functions (signup and scanQR) to work within it :) -->
24
-
<div class="actions" x-data="signupState">
25
-
<div>
26
-
<label for="server">Server Address</label>
27
-
<input id="server" type="url" placeholder="https://your-server.com" x-model="serverAddress" required />
28
-
</div>
29
-
30
-
<div>
31
-
<label for="key">Signup Key</label>
32
-
<input id="key" type="password" placeholder="Enter your signup key" x-model="signupKey" required />
33
-
</div>
34
-
35
-
<p class="hint">Scan a QR code to automatically fill both server address and signup key</p>
36
-
<button type="button" class="btn-qr" @click="scanQR">
37
-
<img src="./src/assets/qr.svg" alt="QR Icon" />
38
-
Scan QR Code
39
-
</button>
40
-
41
-
<button class="btn-primary" @click="signup">Connect</button>
42
-
</div>
43
-
</div>
44
-
45
-
<script>
46
-
function signupState() {
47
-
return {
48
-
serverAddress: "",
49
-
signupKey: "",
50
-
// we have the functions within a bigger function because this is how we can access the variables we define (by using this.variableWeWant)
51
-
signup() {
52
-
alert(this.serverAddress);
53
-
},
54
-
scanQR() {
55
-
alert(this.signupKey);
56
-
},
57
-
};
58
-
}
59
-
</script>
60
-
</body>
61
-
</html>
···
+5
-5
app/package.json
+5
-5
app/package.json
···
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
}
24
}
···
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
}
24
}
-69
app/src/api.ts
-69
app/src/api.ts
···
1
-
import { Store } from "./store.ts";
2
-
3
-
// TODO: test if this is still needed:
4
-
// Don't mind this piece of code, it's a polyfill until chromium decides to merge it (it's been so long)
5
-
// @ts-ignore
6
-
// Uint8Array.prototype.toBase64 = function () {
7
-
// let binary = "";
8
-
// for (let i = 0; i < this.length; i++) {
9
-
// const byte = this[i];
10
-
// if (byte !== undefined) {
11
-
// binary += String.fromCharCode(byte);
12
-
// }
13
-
// }
14
-
// return btoa(binary);
15
-
// };
16
-
17
-
export async function createAccount(signup_key: string): Promise<{ user_id: string; is_admin: boolean }> {
18
-
try {
19
-
const server_url = await Store.get("server_url");
20
-
21
-
const response = await fetch(server_url + "/create-account", {
22
-
method: "POST",
23
-
body: signup_key,
24
-
});
25
-
26
-
if (!response.ok) {
27
-
throw new Error(`${await response.text()}`);
28
-
}
29
-
return await response.json();
30
-
} catch (err) {
31
-
alert(`${err}`);
32
-
throw err;
33
-
}
34
-
}
35
-
36
-
export async function post(endpoint: string, data: Object | string | undefined): Promise<any> {
37
-
try {
38
-
const user_id = await Store.get("user_id");
39
-
const server_url = await Store.get("server_url");
40
-
41
-
const headers: Record<string, string> = {
42
-
"x-auth": JSON.stringify({ user_id }),
43
-
};
44
-
45
-
let stringified_data: string | undefined;
46
-
47
-
if (typeof data === "object") {
48
-
stringified_data = JSON.stringify(data);
49
-
headers["Content-Type"] = "application/json";
50
-
} else {
51
-
stringified_data = data;
52
-
}
53
-
54
-
const res = await fetch(`${server_url}/${endpoint}`, {
55
-
method: "POST",
56
-
headers,
57
-
body: stringified_data,
58
-
});
59
-
60
-
if (!res.ok) {
61
-
throw new Error(`${await res.text()}`);
62
-
}
63
-
64
-
return await res.text();
65
-
} catch (err) {
66
-
alert(`${err}`);
67
-
throw err;
68
-
}
69
-
}
···
+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>
+6
app/src/assets/three-dots.svg
+6
app/src/assets/three-dots.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 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+
<path d="M8 12C9.10457 12 10 12.8954 10 14C10 15.1046 9.10457 16 8 16C6.89543 16 6 15.1046 6 14C6 12.8954 6.89543 12 8 12Z" fill="#000000"/>
4
+
<path d="M8 6C9.10457 6 10 6.89543 10 8C10 9.10457 9.10457 10 8 10C6.89543 10 6 9.10457 6 8C6 6.89543 6.89543 6 8 6Z" fill="#000000"/>
5
+
<path d="M10 2C10 0.89543 9.10457 -4.82823e-08 8 0C6.89543 4.82823e-08 6 0.895431 6 2C6 3.10457 6.89543 4 8 4C9.10457 4 10 3.10457 10 2Z" fill="#000000"/>
6
+
</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>
+152
app/src/home-page/home.css
+152
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
+
body {
20
+
font-family: sans-serif;
21
+
background: #f9fafb;
22
+
margin: 0;
23
+
display: flex;
24
+
justify-content: center;
25
+
}
26
+
27
+
.app {
28
+
width: 100%;
29
+
background: #f9fafb;
30
+
}
31
+
32
+
.svg-icon {
33
+
width: 25px;
34
+
height: 25px;
35
+
margin: auto;
36
+
}
37
+
38
+
header {
39
+
background: #fff;
40
+
border-bottom: 1px solid #e5e7eb;
41
+
padding: 0.75rem 1rem;
42
+
display: flex;
43
+
justify-content: space-between;
44
+
}
45
+
46
+
header h1 {
47
+
font-size: 1rem;
48
+
margin: 0;
49
+
display: flex;
50
+
align-items: center;
51
+
}
52
+
53
+
header .icon-btn {
54
+
border: none;
55
+
background: none;
56
+
cursor: pointer;
57
+
padding: 0.4rem;
58
+
}
59
+
60
+
.status {
61
+
background: #fff;
62
+
border-bottom: 1px solid #f3f4f6;
63
+
padding: 0.75rem 1rem;
64
+
display: flex;
65
+
justify-content: space-between;
66
+
align-items: center;
67
+
font-size: 0.9rem;
68
+
}
69
+
70
+
.content {
71
+
flex: 1;
72
+
overflow-y: auto;
73
+
padding: 1rem;
74
+
}
75
+
76
+
.friends-header {
77
+
display: flex;
78
+
align-items: center;
79
+
gap: 0.5rem;
80
+
margin-bottom: 1rem;
81
+
}
82
+
83
+
.friend-card {
84
+
background: #fff;
85
+
border: 1px solid #e5e7eb;
86
+
border-radius: 8px;
87
+
padding: 1rem;
88
+
margin-bottom: 0.75rem;
89
+
display: flex;
90
+
justify-content: space-between;
91
+
align-items: center;
92
+
}
93
+
94
+
.friend-actions {
95
+
display: flex;
96
+
align-items: center;
97
+
gap: 0.1rem;
98
+
}
99
+
100
+
.friend-actions .view-btn {
101
+
cursor: pointer;
102
+
border-radius: 6px;
103
+
border: 1px solid #d1d5db;
104
+
background: #fff;
105
+
padding: 0.3rem 0.5rem;
106
+
font-size: 0.8rem;
107
+
}
108
+
109
+
.friend-actions .view-btn:hover {
110
+
background: #f3f4f6;
111
+
}
112
+
113
+
.menu-icon {
114
+
width: 16px;
115
+
height: 16px;
116
+
margin: 0;
117
+
}
118
+
119
+
.friend-actions {
120
+
cursor: pointer;
121
+
font-size: 1.2rem;
122
+
color: #6b7280;
123
+
padding: 0 0.3rem;
124
+
user-select: none;
125
+
}
126
+
127
+
.friend-actions {
128
+
color: #111827;
129
+
}
130
+
131
+
.empty-state {
132
+
text-align: center;
133
+
padding: 3rem 1rem;
134
+
color: #6b7280;
135
+
}
136
+
137
+
.empty-state button {
138
+
margin-top: 0.75rem;
139
+
padding: 0.5rem 1rem;
140
+
border: none;
141
+
border-radius: 6px;
142
+
background: #3b82f6;
143
+
color: white;
144
+
cursor: pointer;
145
+
}
146
+
147
+
.friend-actions button img {
148
+
width: 16px;
149
+
height: 16px;
150
+
vertical-align: middle;
151
+
margin-right: 4px;
152
+
}
+119
app/src/home-page/home.html
+119
app/src/home-page/home.html
···
···
1
+
<!doctype html>
2
+
<html lang="en">
3
+
<head>
4
+
<meta charset="UTF-8" />
5
+
<script type="module" src="./home.ts"></script>
6
+
<link rel="stylesheet" href="./home.css" />
7
+
</head>
8
+
9
+
<body>
10
+
<div class="app" x-data="homePageState">
11
+
<!-- Header -->
12
+
<header>
13
+
<h1>PrivacyPin</h1>
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
+
/>
37
+
</button>
38
+
</div>
39
+
</header>
40
+
41
+
<!-- Status -->
42
+
<div class="status">
43
+
<span>Last ping sent:</span>
44
+
<!-- later, figure out how to update this cleanly when we will have the actual data -->
45
+
<span x-text="timeAgo()"></span>
46
+
</div>
47
+
48
+
<!-- Friends -->
49
+
<div class="content">
50
+
<div class="friends-header">
51
+
<h2 style="font-size: 1rem; margin: 0">Friends</h2>
52
+
<span style="color: #6b7280; font-size: 0.9rem"
53
+
>(<span x-text="friends.length"></span>)</span
54
+
>
55
+
</div>
56
+
57
+
<template x-if="friends.length > 0">
58
+
<div>
59
+
<template x-for="friend in friends" :key="friend.id">
60
+
<div class="friend-card">
61
+
<strong x-text="friend.name"></strong>
62
+
<div class="friend-actions">
63
+
<button
64
+
class="view-btn"
65
+
@click="viewLocation(friend.id)"
66
+
>
67
+
<img
68
+
class="svg-icon"
69
+
src="/src/assets/pin-location.svg"
70
+
alt="Pin Icon"
71
+
/>View
72
+
</button>
73
+
<a
74
+
class="menu-icon"
75
+
style="margin-bottom: auto"
76
+
@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>
84
+
</div>
85
+
</div>
86
+
</template>
87
+
</div>
88
+
</template>
89
+
90
+
<template x-if="friends.length === 0">
91
+
<div class="empty-state">
92
+
<div style="font-size: 2rem">๐ฅ</div>
93
+
<h3>No friends yet</h3>
94
+
<p>Add friends to start sharing locations</p>
95
+
<button @click="addFriend()">Add Friend</button>
96
+
</div>
97
+
</template>
98
+
</div>
99
+
100
+
<!-- Admin -->
101
+
<div x-if="isAdmin()">
102
+
<div class="content" style="text-align: center">
103
+
<h4>Admin Controls:</h4>
104
+
<div style="display: flex; justify-content: center">
105
+
<button @click="generateSignupKey()">
106
+
Generate signup key
107
+
</button>
108
+
</div>
109
+
<div
110
+
style="display: flex; justify-content: center"
111
+
x-show="newSignupKey != ''"
112
+
>
113
+
<p>New signup key: <a x-text="newSignupKey"></a></p>
114
+
</div>
115
+
</div>
116
+
</div>
117
+
</div>
118
+
</body>
119
+
</html>
+47
app/src/home-page/home.ts
+47
app/src/home-page/home.ts
···
···
1
+
import Alpine from "alpinejs";
2
+
import { goto } from "../utils/tools.ts";
3
+
import { Store } from "../utils/store.ts";
4
+
import * as api from "../utils/api.ts";
5
+
6
+
Alpine.data("homePageState", () => ({
7
+
friends: [
8
+
{ id: "123", name: "Alice Johnson" },
9
+
{ id: "456", name: "Bob Smith" },
10
+
{ id: "789", name: "Carol Davis" },
11
+
],
12
+
newSignupKey: "",
13
+
14
+
timeAgo() {
15
+
return "2m ago";
16
+
},
17
+
18
+
viewLocation(friend_id: string) {
19
+
alert(`Opening the location for the friend with id ${friend_id}`);
20
+
},
21
+
22
+
friendOptions(friend_id: string) {
23
+
alert(`Options for friend id ${friend_id}`);
24
+
},
25
+
26
+
async updateServer() {
27
+
await api.sendPings("123", "3.14159N 3.14159W");
28
+
},
29
+
30
+
addFriend() {
31
+
alert("Add friend functionality would open here");
32
+
},
33
+
34
+
openSettings() {
35
+
goto("settings");
36
+
},
37
+
38
+
async generateSignupKey() {
39
+
this.newSignupKey = await api.generateSignupKey();
40
+
},
41
+
42
+
isAdmin() {
43
+
return Store.get("is_admin");
44
+
},
45
+
}));
46
+
47
+
Alpine.start();
-1
app/src/main.ts
-1
app/src/main.ts
···
1
-
// idk empty for now, might use later
···
+116
app/src/settings-page/settings.css
+116
app/src/settings-page/settings.css
···
···
1
+
body {
2
+
font-family: system-ui, sans-serif;
3
+
background: #f9fafb;
4
+
display: flex;
5
+
align-items: center;
6
+
justify-content: center;
7
+
height: 100vh;
8
+
margin: 0;
9
+
}
10
+
11
+
.card {
12
+
max-width: 90%;
13
+
background: white;
14
+
border: 1px solid #d1d5db;
15
+
border-radius: 8px;
16
+
padding: 1.5rem;
17
+
box-sizing: border-box;
18
+
}
19
+
20
+
.header {
21
+
text-align: center;
22
+
margin-bottom: 1.5rem;
23
+
}
24
+
25
+
.icon-circle {
26
+
width: 64px;
27
+
height: 64px;
28
+
background: #dbeafe;
29
+
border-radius: 50%;
30
+
display: flex;
31
+
align-items: center;
32
+
justify-content: center;
33
+
margin: 0 auto 1rem;
34
+
}
35
+
36
+
.icon-circle img {
37
+
width: 32px;
38
+
height: 32px;
39
+
}
40
+
41
+
h1 {
42
+
font-size: 1.5rem;
43
+
}
44
+
45
+
p {
46
+
font-size: 0.9rem;
47
+
color: #6b7280;
48
+
}
49
+
50
+
.actions {
51
+
display: flex;
52
+
flex-direction: column;
53
+
gap: 1rem;
54
+
}
55
+
56
+
label {
57
+
display: block;
58
+
font-size: 0.85rem;
59
+
font-weight: 600;
60
+
margin-bottom: 0.25rem;
61
+
}
62
+
63
+
input {
64
+
width: 100%;
65
+
padding: 0.5rem 0.75rem;
66
+
border: 1px solid #d1d5db;
67
+
border-radius: 4px;
68
+
font-size: 0.95rem;
69
+
box-sizing: border-box;
70
+
}
71
+
72
+
input:focus {
73
+
outline: none;
74
+
border-color: #2563eb;
75
+
}
76
+
77
+
button {
78
+
width: 100%;
79
+
padding: 0.6rem;
80
+
font-size: 0.95rem;
81
+
border-radius: 4px;
82
+
cursor: pointer;
83
+
/*transition: background 0.2s ease;*/
84
+
display: flex;
85
+
align-items: center;
86
+
justify-content: center;
87
+
}
88
+
89
+
.btn-primary {
90
+
background: #2563eb;
91
+
color: white;
92
+
border: none;
93
+
}
94
+
95
+
.btn-primary:hover {
96
+
background: #1d4ed8;
97
+
}
98
+
99
+
.btn-secondary {
100
+
background: white;
101
+
gap: 0.5rem;
102
+
border: 1px solid #d1d5db;
103
+
}
104
+
105
+
.btn-secondary:hover {
106
+
background: #f3f4f6;
107
+
}
108
+
109
+
.btn-secondary img {
110
+
width: 16px;
111
+
height: 16px;
112
+
}
113
+
114
+
.hint {
115
+
font-size: 0.75rem;
116
+
}
+24
app/src/settings-page/settings.html
+24
app/src/settings-page/settings.html
···
···
1
+
<!doctype html>
2
+
<html lang="en">
3
+
<head>
4
+
<meta charset="UTF-8" />
5
+
<script type="module" src="./settings.ts"></script>
6
+
<link rel="stylesheet" href="./settings.css" />
7
+
</head>
8
+
9
+
<body>
10
+
<div class="card">
11
+
<div class="actions" x-data="settingsPageState">
12
+
<h3>Settings</h3>
13
+
14
+
<button class="btn-primary" @click="goto('home')">
15
+
Back to Home
16
+
</button>
17
+
18
+
<button class="btn-secondary" @click="await debugLogout()">
19
+
Signout
20
+
</button>
21
+
</div>
22
+
</div>
23
+
</body>
24
+
</html>
+15
app/src/settings-page/settings.ts
+15
app/src/settings-page/settings.ts
···
···
1
+
import Alpine from "alpinejs";
2
+
import { Store } from "../utils/store.ts";
3
+
import { goto } from "../utils/tools.ts";
4
+
5
+
Alpine.data("settingsPageState", () => ({
6
+
async debugLogout() {
7
+
await Store.reset();
8
+
goto("signup");
9
+
},
10
+
goto(newLocation: string) {
11
+
goto(newLocation);
12
+
},
13
+
}));
14
+
15
+
Alpine.start();
+116
app/src/signup-page/signup.css
+116
app/src/signup-page/signup.css
···
···
1
+
body {
2
+
font-family: system-ui, sans-serif;
3
+
background: #f9fafb;
4
+
display: flex;
5
+
align-items: center;
6
+
justify-content: center;
7
+
height: 100vh;
8
+
margin: 0;
9
+
}
10
+
11
+
.card {
12
+
max-width: 90%;
13
+
background: white;
14
+
border: 1px solid #d1d5db;
15
+
border-radius: 8px;
16
+
padding: 1.5rem;
17
+
box-sizing: border-box;
18
+
}
19
+
20
+
.header {
21
+
text-align: center;
22
+
margin-bottom: 1.5rem;
23
+
}
24
+
25
+
.icon-circle {
26
+
width: 64px;
27
+
height: 64px;
28
+
background: #dbeafe;
29
+
border-radius: 50%;
30
+
display: flex;
31
+
align-items: center;
32
+
justify-content: center;
33
+
margin: 0 auto 1rem;
34
+
}
35
+
36
+
.icon-circle img {
37
+
width: 48px;
38
+
height: 48px;
39
+
}
40
+
41
+
h1 {
42
+
font-size: 1.5rem;
43
+
}
44
+
45
+
p {
46
+
font-size: 0.9rem;
47
+
color: #6b7280;
48
+
}
49
+
50
+
.actions {
51
+
display: flex;
52
+
flex-direction: column;
53
+
gap: 1rem;
54
+
}
55
+
56
+
label {
57
+
display: block;
58
+
font-size: 0.85rem;
59
+
font-weight: 600;
60
+
margin-bottom: 0.25rem;
61
+
}
62
+
63
+
input {
64
+
width: 100%;
65
+
padding: 0.5rem 0.75rem;
66
+
border: 1px solid #d1d5db;
67
+
border-radius: 4px;
68
+
font-size: 0.95rem;
69
+
box-sizing: border-box;
70
+
}
71
+
72
+
input:focus {
73
+
outline: none;
74
+
border-color: #2563eb;
75
+
}
76
+
77
+
button {
78
+
width: 100%;
79
+
padding: 0.6rem;
80
+
font-size: 0.95rem;
81
+
border-radius: 4px;
82
+
cursor: pointer;
83
+
/*transition: background 0.2s ease;*/
84
+
display: flex;
85
+
align-items: center;
86
+
justify-content: center;
87
+
}
88
+
89
+
.btn-primary {
90
+
background: #2563eb;
91
+
color: white;
92
+
border: none;
93
+
}
94
+
95
+
.btn-primary:hover {
96
+
background: #1d4ed8;
97
+
}
98
+
99
+
.btn-qr {
100
+
background: white;
101
+
gap: 0.5rem;
102
+
border: 1px solid #d1d5db;
103
+
}
104
+
105
+
.btn-qr:hover {
106
+
background: #f3f4f6;
107
+
}
108
+
109
+
.btn-qr img {
110
+
width: 20px;
111
+
height: 16px;
112
+
}
113
+
114
+
.hint {
115
+
font-size: 0.75rem;
116
+
}
+69
app/src/signup-page/signup.html
+69
app/src/signup-page/signup.html
···
···
1
+
<!doctype html>
2
+
<html lang="en">
3
+
<head>
4
+
<meta charset="UTF-8" />
5
+
<script type="module" src="./signup.ts"></script>
6
+
<link rel="stylesheet" href="./signup.css" />
7
+
</head>
8
+
9
+
<body>
10
+
<div class="card">
11
+
<div class="header">
12
+
<div class="icon-circle">
13
+
<img src="/src/assets/pin-location.svg" alt="Pin Icon" />
14
+
</div>
15
+
<h1>PrivacyPin</h1>
16
+
<p>Connect with a server to start sharing</p>
17
+
</div>
18
+
19
+
<!-- x-data connects this element to the signupPageState Alpine component, enabling its data (serverAddress and signupKey) and functions (signup and scanQR) to work within it :) -->
20
+
<!-- TODO: make this a form instead? -->
21
+
<div class="actions" x-data="signupPageState">
22
+
<div>
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
+
/>
31
+
</div>
32
+
33
+
<div>
34
+
<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
+
/>
42
+
</div>
43
+
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" />
55
+
Scan QR Code
56
+
</button>
57
+
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>
66
+
</div>
67
+
</div>
68
+
</body>
69
+
</html>
+31
app/src/signup-page/signup.ts
+31
app/src/signup-page/signup.ts
···
···
1
+
import Alpine from "alpinejs";
2
+
import { createAccount } from "../utils/api.ts";
3
+
4
+
Alpine.data("signupPageState", () => ({
5
+
serverAddress: "",
6
+
signupKey: "",
7
+
isDoingStuff: false,
8
+
9
+
async signup() {
10
+
this.isDoingStuff = true;
11
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // temp
12
+
try {
13
+
await createAccount(this.serverAddress, this.signupKey);
14
+
window.location.href = "/src/home-page/home.html";
15
+
} catch (e) {
16
+
const err = e instanceof Error ? e.message : e;
17
+
alert(`Sign-up failed: ${err}`);
18
+
this.isDoingStuff = false;
19
+
}
20
+
},
21
+
22
+
async scanQR() {
23
+
this.isDoingStuff = true;
24
+
await new Promise((resolve) => setTimeout(resolve, 1000));
25
+
this.serverAddress = "http://127.0.0.1:3000";
26
+
this.signupKey = "dummy signup key";
27
+
this.isDoingStuff = false;
28
+
},
29
+
}));
30
+
31
+
Alpine.start();
-33
app/src/store.ts
-33
app/src/store.ts
···
1
-
import { Store as TauriStore } from "@tauri-apps/plugin-store";
2
-
3
-
type Settings = {
4
-
server_url: string;
5
-
user_id: string;
6
-
private_key: JsonWebKey;
7
-
friends: ClientFriend[];
8
-
is_admin: boolean;
9
-
};
10
-
11
-
export const Store = {
12
-
async get<T extends keyof Settings>(key: T): Promise<Settings[T]> {
13
-
const store = await TauriStore.load("settings.json");
14
-
const value = await store.get<Settings[T]>(key);
15
-
if (value === undefined) {
16
-
alert(`Key ${key} not found in store`);
17
-
throw new Error(`Key ${key} not found in store`);
18
-
}
19
-
return value;
20
-
},
21
-
22
-
async set<T extends keyof Settings>(key: T, value: Settings[T]): Promise<void> {
23
-
const store = await TauriStore.load("settings.json");
24
-
await store.set(key, value);
25
-
await store.save();
26
-
},
27
-
28
-
async isLoggedIn(): Promise<boolean> {
29
-
const store = await TauriStore.load("settings.json");
30
-
const user_id = await store.get<string>("user_id");
31
-
return user_id !== undefined;
32
-
},
33
-
};
···
-124
app/src/styles.css
-124
app/src/styles.css
···
1
-
body {
2
-
font-family: system-ui, sans-serif;
3
-
background: #f9fafb;
4
-
display: flex;
5
-
align-items: center;
6
-
justify-content: center;
7
-
height: 100vh;
8
-
margin: 0;
9
-
}
10
-
11
-
.card {
12
-
width: 100%;
13
-
max-width: 360px;
14
-
background: white;
15
-
border: 1px solid #d1d5db;
16
-
border-radius: 8px;
17
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
18
-
padding: 1.5rem;
19
-
box-sizing: border-box;
20
-
}
21
-
22
-
.header {
23
-
text-align: center;
24
-
margin-bottom: 1.5rem;
25
-
}
26
-
27
-
.icon-circle {
28
-
width: 64px;
29
-
height: 64px;
30
-
background: #dbeafe;
31
-
border-radius: 50%;
32
-
display: flex;
33
-
align-items: center;
34
-
justify-content: center;
35
-
margin: 0 auto 1rem;
36
-
}
37
-
38
-
.icon-circle img {
39
-
width: 32px;
40
-
height: 32px;
41
-
}
42
-
43
-
h1 {
44
-
font-size: 1.5rem;
45
-
margin: 0;
46
-
font-weight: 700;
47
-
}
48
-
49
-
p {
50
-
font-size: 0.9rem;
51
-
color: #6b7280;
52
-
}
53
-
54
-
.actions {
55
-
display: flex;
56
-
flex-direction: column;
57
-
gap: 1rem;
58
-
}
59
-
60
-
label {
61
-
display: block;
62
-
font-size: 0.85rem;
63
-
font-weight: 600;
64
-
margin-bottom: 0.25rem;
65
-
}
66
-
67
-
input {
68
-
width: 100%;
69
-
padding: 0.5rem 0.75rem;
70
-
border: 1px solid #d1d5db;
71
-
border-radius: 4px;
72
-
font-size: 0.95rem;
73
-
box-sizing: border-box;
74
-
}
75
-
76
-
input:focus {
77
-
outline: none;
78
-
border-color: #2563eb;
79
-
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
80
-
}
81
-
82
-
button {
83
-
width: 100%;
84
-
padding: 0.6rem;
85
-
font-size: 0.95rem;
86
-
border-radius: 4px;
87
-
cursor: pointer;
88
-
transition: background 0.2s ease;
89
-
display: flex;
90
-
align-items: center;
91
-
justify-content: center;
92
-
gap: 0.5rem;
93
-
}
94
-
95
-
.btn-primary {
96
-
background: #2563eb;
97
-
color: white;
98
-
border: none;
99
-
}
100
-
101
-
.btn-primary:hover {
102
-
background: #1d4ed8;
103
-
}
104
-
105
-
.btn-qr {
106
-
background: white;
107
-
border: 1px solid #d1d5db;
108
-
color: #374151;
109
-
}
110
-
111
-
.btn-qr:hover {
112
-
background: #f3f4f6;
113
-
}
114
-
115
-
.btn-qr img {
116
-
width: 16px;
117
-
height: 16px;
118
-
}
119
-
120
-
.hint {
121
-
font-size: 0.75rem;
122
-
color: #6b7280;
123
-
margin-top: 0.25rem;
124
-
}
···
+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 {};
+89
app/src/utils/api.ts
+89
app/src/utils/api.ts
···
···
1
+
import { Store } from "./store.ts";
2
+
3
+
function bufToBase64(buf: ArrayBuffer): string {
4
+
return new Uint8Array(buf).toBase64();
5
+
}
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);
15
+
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
+
});
21
+
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
+
}
34
+
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
+
}
39
+
40
+
export async function requestFriendRequest(friend_id: string): Promise<void> {
41
+
await post("request-friend-request", friend_id);
42
+
}
43
+
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
+
}
48
+
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
+
}
53
+
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
+
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");
66
+
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);
75
+
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
+
};
80
+
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
+
});
86
+
87
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
88
+
return await res.text();
89
+
}
+40
app/src/utils/store.ts
+40
app/src/utils/store.ts
···
···
1
+
import { Store as TauriStore } from "@tauri-apps/plugin-store";
2
+
3
+
type Settings = {
4
+
server_url: string;
5
+
user_id: string;
6
+
friends: { name: string; id: string }[];
7
+
is_admin: boolean;
8
+
priv_key: string;
9
+
};
10
+
11
+
export const Store = {
12
+
async get<T extends keyof Settings>(key: T): Promise<Settings[T]> {
13
+
const store = await TauriStore.load("settings.json");
14
+
const value = await store.get<Settings[T]>(key);
15
+
if (value === undefined) {
16
+
alert(`Key ${key} not found in store`);
17
+
throw new Error(`Key ${key} not found in store`);
18
+
}
19
+
return value;
20
+
},
21
+
22
+
async set<T extends keyof Settings>(key: T, value: Settings[T]): Promise<void> {
23
+
const store = await TauriStore.load("settings.json");
24
+
await store.set(key, value);
25
+
await store.save();
26
+
},
27
+
28
+
async isLoggedIn(): Promise<boolean> {
29
+
const store = await TauriStore.load("settings.json");
30
+
const user_id = await store.get<string>("user_id");
31
+
return user_id !== undefined;
32
+
},
33
+
34
+
// FOR TESTING ONLY
35
+
async reset(): Promise<void> {
36
+
const store = await TauriStore.load("settings.json");
37
+
await store.reset();
38
+
await store.save();
39
+
},
40
+
};
+4
app/src/utils/tools.ts
+4
app/src/utils/tools.ts
+5
-8
app/src-tauri/capabilities/default.json
+5
-8
app/src-tauri/capabilities/default.json
+27
-1
app/src-tauri/src/lib.rs
+27
-1
app/src-tauri/src/lib.rs
···
1
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
2
#[tauri::command]
3
fn greet(name: &str) -> String {
···
7
#[cfg_attr(mobile, tauri::mobile_entry_point)]
8
pub fn run() {
9
tauri::Builder::default()
10
-
.plugin(tauri_plugin_opener::init())
11
.invoke_handler(tauri::generate_handler![greet])
12
.run(tauri::generate_context!())
13
.expect("error while running tauri application");
···
1
+
use std::path::PathBuf;
2
+
use tauri::{WebviewUrl, WebviewWindowBuilder};
3
+
use tauri_plugin_store::StoreBuilder;
4
+
5
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
6
#[tauri::command]
7
fn greet(name: &str) -> String {
···
11
#[cfg_attr(mobile, tauri::mobile_entry_point)]
12
pub fn run() {
13
tauri::Builder::default()
14
+
.setup(|app| {
15
+
let app_handle = app.handle();
16
+
let store_path = PathBuf::from("settings.json");
17
+
let store = StoreBuilder::new(app_handle, store_path).build()?;
18
+
19
+
let user_id = store.get("user_id");
20
+
21
+
let page = if user_id.is_some() {
22
+
"/src/home-page/home.html"
23
+
} else {
24
+
"/src/signup-page/signup.html"
25
+
};
26
+
27
+
// create and open window directly at the correct page
28
+
WebviewWindowBuilder::new(app_handle, "main", WebviewUrl::App(page.into()))
29
+
.title("privacypin")
30
+
.inner_size(412.0, 715.0)
31
+
.resizable(false)
32
+
.build()?;
33
+
34
+
Ok(())
35
+
})
36
+
.plugin(tauri_plugin_store::Builder::default().build())
37
.invoke_handler(tauri::generate_handler![greet])
38
.run(tauri::generate_context!())
39
.expect("error while running tauri application");
+1
-8
app/src-tauri/tauri.conf.json
+1
-8
app/src-tauri/tauri.conf.json
-6
app/tauri.svg
-6
app/tauri.svg
···
1
-
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
2
-
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
3
-
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
4
-
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
5
-
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
6
-
</svg>
···
+7
-1
app/tsconfig.json
+7
-1
app/tsconfig.json
-25
app/typescript.svg
-25
app/typescript.svg
···
1
-
<?xml version="1.0" standalone="no"?>
2
-
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
3
-
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
4
-
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
5
-
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
6
-
preserveAspectRatio="xMidYMid meet">
7
-
8
-
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
9
-
fill="#2D79C7" stroke="none">
10
-
<path d="M430 5109 c-130 -19 -248 -88 -325 -191 -53 -71 -83 -147 -96 -247
11
-
-6 -49 -9 -813 -7 -2166 l3 -2090 22 -65 c54 -159 170 -273 328 -323 l70 -22
12
-
2140 0 2140 0 66 23 c160 55 272 169 322 327 l22 70 0 2135 0 2135 -22 70
13
-
c-49 157 -155 265 -319 327 l-59 23 -2115 1 c-1163 1 -2140 -2 -2170 -7z
14
-
m3931 -2383 c48 -9 120 -26 160 -39 l74 -23 3 -237 c1 -130 0 -237 -2 -237 -3
15
-
0 -26 14 -53 30 -61 38 -197 84 -310 106 -110 20 -293 15 -368 -12 -111 -39
16
-
-175 -110 -175 -193 0 -110 97 -197 335 -300 140 -61 309 -146 375 -189 30
17
-
-20 87 -68 126 -107 119 -117 164 -234 164 -426 0 -310 -145 -518 -430 -613
18
-
-131 -43 -248 -59 -445 -60 -243 -1 -405 24 -577 90 l-68 26 0 242 c0 175 -3
19
-
245 -12 254 -9 9 -9 12 0 12 7 0 12 -4 12 -9 0 -17 139 -102 223 -138 136 -57
20
-
233 -77 382 -76 145 0 224 19 295 68 75 52 100 156 59 242 -41 84 -135 148
21
-
-374 253 -367 161 -522 300 -581 520 -23 86 -23 253 -1 337 73 275 312 448
22
-
682 492 109 13 401 6 506 -13z m-1391 -241 l0 -205 -320 0 -320 0 0 -915 0
23
-
-915 -255 0 -255 0 0 915 0 915 -320 0 -320 0 0 205 0 205 895 0 895 0 0 -205z"/>
24
-
</g>
25
-
</svg>
···
+203
-58
server/Cargo.lock
+203
-58
server/Cargo.lock
···
3
version = 4
4
5
[[package]]
6
name = "atomic-waker"
7
version = "1.1.2"
8
source = "registry+https://github.com/rust-lang/crates.io-index"
···
10
11
[[package]]
12
name = "axum"
13
-
version = "0.8.6"
14
source = "registry+https://github.com/rust-lang/crates.io-index"
15
-
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
16
dependencies = [
17
"axum-core",
18
"bytes",
···
43
44
[[package]]
45
name = "axum-core"
46
-
version = "0.5.5"
47
source = "registry+https://github.com/rust-lang/crates.io-index"
48
-
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
49
dependencies = [
50
"bytes",
51
"futures-core",
···
83
84
[[package]]
85
name = "bytes"
86
-
version = "1.10.1"
87
source = "registry+https://github.com/rust-lang/crates.io-index"
88
-
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
89
90
[[package]]
91
name = "cfg-if"
···
110
111
[[package]]
112
name = "crypto-common"
113
-
version = "0.2.0-rc.5"
114
source = "registry+https://github.com/rust-lang/crates.io-index"
115
-
checksum = "919bd05924682a5480aec713596b9e2aabed3a0a6022fab6847f85a99e5f190a"
116
dependencies = [
117
"hybrid-array",
118
]
119
120
[[package]]
121
name = "curve25519-dalek"
122
-
version = "5.0.0-pre.1"
123
source = "registry+https://github.com/rust-lang/crates.io-index"
124
-
checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7"
125
dependencies = [
126
"cfg-if",
127
"cpufeatures",
···
146
147
[[package]]
148
name = "der"
149
-
version = "0.8.0-rc.9"
150
source = "registry+https://github.com/rust-lang/crates.io-index"
151
-
checksum = "e9d8dd2f26c86b27a2a8ea2767ec7f9df7a89516e4794e54ac01ee618dda3aa4"
152
dependencies = [
153
"const-oid",
154
]
155
156
[[package]]
157
name = "digest"
158
-
version = "0.11.0-rc.4"
159
source = "registry+https://github.com/rust-lang/crates.io-index"
160
-
checksum = "ea390c940e465846d64775e55e3115d5dc934acb953de6f6e6360bc232fe2bf7"
161
dependencies = [
162
"block-buffer",
163
"crypto-common",
···
175
176
[[package]]
177
name = "ed25519-dalek"
178
-
version = "3.0.0-pre.1"
179
source = "registry+https://github.com/rust-lang/crates.io-index"
180
-
checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1"
181
dependencies = [
182
"curve25519-dalek",
183
"ed25519",
···
187
]
188
189
[[package]]
190
name = "fiat-crypto"
191
version = "0.3.0"
192
source = "registry+https://github.com/rust-lang/crates.io-index"
193
checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
194
-
195
-
[[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
name = "form_urlencoded"
···
253
254
[[package]]
255
name = "http"
256
-
version = "1.3.1"
257
source = "registry+https://github.com/rust-lang/crates.io-index"
258
-
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
259
dependencies = [
260
"bytes",
261
-
"fnv",
262
"itoa",
263
]
264
···
308
309
[[package]]
310
name = "hyper"
311
-
version = "1.7.0"
312
source = "registry+https://github.com/rust-lang/crates.io-index"
313
-
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
314
dependencies = [
315
"atomic-waker",
316
"bytes",
···
329
330
[[package]]
331
name = "hyper-util"
332
-
version = "0.1.17"
333
source = "registry+https://github.com/rust-lang/crates.io-index"
334
-
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
335
dependencies = [
336
"bytes",
337
"futures-core",
···
345
346
[[package]]
347
name = "itoa"
348
-
version = "1.0.15"
349
source = "registry+https://github.com/rust-lang/crates.io-index"
350
-
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
351
352
[[package]]
353
name = "libc"
354
-
version = "0.2.177"
355
source = "registry+https://github.com/rust-lang/crates.io-index"
356
-
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
357
358
[[package]]
359
name = "lock_api"
···
366
367
[[package]]
368
name = "log"
369
-
version = "0.4.28"
370
source = "registry+https://github.com/rust-lang/crates.io-index"
371
-
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
372
373
[[package]]
374
name = "matchit"
···
390
391
[[package]]
392
name = "mio"
393
-
version = "1.1.0"
394
source = "registry+https://github.com/rust-lang/crates.io-index"
395
-
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
396
dependencies = [
397
"libc",
398
"wasi",
···
409
]
410
411
[[package]]
412
name = "once_cell"
413
version = "1.21.3"
414
source = "registry+https://github.com/rust-lang/crates.io-index"
···
476
477
[[package]]
478
name = "proc-macro2"
479
-
version = "1.0.103"
480
source = "registry+https://github.com/rust-lang/crates.io-index"
481
-
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
482
dependencies = [
483
"unicode-ident",
484
]
···
530
dependencies = [
531
"bitflags",
532
]
533
534
[[package]]
535
name = "rust-server"
···
542
"serde",
543
"serde_json",
544
"tokio",
545
]
546
547
[[package]]
···
555
556
[[package]]
557
name = "ryu"
558
-
version = "1.0.20"
559
source = "registry+https://github.com/rust-lang/crates.io-index"
560
-
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
561
562
[[package]]
563
name = "scopeguard"
···
603
604
[[package]]
605
name = "serde_json"
606
-
version = "1.0.145"
607
source = "registry+https://github.com/rust-lang/crates.io-index"
608
-
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
609
dependencies = [
610
"itoa",
611
"memchr",
612
-
"ryu",
613
"serde",
614
"serde_core",
615
]
616
617
[[package]]
···
649
]
650
651
[[package]]
652
name = "signal-hook-registry"
653
-
version = "1.4.6"
654
source = "registry+https://github.com/rust-lang/crates.io-index"
655
-
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
656
dependencies = [
657
"libc",
658
]
659
660
[[package]]
661
name = "signature"
662
-
version = "3.0.0-rc.5"
663
source = "registry+https://github.com/rust-lang/crates.io-index"
664
-
checksum = "2a0251c9d6468f4ba853b6352b190fb7c1e405087779917c238445eb03993826"
665
666
[[package]]
667
name = "smallvec"
···
696
697
[[package]]
698
name = "syn"
699
-
version = "2.0.109"
700
source = "registry+https://github.com/rust-lang/crates.io-index"
701
-
checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f"
702
dependencies = [
703
"proc-macro2",
704
"quote",
···
710
version = "1.0.2"
711
source = "registry+https://github.com/rust-lang/crates.io-index"
712
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
713
714
[[package]]
715
name = "tokio"
···
756
]
757
758
[[package]]
759
name = "tower-layer"
760
version = "0.3.3"
761
source = "registry+https://github.com/rust-lang/crates.io-index"
···
769
770
[[package]]
771
name = "tracing"
772
-
version = "0.1.41"
773
source = "registry+https://github.com/rust-lang/crates.io-index"
774
-
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
775
dependencies = [
776
"log",
777
"pin-project-lite",
778
"tracing-core",
779
]
780
781
[[package]]
782
name = "tracing-core"
783
-
version = "0.1.34"
784
source = "registry+https://github.com/rust-lang/crates.io-index"
785
-
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
786
dependencies = [
787
"once_cell",
788
]
789
790
[[package]]
···
798
version = "1.0.22"
799
source = "registry+https://github.com/rust-lang/crates.io-index"
800
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
801
802
[[package]]
803
name = "wasi"
···
896
897
[[package]]
898
name = "zerocopy"
899
-
version = "0.8.27"
900
source = "registry+https://github.com/rust-lang/crates.io-index"
901
-
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
902
dependencies = [
903
"zerocopy-derive",
904
]
905
906
[[package]]
907
name = "zerocopy-derive"
908
-
version = "0.8.27"
909
source = "registry+https://github.com/rust-lang/crates.io-index"
910
-
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
911
dependencies = [
912
"proc-macro2",
913
"quote",
···
919
version = "1.8.2"
920
source = "registry+https://github.com/rust-lang/crates.io-index"
921
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
···
3
version = 4
4
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
name = "atomic-waker"
16
version = "1.1.2"
17
source = "registry+https://github.com/rust-lang/crates.io-index"
···
19
20
[[package]]
21
name = "axum"
22
+
version = "0.8.8"
23
source = "registry+https://github.com/rust-lang/crates.io-index"
24
+
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
25
dependencies = [
26
"axum-core",
27
"bytes",
···
52
53
[[package]]
54
name = "axum-core"
55
+
version = "0.5.6"
56
source = "registry+https://github.com/rust-lang/crates.io-index"
57
+
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
58
dependencies = [
59
"bytes",
60
"futures-core",
···
92
93
[[package]]
94
name = "bytes"
95
+
version = "1.11.0"
96
source = "registry+https://github.com/rust-lang/crates.io-index"
97
+
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
98
99
[[package]]
100
name = "cfg-if"
···
119
120
[[package]]
121
name = "crypto-common"
122
+
version = "0.2.0-rc.8"
123
source = "registry+https://github.com/rust-lang/crates.io-index"
124
+
checksum = "e6165b8029cdc3e765b74d3548f85999ee799d5124877ce45c2c85ca78e4d4aa"
125
dependencies = [
126
"hybrid-array",
127
]
128
129
[[package]]
130
name = "curve25519-dalek"
131
+
version = "5.0.0-pre.3"
132
source = "registry+https://github.com/rust-lang/crates.io-index"
133
+
checksum = "92419e1cdc506051ffd30713ad09d0ec6a24bba9197e12989de389e35b19c77a"
134
dependencies = [
135
"cfg-if",
136
"cpufeatures",
···
155
156
[[package]]
157
name = "der"
158
+
version = "0.8.0-rc.10"
159
source = "registry+https://github.com/rust-lang/crates.io-index"
160
+
checksum = "02c1d73e9668ea6b6a28172aa55f3ebec38507131ce179051c8033b5c6037653"
161
dependencies = [
162
"const-oid",
163
]
164
165
[[package]]
166
name = "digest"
167
+
version = "0.11.0-rc.5"
168
source = "registry+https://github.com/rust-lang/crates.io-index"
169
+
checksum = "ebf9423bafb058e4142194330c52273c343f8a5beb7176d052f0e73b17dd35b9"
170
dependencies = [
171
"block-buffer",
172
"crypto-common",
···
184
185
[[package]]
186
name = "ed25519-dalek"
187
+
version = "3.0.0-pre.3"
188
source = "registry+https://github.com/rust-lang/crates.io-index"
189
+
checksum = "5d6d275a4ffdfc16e98fbcb5f5417214a06957c7cdc6eb2815c2dc50dce1c1dd"
190
dependencies = [
191
"curve25519-dalek",
192
"ed25519",
···
196
]
197
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]]
209
name = "fiat-crypto"
210
version = "0.3.0"
211
source = "registry+https://github.com/rust-lang/crates.io-index"
212
checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
213
214
[[package]]
215
name = "form_urlencoded"
···
266
267
[[package]]
268
name = "http"
269
+
version = "1.4.0"
270
source = "registry+https://github.com/rust-lang/crates.io-index"
271
+
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
272
dependencies = [
273
"bytes",
274
"itoa",
275
]
276
···
320
321
[[package]]
322
name = "hyper"
323
+
version = "1.8.1"
324
source = "registry+https://github.com/rust-lang/crates.io-index"
325
+
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
326
dependencies = [
327
"atomic-waker",
328
"bytes",
···
341
342
[[package]]
343
name = "hyper-util"
344
+
version = "0.1.19"
345
source = "registry+https://github.com/rust-lang/crates.io-index"
346
+
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
347
dependencies = [
348
"bytes",
349
"futures-core",
···
357
358
[[package]]
359
name = "itoa"
360
+
version = "1.0.17"
361
+
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"
369
370
[[package]]
371
name = "libc"
372
+
version = "0.2.178"
373
source = "registry+https://github.com/rust-lang/crates.io-index"
374
+
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
375
376
[[package]]
377
name = "lock_api"
···
384
385
[[package]]
386
name = "log"
387
+
version = "0.4.29"
388
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
+
]
399
400
[[package]]
401
name = "matchit"
···
417
418
[[package]]
419
name = "mio"
420
+
version = "1.1.1"
421
source = "registry+https://github.com/rust-lang/crates.io-index"
422
+
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
423
dependencies = [
424
"libc",
425
"wasi",
···
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
+
]
446
+
447
+
[[package]]
448
name = "once_cell"
449
version = "1.21.3"
450
source = "registry+https://github.com/rust-lang/crates.io-index"
···
512
513
[[package]]
514
name = "proc-macro2"
515
+
version = "1.0.104"
516
source = "registry+https://github.com/rust-lang/crates.io-index"
517
+
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
518
dependencies = [
519
"unicode-ident",
520
]
···
566
dependencies = [
567
"bitflags",
568
]
569
+
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]]
588
name = "rust-server"
···
595
"serde",
596
"serde_json",
597
"tokio",
598
+
"tower-http",
599
+
"tracing",
600
+
"tracing-subscriber",
601
]
602
603
[[package]]
···
611
612
[[package]]
613
name = "ryu"
614
+
version = "1.0.22"
615
source = "registry+https://github.com/rust-lang/crates.io-index"
616
+
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
617
618
[[package]]
619
name = "scopeguard"
···
659
660
[[package]]
661
name = "serde_json"
662
+
version = "1.0.148"
663
source = "registry+https://github.com/rust-lang/crates.io-index"
664
+
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
665
dependencies = [
666
"itoa",
667
"memchr",
668
"serde",
669
"serde_core",
670
+
"zmij",
671
]
672
673
[[package]]
···
705
]
706
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]]
717
name = "signal-hook-registry"
718
+
version = "1.4.8"
719
source = "registry+https://github.com/rust-lang/crates.io-index"
720
+
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
721
dependencies = [
722
+
"errno",
723
"libc",
724
]
725
726
[[package]]
727
name = "signature"
728
+
version = "3.0.0-rc.6"
729
source = "registry+https://github.com/rust-lang/crates.io-index"
730
+
checksum = "597a96996ccff7dfa16f052bd995b4cecc72af22c35138738dc029f0ead6608d"
731
732
[[package]]
733
name = "smallvec"
···
762
763
[[package]]
764
name = "syn"
765
+
version = "2.0.112"
766
source = "registry+https://github.com/rust-lang/crates.io-index"
767
+
checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4"
768
dependencies = [
769
"proc-macro2",
770
"quote",
···
776
version = "1.0.2"
777
source = "registry+https://github.com/rust-lang/crates.io-index"
778
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
779
+
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]]
790
name = "tokio"
···
831
]
832
833
[[package]]
834
+
name = "tower-http"
835
+
version = "0.6.8"
836
+
source = "registry+https://github.com/rust-lang/crates.io-index"
837
+
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
838
+
dependencies = [
839
+
"bitflags",
840
+
"bytes",
841
+
"http",
842
+
"http-body",
843
+
"pin-project-lite",
844
+
"tower-layer",
845
+
"tower-service",
846
+
"tracing",
847
+
]
848
+
849
+
[[package]]
850
name = "tower-layer"
851
version = "0.3.3"
852
source = "registry+https://github.com/rust-lang/crates.io-index"
···
860
861
[[package]]
862
name = "tracing"
863
+
version = "0.1.44"
864
source = "registry+https://github.com/rust-lang/crates.io-index"
865
+
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
866
dependencies = [
867
"log",
868
"pin-project-lite",
869
+
"tracing-attributes",
870
"tracing-core",
871
]
872
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]]
885
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"
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",
921
]
922
923
[[package]]
···
931
version = "1.0.22"
932
source = "registry+https://github.com/rust-lang/crates.io-index"
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"
940
941
[[package]]
942
name = "wasi"
···
1035
1036
[[package]]
1037
name = "zerocopy"
1038
+
version = "0.8.31"
1039
source = "registry+https://github.com/rust-lang/crates.io-index"
1040
+
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
1041
dependencies = [
1042
"zerocopy-derive",
1043
]
1044
1045
[[package]]
1046
name = "zerocopy-derive"
1047
+
version = "0.8.31"
1048
source = "registry+https://github.com/rust-lang/crates.io-index"
1049
+
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
1050
dependencies = [
1051
"proc-macro2",
1052
"quote",
···
1058
version = "1.8.2"
1059
source = "registry+https://github.com/rust-lang/crates.io-index"
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
server/Cargo.toml
+3
server/Cargo.toml
···
11
serde = { version = "1.0.228", features = ["derive"] }
12
serde_json = "1.0.145"
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"] }
+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
use ed25519_dalek::VerifyingKey;
4
use nanoid::nanoid;
5
6
-
use crate::types::*;
7
-
8
-
macro_rules! my_err {
9
-
($msg:expr) => {
10
-
Err(MyErr($msg))
11
-
};
12
-
}
13
14
pub async fn create_user(
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
16
Json(payload): Json<CreateUserRequest>,
17
-
) -> Result<Json<CreateAccountResponse>, MyErr> {
18
let key_used = { state.signup_keys.lock().await.remove(&payload.signup_key) };
19
20
if !key_used {
21
-
return my_err!("Signup key was not there");
22
}
23
24
// todo check
25
let pub_key_bytes = match BASE64_STANDARD.decode(&payload.pub_key_b64) {
26
Ok(b) => b,
27
-
Err(_) => return my_err!("Invalid base64 public key"),
28
};
29
30
// 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
-
};
39
40
let user_id = nanoid!(5);
41
let mut is_admin = false;
···
54
return Ok(Json(CreateAccountResponse { user_id, is_admin }));
55
}
56
57
-
pub async fn generate_signup_key(State(state): State<AppState>) -> Result<String, MyErr> {
58
let new_signup_key = nanoid!(5);
59
let mut signup_keys = state.signup_keys.lock().await;
60
···
64
return Ok(new_signup_key);
65
}
66
67
-
pub async fn create_friend_request(
68
State(state): State<AppState>,
69
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");
74
}
75
76
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
-
}
84
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);
91
92
-
let friend_request_accepted = { state.friend_requests.lock().await.remove(&link) };
93
94
-
if !friend_request_accepted {
95
-
return my_err!("Friend request not found");
96
}
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
-
105
return Ok(());
106
}
107
···
109
State(state): State<AppState>,
110
Extension(user_id): Extension<String>,
111
friend_id: String,
112
-
) -> Result<PlainBool, MyErr> {
113
let link = Link::new(friend_id, user_id);
114
let links = state.links.lock().await;
115
let accepted = links.contains(&link);
···
120
State(state): State<AppState>,
121
Extension(user_id): Extension<String>,
122
Json(pings): Json<Vec<PingPayload>>,
123
-
) -> Result<(), MyErr> {
124
let links = state.links.lock().await;
125
for ping in &pings {
126
let link = Link::new(user_id.clone(), ping.receiver_id.clone());
127
if !links.contains(&link) {
128
-
return my_err!("Ping receiver is not linked to sender");
129
}
130
}
131
drop(links);
···
147
State(state): State<AppState>,
148
Extension(user_id): Extension<String>,
149
sender_id: String,
150
-
) -> Result<EncryptedPingVec, MyErr> {
151
let link = Link::new(user_id, sender_id);
152
let links = state.links.lock().await;
153
154
if !links.contains(&link) {
155
-
return my_err!("No link exists between these users");
156
}
157
drop(links);
158
···
3
use ed25519_dalek::VerifyingKey;
4
use nanoid::nanoid;
5
6
+
use crate::{ReqBail, SrvErr, types::*};
7
8
pub async fn create_user(
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
10
Json(payload): Json<CreateUserRequest>,
11
+
) -> Result<Json<CreateAccountResponse>, SrvErr> {
12
let key_used = { state.signup_keys.lock().await.remove(&payload.signup_key) };
13
14
if !key_used {
15
+
ReqBail!("Signup key was not there");
16
}
17
18
// todo check
19
let pub_key_bytes = match BASE64_STANDARD.decode(&payload.pub_key_b64) {
20
Ok(b) => b,
21
+
Err(_) => ReqBail!("Invalid base64 public key"),
22
};
23
24
// 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
32
let user_id = nanoid!(5);
33
let mut is_admin = false;
···
46
return Ok(Json(CreateAccountResponse { user_id, is_admin }));
47
}
48
49
+
pub async fn generate_signup_key(State(state): State<AppState>) -> Result<String, SrvErr> {
50
let new_signup_key = nanoid!(5);
51
let mut signup_keys = state.signup_keys.lock().await;
52
···
56
return Ok(new_signup_key);
57
}
58
59
+
pub async fn request_friend_request(
60
State(state): State<AppState>,
61
Extension(user_id): Extension<String>,
62
+
friend_id: String,
63
+
) -> Result<(), SrvErr> {
64
+
if friend_id == user_id {
65
+
ReqBail!("Cannot friend yourself");
66
}
67
68
let mut friend_requests = state.friend_requests.lock().await;
69
+
let link = Link::new(friend_id, user_id);
70
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);
76
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);
80
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);
87
}
88
89
return Ok(());
90
}
91
···
93
State(state): State<AppState>,
94
Extension(user_id): Extension<String>,
95
friend_id: String,
96
+
) -> Result<PlainBool, SrvErr> {
97
let link = Link::new(friend_id, user_id);
98
let links = state.links.lock().await;
99
let accepted = links.contains(&link);
···
104
State(state): State<AppState>,
105
Extension(user_id): Extension<String>,
106
Json(pings): Json<Vec<PingPayload>>,
107
+
) -> Result<(), SrvErr> {
108
let links = state.links.lock().await;
109
for ping in &pings {
110
let link = Link::new(user_id.clone(), ping.receiver_id.clone());
111
if !links.contains(&link) {
112
+
ReqBail!("Ping receiver is not linked to sender");
113
}
114
}
115
drop(links);
···
131
State(state): State<AppState>,
132
Extension(user_id): Extension<String>,
133
sender_id: String,
134
+
) -> Result<EncryptedPingVec, SrvErr> {
135
let link = Link::new(user_id, sender_id);
136
let links = state.links.lock().await;
137
138
if !links.contains(&link) {
139
+
ReqBail!("No link exists between these users");
140
}
141
drop(links);
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
+
}
+22
-103
server/src/main.rs
+22
-103
server/src/main.rs
···
1
use std::{collections::HashMap, sync::Arc};
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;
13
use nanoid::nanoid;
14
-
use serde::Deserialize;
15
-
use tokio::sync::Mutex;
16
-
17
use std::collections::HashSet;
18
19
mod handlers;
20
mod types;
21
22
use handlers::*;
23
use types::*;
24
25
#[tokio::main]
26
async fn main() {
27
-
// initialize tracing
28
-
// tracing_subscriber::fmt::init();
29
30
// TODO: should this be inside an Arc?
31
let state = AppState {
···
41
// Until we have disk saves, always generate a admin signup key since there will be no admin set at launch
42
let admin_signup_key = nanoid!(5);
43
println!("Admin signup key: {admin_signup_key}");
44
state.signup_keys.lock().await.insert(admin_signup_key);
45
46
// build our application with a route
···
48
.route("/", post(|| async { "You just sent a POST to /" })) // for testing
49
.route("/create-account", post(create_user))
50
.route("/generate-signup-key", post(generate_signup_key))
51
-
.route("/create-friend-request", post(create_friend_request))
52
-
.route("/accept-friend-request", post(accept_friend_request))
53
.route(
54
"/is-friend-request-accepted",
55
post(is_friend_request_accepted),
···
57
.route("/send-pings", post(send_pings))
58
.route("/get-pings", post(get_pings))
59
.with_state(state.clone())
60
-
.layer(axum::middleware::from_fn_with_state(state, auth_test));
61
62
-
// run our app with hyper, listening globally on port 3000
63
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
64
axum::serve(listener, app).await.unwrap();
65
}
66
67
-
async fn auth_test(State(state): State<AppState>, req: Request, next: Next) -> impl IntoResponse {
68
-
let endpoint = req.uri().path().to_owned();
69
-
if endpoint != "/create-account" {
70
-
// CURSED STUFF BEGIN
71
-
let (parts, body) = req.into_parts();
72
-
let body_bytes = to_bytes(body, usize::MAX).await.unwrap();
73
-
let new_body = Body::from(body_bytes.clone());
74
-
let mut req = Request::from_parts(parts, new_body);
75
-
// CURSED STUFF END
76
-
77
-
// let auth_header = match req.headers().get("x-auth").and_then(|v| v.to_str().ok()) {
78
-
// Some(h) => h,
79
-
// None => todo!("header issues"),
80
-
// };
81
-
// println!("Headers before from_str: {auth_header}");
82
-
83
-
// let auth_data: Auth = match serde_json::from_str(&auth_header) {
84
-
// Ok(v) => v,
85
-
// Err(_) => todo!("parsing json issues"),
86
-
// };
87
-
let auth_header = req
88
-
.headers()
89
-
.get("x-auth")
90
-
.and_then(|v| v.to_str().ok())
91
-
.unwrap_or_else(|| panic!("missing x-auth header"));
92
-
93
-
let decoded_auth = BASE64_STANDARD
94
-
.decode(auth_header)
95
-
.unwrap_or_else(|_| panic!("invalid base64 in x-auth header"));
96
-
97
-
let auth_str = String::from_utf8(decoded_auth)
98
-
.unwrap_or_else(|_| panic!("invalid utf8 in x-auth header"));
99
-
100
-
let auth_data: Auth = serde_json::from_str(&auth_str)
101
-
.unwrap_or_else(|e| panic!("failed to parse x-auth JSON: {e}"));
102
-
103
-
let users = state.users.lock().await;
104
-
let user_id = auth_data.user_id;
105
-
let user = users
106
-
.iter()
107
-
.find(|u| u.id == user_id)
108
-
.unwrap_or_else(|| panic!("User not found"));
109
-
let verifying_key = user.pub_key.clone();
110
-
drop(users);
111
-
112
-
////////////////////////////////////
113
-
//////////////////////////////////// unsure
114
-
////////////////////////////////////
115
-
116
-
let sig_vec = BASE64_STANDARD.decode(&auth_data.signature).unwrap();
117
-
let sig_bytes: [u8; 64] = sig_vec.try_into().expect("invalid signature length");
118
-
let signature = Signature::from_bytes(&sig_bytes);
119
-
120
-
if let Err(err) = verifying_key.verify_strict(&body_bytes, &signature) {
121
-
panic!("Signature verification failed: {err}");
122
-
}
123
-
// println!("Signature verified!");
124
-
125
-
////////////////////////////////////
126
-
////////////////////////////////////
127
-
////////////////////////////////////
128
-
129
-
// TODO: Maybe make the endpoints an enum
130
-
if endpoint == "/generate-signup-key" {
131
-
let admin_id = state.admin_id.lock().await;
132
-
if admin_id.as_ref() != Some(&user_id) {
133
-
todo!("not allowed")
134
-
}
135
-
}
136
-
137
-
req.extensions_mut().insert(user_id);
138
-
139
-
return next.run(req).await;
140
-
}
141
-
142
-
return next.run(req).await;
143
-
}
144
-
145
-
// For now, only user_id for identification
146
-
#[derive(Debug, Deserialize, Clone)]
147
-
struct Auth {
148
-
user_id: String,
149
-
signature: String, // base 64
150
-
}
···
1
use std::{collections::HashMap, sync::Arc};
2
3
+
use axum::{Router, routing::post};
4
use nanoid::nanoid;
5
use std::collections::HashSet;
6
+
use tokio::sync::Mutex;
7
+
use tower_http::cors::CorsLayer;
8
+
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
9
10
+
mod auth;
11
mod handlers;
12
+
mod log;
13
mod types;
14
15
use handlers::*;
16
use types::*;
17
18
+
use crate::auth::auth_test;
19
+
use crate::log::log_req_res_bodies;
20
+
21
#[tokio::main]
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
31
// TODO: should this be inside an Arc?
32
let state = AppState {
···
42
// Until we have disk saves, always generate a admin signup key since there will be no admin set at launch
43
let admin_signup_key = nanoid!(5);
44
println!("Admin signup key: {admin_signup_key}");
45
+
println!("http://127.0.0.1:3000");
46
state.signup_keys.lock().await.insert(admin_signup_key);
47
48
// build our application with a route
···
50
.route("/", post(|| async { "You just sent a POST to /" })) // for testing
51
.route("/create-account", post(create_user))
52
.route("/generate-signup-key", post(generate_signup_key))
53
+
.route("/request-friend-request", post(request_friend_request))
54
.route(
55
"/is-friend-request-accepted",
56
post(is_friend_request_accepted),
···
58
.route("/send-pings", post(send_pings))
59
.route("/get-pings", post(get_pings))
60
.with_state(state.clone())
61
+
.layer(axum::middleware::from_fn_with_state(state, auth_test))
62
+
.layer(axum::middleware::from_fn(log_req_res_bodies))
63
+
.layer(CorsLayer::permissive());
64
65
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
66
axum::serve(listener, app).await.unwrap();
67
}
68
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
pub ring_buffer_cap: usize,
23
}
24
25
pub struct RingBuffer {
26
pub ring: Box<[Option<EncryptedPing>]>,
27
pub idx: usize,
···
80
pub id: String,
81
pub pub_key: VerifyingKey,
82
}
83
-
// pub struct User(pub String);
84
85
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
86
pub struct Link(String, String);
···
116
117
impl IntoResponse for EncryptedPingVec {
118
fn into_response(self) -> Response {
119
-
axum::Json(self.0).into_response() // TODO: check if this is correct
120
}
121
}
122
123
-
pub struct MyErr(pub &'static str);
124
125
-
impl IntoResponse for MyErr {
126
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();
129
}
130
}
···
22
pub ring_buffer_cap: usize,
23
}
24
25
+
#[derive(Debug, Deserialize, Clone)]
26
+
pub struct AuthData {
27
+
pub user_id: String,
28
+
pub signature: String,
29
+
}
30
+
31
pub struct RingBuffer {
32
pub ring: Box<[Option<EncryptedPing>]>,
33
pub idx: usize,
···
86
pub id: String,
87
pub pub_key: VerifyingKey,
88
}
89
90
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
91
pub struct Link(String, String);
···
121
122
impl IntoResponse for EncryptedPingVec {
123
fn into_response(self) -> Response {
124
+
axum::Json(self.0).into_response()
125
}
126
}
127
128
+
#[derive(Debug)]
129
+
pub struct SrvErr {
130
+
pub msg: String,
131
+
pub cause: Option<String>,
132
+
}
133
134
+
impl IntoResponse for SrvErr {
135
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()
152
}
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
+
}
-54
server/test/autils.ts
-54
server/test/autils.ts
···
1
-
import { expect } from "bun:test";
2
-
3
-
export const URL = "http://localhost:3000";
4
-
5
-
export async function generateUser(
6
-
signup_key: string | undefined,
7
-
should_be_admin: boolean = false,
8
-
): Promise<string> {
9
-
if (signup_key === undefined) {
10
-
throw new Error("signup_key was not provided or captured from server output");
11
-
}
12
-
13
-
const res = await fetch(`${URL}/create-account`, {
14
-
method: "POST",
15
-
body: signup_key,
16
-
});
17
-
const json = await res.json();
18
-
expect(res.status).toBe(200);
19
-
expect(json).toEqual({
20
-
user_id: expect.any(String),
21
-
is_admin: should_be_admin,
22
-
});
23
-
24
-
return json.user_id;
25
-
}
26
-
27
-
export async function post(
28
-
endpoint: string,
29
-
user_id: string,
30
-
data: Object | string | undefined,
31
-
): Promise<any> {
32
-
const headers: Record<string, string> = {
33
-
"x-auth": JSON.stringify({ user_id }),
34
-
};
35
-
36
-
let stringified_data: string | undefined;
37
-
38
-
if (typeof data === "object") {
39
-
stringified_data = JSON.stringify(data);
40
-
headers["Content-Type"] = "application/json";
41
-
} else {
42
-
stringified_data = data;
43
-
}
44
-
45
-
const res = await fetch(`${URL}/${endpoint}`, {
46
-
method: "POST",
47
-
headers,
48
-
body: stringified_data,
49
-
});
50
-
51
-
expect(res.status).toBe(200);
52
-
53
-
return await res.text();
54
-
}
···
+1
-1
server/test/utils.ts
+1
-1
server/test/utils.ts
···
1
import { expect } from "bun:test";
2
3
-
export const URL = "http://localhost:3000";
4
5
// Generate an Ed25519 keypair and register it with the server.
6
export async function generateUser(signup_key: string | undefined, should_be_admin: boolean = false): Promise<{ user_id: string; pubKey: Uint8Array; privKey: Uint8Array }> {
···
1
import { expect } from "bun:test";
2
3
+
export const URL = "http://127.0.0.1:3000";
4
5
// Generate an Ed25519 keypair and register it with the server.
6
export async function generateUser(signup_key: string | undefined, should_be_admin: boolean = false): Promise<{ user_id: string; pubKey: Uint8Array; privKey: Uint8Array }> {