summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-06-01 11:13:55 +0200
committerRory& <root@rory.gay>2025-06-01 11:13:55 +0200
commit4e12e02bc805170e6b03d33e0ef894b2a3021fb3 (patch)
treea525a35cfcc28f80bbe33d152fe483d14d8b23d1
parentUpdate test client (diff)
downloadnodejs-final-assignment-4e12e02bc805170e6b03d33e0ef894b2a3021fb3.tar.xz
Add alarm endpoints, basic budget routes, spend history
-rw-r--r--plan.md7
-rw-r--r--src/api/middlewares/authMiddleware.js8
-rw-r--r--src/api/routes/alarmRoutes.js41
-rw-r--r--src/api/routes/budgetRoutes.js20
-rw-r--r--src/api/routes/index.js2
-rw-r--r--src/db/dbAccess/user.js13
-rw-r--r--src/db/schemas/spendHistory.js29
-rw-r--r--src/db/schemas/user.js29
-rw-r--r--testFrontend/SafeNSound.Frontend/App.razor8
-rw-r--r--testFrontend/SafeNSound.Frontend/Layout/NavMenu.razor4
-rw-r--r--testFrontend/SafeNSound.Frontend/Pages/Alarm.razor36
-rw-r--r--testFrontend/SafeNSound.Frontend/Pages/Auth.razor4
-rw-r--r--testFrontend/SafeNSound.Frontend/Pages/Weather.razor60
-rw-r--r--testFrontend/SafeNSound.Frontend/_Imports.razor3
-rw-r--r--testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs43
-rw-r--r--testFrontend/SafeNSound.Sdk/WrappedHttpClient.cs4
16 files changed, 239 insertions, 72 deletions
diff --git a/plan.md b/plan.md

index d9e3637..59edd66 100644 --- a/plan.md +++ b/plan.md
@@ -17,14 +17,17 @@ - [x] Registration - [ ] Validation based on type - [x] Login + - [x] Delete - [ ] Password reset - [ ] User profile management - [ ] Device management - [ ] Organisation (who's coming? announcement of events, ...) - [ ] Budgeting with tracking - - [ ] Review spending + - [ ] Get current budget + - [ ] Review spending history - [ ] Request additional budget in case of emergency - [ ] Emergency alarm + - [ ] Silencing/clearing - [ ] Optional: integration with park's emergency services - [ ] Emergency contact & info card - [ ] Limitations on consumptions @@ -40,4 +43,4 @@ - [ ] Integration API for park infrastructure (e.g., user-only WiFi network, payment via park wristband according to budget, ...) - [ ] User-only WiFi network - [ ] Payment via park wristband according to budget - - [ ] Integration with park's existing systems for seamless user experience \ No newline at end of file + - [ ] Integration with park's existing systems for seamless user experience diff --git a/src/api/middlewares/authMiddleware.js b/src/api/middlewares/authMiddleware.js
index 1187112..8553517 100644 --- a/src/api/middlewares/authMiddleware.js +++ b/src/api/middlewares/authMiddleware.js
@@ -1,5 +1,5 @@ import { validateJwtToken } from '#util/jwtUtils.js'; -import { DbUser } from '#db/schemas/index.js'; +import { DbUser, UserType } from '#db/schemas/index.js'; /** * @param options {AuthValidationOptions} @@ -15,6 +15,12 @@ export function validateAuth(options) { const user = (req.user = await DbUser.findById(auth.id).exec()); + // admin can do everything + if (user.type == UserType.ADMIN) { + next(); + return; + } + if (options.roles && !options.roles.includes(user.type)) { res.status(401).send('Unauthorized'); return; diff --git a/src/api/routes/alarmRoutes.js b/src/api/routes/alarmRoutes.js new file mode 100644
index 0000000..5170327 --- /dev/null +++ b/src/api/routes/alarmRoutes.js
@@ -0,0 +1,41 @@ +import { validateAuth } from '#api/middlewares/index.js'; +import { UserType } from '#db/schemas/index.js'; + +export const alarmByUserRoute = { + route: '/alarm/:id', + onGetValidation: validateAuth({ roles: [UserType.MONITOR] }), + async onGet(req, res) { + const user = await getUserById(req.query.id); + res.send(user.alarm); + }, + + onDeleteValidation: validateAuth({ roles: [UserType.MONITOR] }), + async onDelete(req, res) { + const user = await getUserById(req.query.id); + user.alarm = null; + await user.save(); + res.status(204).send(); + } +}; + +export const alarmRoute = { + onGetValidation: validateAuth({ roles: [UserType.USER] }), + async onGet(req, res) { + res.send(req.user.alarm); + }, + + route: '/alarm/@me', + onPutValidation: validateAuth({ roles: [UserType.USER] }), + async onPut(req, res) { + req.user.alarm = req.body; + await req.user.save(); + res.status(204).send(); + }, + + onDeleteValidation: validateAuth({ roles: [UserType.USER] }), + async onDelete(req, res) { + req.user.alarm = null; + await req.user.save(); + res.status(204).send(); + } +}; diff --git a/src/api/routes/budgetRoutes.js b/src/api/routes/budgetRoutes.js new file mode 100644
index 0000000..ed827e8 --- /dev/null +++ b/src/api/routes/budgetRoutes.js
@@ -0,0 +1,20 @@ +import { validateAuth } from '#api/middlewares/index.js'; +import { UserType } from '#db/schemas/index.js'; + +export const getBudgetByUserRoute = { + route: '/budget/:id', + onGetValidation: validateAuth({ roles: [UserType.MONITOR] }), + onGet(req, res) {} +}; + +export const addBudgetByUserRoute = { + route: '/budget/:id/add', + onGetValidation: validateAuth({ roles: [UserType.MONITOR] }), + onGet(req, res) {} +}; + +export const getBudgetRoute = { + route: '/budget/@me', + onGetValidation: validateAuth({ roles: [UserType.USER] }), + onGet(req, res) {} +}; diff --git a/src/api/routes/index.js b/src/api/routes/index.js
index 745dd27..4feeb11 100644 --- a/src/api/routes/index.js +++ b/src/api/routes/index.js
@@ -2,3 +2,5 @@ export * from './statusRoute.js'; export * from './indexRoute.js'; export * from './auth/index.js'; +export * from './budgetRoutes.js'; +export * from './alarmRoutes.js'; diff --git a/src/db/dbAccess/user.js b/src/db/dbAccess/user.js
index fad5ba3..4ab70fd 100644 --- a/src/db/dbAccess/user.js +++ b/src/db/dbAccess/user.js
@@ -7,6 +7,19 @@ import { generateJwtToken } from '#util/jwtUtils.js'; async function whoAmI(token) {} +async function getUserById(id) { + const user = await DbUser.findById(id); + if (!user) { + throw new SafeNSoundError({ + errCode: 'ENTITY_NOT_FOUND', + message: 'No such user!' + }); + } + + console.log(user); + return user; +} + async function getUserByAuth(data) { if (!(data instanceof AuthDto)) throw new Error('Invalid data type. Expected AuthDto.'); diff --git a/src/db/schemas/spendHistory.js b/src/db/schemas/spendHistory.js new file mode 100644
index 0000000..b12bcc3 --- /dev/null +++ b/src/db/schemas/spendHistory.js
@@ -0,0 +1,29 @@ +import { model, Schema } from 'mongoose'; +import { hash, compare } from 'bcrypt'; +import {ref} from "joi"; + +/** + * User schema for MongoDB. + * @type {module:mongoose.Schema} + */ +export const spendHistorySchema = new Schema({ + spentBy: { + type: ObjectId, + ref: "users" + } + createdAt: { + type: Date, + default: Date.now, + immutable: true + } +}); + +export const UserType = Object.freeze({ + USER: 'user', + MONITOR: 'monitor', + ADMIN: 'admin' +}); + +export const DbUser = model('user', userSchema); + +console.log('[MONGODB] User schema initialized successfully!'); diff --git a/src/db/schemas/user.js b/src/db/schemas/user.js
index f490966..063fddf 100644 --- a/src/db/schemas/user.js +++ b/src/db/schemas/user.js
@@ -1,6 +1,17 @@ import { model, Schema } from 'mongoose'; import { hash, compare } from 'bcrypt'; +export const UserType = Object.freeze({ + USER: 'user', + MONITOR: 'monitor', + ADMIN: 'admin' +}); + +export const AlarmType = Object.freeze({ + FALL: 'fall', + TOILET: 'toilet' +}); + export const deviceSchema = new Schema({ name: { type: String, @@ -19,6 +30,19 @@ export const deviceSchema = new Schema({ } }); +export const alarmSchema = new Schema({ + createdAt: { + type: Date, + default: Date.now, + immutable: true + }, + reason: { + type: String, + enum: Object.values(AlarmType), + required: true + } +}); + /** * User schema for MongoDB. * @type {module:mongoose.Schema} @@ -42,7 +66,7 @@ export const userSchema = new Schema({ }, type: { type: String, - enum: ['user', 'monitor', 'admin'], + enum: Object.values(UserType), default: 'user' }, createdAt: { @@ -53,6 +77,9 @@ export const userSchema = new Schema({ devices: { type: [deviceSchema], default: [] + }, + alarm: { + type: alarmSchema } }); diff --git a/testFrontend/SafeNSound.Frontend/App.razor b/testFrontend/SafeNSound.Frontend/App.razor
index c7730d1..13c8280 100644 --- a/testFrontend/SafeNSound.Frontend/App.razor +++ b/testFrontend/SafeNSound.Frontend/App.razor
@@ -9,4 +9,10 @@ <p role="alert">Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> -</Router> \ No newline at end of file +</Router> + +@code { + + public static SafeNSoundClient? Client { get; set; } + +} \ No newline at end of file diff --git a/testFrontend/SafeNSound.Frontend/Layout/NavMenu.razor b/testFrontend/SafeNSound.Frontend/Layout/NavMenu.razor
index 2a0fd8a..b688610 100644 --- a/testFrontend/SafeNSound.Frontend/Layout/NavMenu.razor +++ b/testFrontend/SafeNSound.Frontend/Layout/NavMenu.razor
@@ -15,8 +15,8 @@ </NavLink> </div> <div class="nav-item px-3"> - <NavLink class="nav-link" href="counter"> - <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter + <NavLink class="nav-link" href="/Alarm"> + <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Alarm </NavLink> </div> <div class="nav-item px-3"> diff --git a/testFrontend/SafeNSound.Frontend/Pages/Alarm.razor b/testFrontend/SafeNSound.Frontend/Pages/Alarm.razor new file mode 100644
index 0000000..9b90ef4 --- /dev/null +++ b/testFrontend/SafeNSound.Frontend/Pages/Alarm.razor
@@ -0,0 +1,36 @@ +@page "/Alarm" + +<h1>Alarm</h1> + +<br/><br/> + +@if (Exception != null) { + <div class="alert alert-danger"> + <strong>Error:</strong><br/> + <pre> + @Exception + </pre> + </div> +} + +@if (Result != null) { + <div class="alert alert-success"> + <strong>Result:</strong><br/> + <pre> + @Result.ToJson(indent: true) + </pre> + </div> +} + +@code { + private Exception? Exception { get; set; } + private object? Result { get; set; } + + protected override async Task OnInitializedAsync() { + if (App.Client is null) { + NavigationManager.NavigateTo("/Auth"); + return; + } + } + +} \ No newline at end of file diff --git a/testFrontend/SafeNSound.Frontend/Pages/Auth.razor b/testFrontend/SafeNSound.Frontend/Pages/Auth.razor
index 3db77a1..c58a996 100644 --- a/testFrontend/SafeNSound.Frontend/Pages/Auth.razor +++ b/testFrontend/SafeNSound.Frontend/Pages/Auth.razor
@@ -73,11 +73,13 @@ Result = null; Exception = null; try { - Result = await Authentication.Login(new() { + SafeNSoundAuthResult result; + Result = result = await Authentication.Login(new() { Username = Username, Password = Password, Email = Email }); + App.Client = new SafeNSoundClient(Config, result.AccessToken); } catch (Exception ex) { Exception = ex; diff --git a/testFrontend/SafeNSound.Frontend/Pages/Weather.razor b/testFrontend/SafeNSound.Frontend/Pages/Weather.razor deleted file mode 100644
index a0ca515..0000000 --- a/testFrontend/SafeNSound.Frontend/Pages/Weather.razor +++ /dev/null
@@ -1,60 +0,0 @@ -@page "/weather" -@inject HttpClient Http - -<PageTitle>Weather</PageTitle> - -<h1>Weather</h1> - -<p>This component demonstrates fetching data from the server.</p> - -@if (forecasts == null) -{ - <p> - <em>Loading...</em> - </p> -} -else -{ - <table class="table"> - <thead> - <tr> - <th>Date</th> - <th aria-label="Temperature in Celsius">Temp. (C)</th> - <th aria-label="Temperature in Farenheit">Temp. (F)</th> - <th>Summary</th> - </tr> - </thead> - <tbody> - @foreach (var forecast in forecasts) - { - <tr> - <td>@forecast.Date.ToShortDateString()</td> - <td>@forecast.TemperatureC</td> - <td>@forecast.TemperatureF</td> - <td>@forecast.Summary</td> - </tr> - } - </tbody> - </table> -} - -@code { - private WeatherForecast[]? forecasts; - - protected override async Task OnInitializedAsync() - { - forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json"); - } - - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public string? Summary { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - } - -} \ No newline at end of file diff --git a/testFrontend/SafeNSound.Frontend/_Imports.razor b/testFrontend/SafeNSound.Frontend/_Imports.razor
index 0103f88..18a240e 100644 --- a/testFrontend/SafeNSound.Frontend/_Imports.razor +++ b/testFrontend/SafeNSound.Frontend/_Imports.razor
@@ -14,4 +14,5 @@ @using ArcaneLibs.Extensions @inject SafeNSoundAuthentication Authentication -@inject SafeNSoundConfiguration Config \ No newline at end of file +@inject SafeNSoundConfiguration Config +@inject NavigationManager NavigationManager \ No newline at end of file diff --git a/testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs b/testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs
index dee3913..05d0af9 100644 --- a/testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs +++ b/testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs
@@ -1,6 +1,47 @@ +using System.Net.Http.Json; + namespace SafeNSound.Sdk; -public class SafeNSoundClient(SafeNSoundConfiguration config) +public class SafeNSoundClient(SafeNSoundConfiguration config, string accessToken) { + public WrappedHttpClient HttpClient { get; } = new() + { + BaseAddress = new Uri(config.BaseUri), + DefaultRequestHeaders = + { + Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken) + } + }; +#region Alarm + + public async Task<AlarmDto> GetAlarm(string userId = "@me") { + var res = await HttpClient.GetAsync($"/alarm/{userId}"); + res.EnsureSuccessStatusCode(); + return (await res.Content.ReadFromJsonAsync<AlarmDto>())!; + } + + public async Task SetAlarm(AlarmDto alarm) { + var res = await HttpClient.PutAsJsonAsync("/alarm/@me", alarm); + res.EnsureSuccessStatusCode(); + } + + public async Task DeleteAlarm(string userId = "@me") { + var res = await HttpClient.DeleteAsync($"/alarm/{userId}"); + res.EnsureSuccessStatusCode(); + } + + + +#endregion + +#region Budget + + + +#endregion +} + + +public class AlarmDto { } \ No newline at end of file diff --git a/testFrontend/SafeNSound.Sdk/WrappedHttpClient.cs b/testFrontend/SafeNSound.Sdk/WrappedHttpClient.cs
index 2398a0b..e4b4500 100644 --- a/testFrontend/SafeNSound.Sdk/WrappedHttpClient.cs +++ b/testFrontend/SafeNSound.Sdk/WrappedHttpClient.cs
@@ -324,10 +324,10 @@ public class WrappedHttpClient return await SendAsync(request, cancellationToken); } - public async Task DeleteAsync(string url) + public async Task<HttpResponseMessage> DeleteAsync(string url) { var request = new HttpRequestMessage(HttpMethod.Delete, url); - await SendAsync(request); + return await SendAsync(request); } public async Task<HttpResponseMessage> DeleteAsJsonAsync<T>(string url, T payload)