Skip to content

Commit 5807369

Browse files
committed
Implement authentication system and enhance data handling features
1 parent dc6c2c4 commit 5807369

File tree

8 files changed

+138
-17
lines changed

8 files changed

+138
-17
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
## Backend
44

55
- [x] extract data from site every 30s and store in db
6-
- [ ] ability to download data in db as csv
6+
- [x] ability to download data in db as csv
77
- [x] when tracking multiple stations only extract one at time, round robin style, to avoid hiting deno ratelimit
88

99
## Frontend
1010

1111
- [x] Select stations (input, seperated by comma)
12-
- [ ] download selected stations wind data
12+
- [x] download selected stations wind data
13+
- [ ] authentication system, password
1314
- [ ] view selected stations history online

backend/deno.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
{
22
"tasks": {
3-
"dev": "deno run --watch main.ts",
4-
"start": "deno run --unstable-cron --unstable-kv -A main.ts",
5-
"download_kv": "deno run --unstable-cron --unstable-kv -A download_kv.ts"
3+
"dev": "KV_STORE=kv.sqlite deno serve --unstable-cron --unstable-kv -A --watch main.ts"
64
},
75
"imports": {
86
"@oak/oak": "jsr:@oak/oak@^17.1.4",

backend/main.ts

+36-6
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@ import { stringify } from "jsr:@std/csv/stringify";
66

77
const kv = await Deno.openKv(Deno.env.get("KV_STORE") || undefined);
88

9-
Deno.cron("Download wind data", "*/10 * * * *", async () => {
9+
Deno.cron(
10+
"Download wind data",
11+
`*/${Deno.env.get("CRON_INTERVAL") || "1"} * * * *`,
12+
async () => {
13+
await downloadNextStationWindData();
14+
}
15+
);
16+
if (Deno.env.get("INITIAL_DOWNLOAD") === "true") {
1017
await downloadNextStationWindData();
11-
});
12-
await downloadNextStationWindData();
18+
}
1319

1420
async function downloadNextStationWindData() {
1521
const trackedStations =
@@ -74,12 +80,36 @@ async function downloadWindData(stationId: number) {
7480

7581
const router = new Router();
7682

83+
router.post("/login", async (ctx) => {
84+
const body = await ctx.request.body.text();
85+
const formData = new URLSearchParams(body);
86+
const password = formData.get("password");
87+
88+
if (password === (Deno.env.get("PASSWORD") || "")) {
89+
ctx.response.status = 200;
90+
} else {
91+
ctx.response.status = 401;
92+
ctx.response.body = "Incorrect password";
93+
}
94+
});
95+
7796
router.get("/tracked-stations", async (ctx) => {
7897
const trackedStations = (await kv.get<number[]>(["trackedStations"])).value;
7998
ctx.response.body = trackedStations ?? [];
8099
});
81100

82101
router.post("/tracked-stations", async (ctx) => {
102+
if (
103+
(ctx.request.headers.get("Authorization") ?? "") !==
104+
(Deno.env.get("PASSWORD") ?? "")
105+
) {
106+
ctx.response.status = 401;
107+
ctx.response.body = {
108+
msg: "Unauthorized",
109+
};
110+
return;
111+
}
112+
83113
try {
84114
const body = await ctx.request.body.text();
85115
const formData = new URLSearchParams(body);
@@ -96,11 +126,11 @@ router.post("/tracked-stations", async (ctx) => {
96126
ctx.response.body = { msg: "Saved Successfully", trackedStations };
97127
} else {
98128
ctx.response.status = 500;
99-
ctx.response.body = "Failed to save tracked stations";
129+
ctx.response.body = { msg: "Failed to save tracked stations" };
100130
}
101131
} catch (err) {
102132
ctx.response.status = 500;
103-
ctx.response.body = "Failed to save tracked stations";
133+
ctx.response.body = { msg: "Failed to save tracked stations" };
104134
}
105135
});
106136

@@ -155,7 +185,7 @@ router.get("/wind-history/:stationId/csv", async (ctx) => {
155185
});
156186

157187
const app = new Application();
158-
app.use(oakCors({ origin: "*" }));
188+
app.use(oakCors({ origin: Deno.env.get("ORIGIN") || "*" }));
159189
app.use(router.routes());
160190
app.use(router.allowedMethods());
161191

frontend/.env

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VITE_API_URL=http://localhost:3000
1+
VITE_API_URL=http://localhost:8000

frontend/src/App.svelte

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
import { Col, Container, Row } from "@sveltestrap/sveltestrap";
33
import SelectTrackedStatiosn from "./lib/SelectTrackedStations.svelte";
44
import Stations from "./lib/Stations.svelte";
5+
import Login from "./lib/Login.svelte";
56
</script>
67

78
<main>
89
<Container sm>
910
<Row
1011
><Col sm="12" md={{ offset: 4, size: 5 }}
11-
><h1 class="m-4">wind2speed-tracker</h1></Col
12+
><h1 class="mt-4 mb-4">wind2speed-tracker</h1></Col
1213
></Row
1314
>
1415
<Row>
@@ -22,4 +23,5 @@
2223
></Row
2324
>
2425
</Container>
26+
<Login></Login>
2527
</main>

frontend/src/lib/Login.svelte

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<script lang="ts">
2+
import {
3+
Button,
4+
Input,
5+
InputGroup,
6+
Modal,
7+
ModalBody,
8+
ModalFooter,
9+
ModalHeader,
10+
} from "@sveltestrap/sveltestrap";
11+
import { authState } from "./auth.svelte";
12+
13+
let open = $state(true);
14+
let loggingIn = $state(false);
15+
let password = $state("");
16+
let error = $state("");
17+
let inputClasses = $derived.by(() => (error ? "is-invalid" : ""));
18+
19+
function login(e: SubmitEvent) {
20+
e.preventDefault();
21+
loggingIn = true;
22+
23+
const form = e.currentTarget as HTMLFormElement;
24+
const formData = new URLSearchParams(new FormData(form) as any);
25+
26+
fetch(import.meta.env.VITE_API_URL + "/login", {
27+
method: "POST",
28+
headers: {
29+
"Content-Type": "application/x-www-form-urlencoded",
30+
},
31+
body: formData.toString(),
32+
})
33+
.then(async (res) => {
34+
if (res.status === 200) {
35+
authState.loggedIn = true;
36+
authState.password = password;
37+
open = false;
38+
} else {
39+
error = (await res.text()) || res.statusText;
40+
}
41+
})
42+
.catch((err) => {
43+
console.error(error);
44+
error = err.message;
45+
})
46+
.finally(() => {
47+
loggingIn = false;
48+
});
49+
// authState.loggedIn = true;
50+
// open = false;
51+
}
52+
</script>
53+
54+
<Modal isOpen={open} centered={true}>
55+
<form onsubmit={login}>
56+
<ModalHeader>Login</ModalHeader>
57+
<ModalBody>
58+
<InputGroup class={"has-validation " + inputClasses}>
59+
<Input
60+
name="password"
61+
type="password"
62+
placeholder="Password"
63+
bind:value={password}
64+
disabled={loggingIn}
65+
class={inputClasses}
66+
/>{#if error}
67+
<div class="invalid-feedback">{error}</div>
68+
{/if}</InputGroup
69+
>
70+
</ModalBody>
71+
<ModalFooter>
72+
<Button color="primary" type="submit" disabled={loggingIn}
73+
>Login</Button
74+
>
75+
<Button color="secondary" onclick={() => (open = false)}
76+
>Guest</Button
77+
>
78+
</ModalFooter>
79+
</form>
80+
</Modal>

frontend/src/lib/SelectTrackedStations.svelte

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script lang="ts">
22
import { Button, Input, InputGroup } from "@sveltestrap/sveltestrap";
3+
import { authState } from "./auth.svelte";
34
45
let loading = $state(true);
56
$effect(() => {
@@ -36,6 +37,7 @@
3637
method: "POST",
3738
headers: {
3839
"Content-Type": "application/x-www-form-urlencoded",
40+
Authorization: authState.password,
3941
},
4042
body: formData.toString(),
4143
})
@@ -48,12 +50,14 @@
4850
msg = res.statusText;
4951
} else {
5052
msg = data.msg;
51-
trackedStationsStr = data.trackedStations.join(",");
53+
if (res.ok)
54+
trackedStationsStr = data.trackedStations.join(",");
5255
}
5356
success = res.ok;
5457
submitted = true;
5558
})
5659
.catch((err) => {
60+
console.error(err);
5761
msg = err.message;
5862
success = false;
5963
submitted = true;
@@ -74,13 +78,15 @@
7478
class={inputValidation}
7579
oninput={() => (submitted = false)}
7680
bind:value={trackedStationsStr}
77-
disabled={loading}
81+
disabled={!authState.loggedIn || loading}
7882
/>
7983
<label for="trackedStations">Tracked Stations</label>
8084
</div>
8185

82-
<Button color="primary" type="submit" disabled={submitting || loading}
83-
>Save</Button
86+
<Button
87+
color="primary"
88+
type="submit"
89+
disabled={!authState.loggedIn || submitting || loading}>Save</Button
8490
>
8591
{#if success}
8692
<div class="valid-feedback">{msg}</div>

frontend/src/lib/auth.svelte.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export let authState: { loggedIn: boolean; password: string } = $state({
2+
loggedIn: false,
3+
password: "",
4+
});

0 commit comments

Comments
 (0)