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)
|