summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-06-03 01:01:40 +0200
committerRory& <root@rory.gay>2025-06-03 01:01:40 +0200
commit6f3f08ed340e59a62a2d0428a5c32f99551ef1ce (patch)
treeff77390b1d3ea61414c14c94ac1fa2a05030879b
parentMore alarm testing (diff)
downloadnodejs-final-assignment-6f3f08ed340e59a62a2d0428a5c32f99551ef1ce.tar.xz
Fix performance issues, add fake user bot to test client, more testing
-rw-r--r--.idea/runConfigurations/dev_watch.xml14
-rw-r--r--README.md3
-rw-r--r--package.json2
-rw-r--r--src/api/middlewares/authMiddleware.js23
-rw-r--r--src/api/middlewares/errorMiddleware.js1
-rw-r--r--src/api/routes/adminRoutes.js55
-rw-r--r--src/api/routes/alarmRoutes.js38
-rw-r--r--src/api/routes/assignedUserRoutes.js56
-rw-r--r--src/api/routes/auth/accountRoutes.js2
-rw-r--r--src/api/routes/index.js2
-rw-r--r--src/api/routes/statusRoute.js2
-rw-r--r--src/db/db.js2
-rw-r--r--src/db/dbAccess/user.js6
-rw-r--r--src/db/schemas/user.js23
-rw-r--r--src/dto/AlarmDto.js4
-rw-r--r--testFrontend/.idea/.idea.SafeNSound/.idea/vcs.xml2
-rw-r--r--testFrontend/SafeNSound.FakeUser/MonitorService.cs52
-rw-r--r--testFrontend/SafeNSound.FakeUser/Program.cs22
-rw-r--r--testFrontend/SafeNSound.FakeUser/RandomAlarmService.cs35
-rw-r--r--testFrontend/SafeNSound.FakeUser/SafeNSound.FakeUser.csproj10
-rw-r--r--testFrontend/SafeNSound.FakeUser/UserStore.cs97
-rw-r--r--testFrontend/SafeNSound.FakeUser/appsettings.json13
-rw-r--r--testFrontend/SafeNSound.Frontend/Pages/Alarm.razor5
-rw-r--r--testFrontend/SafeNSound.Frontend/Pages/Auth.razor82
-rw-r--r--testFrontend/SafeNSound.Frontend/Program.cs2
-rw-r--r--testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs17
-rw-r--r--testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs38
-rw-r--r--testFrontend/SafeNSound.Sdk/WrappedHttpClient.cs13
-rw-r--r--testFrontend/SafeNSound.sln14
-rw-r--r--testFrontend/SafeNSound.sln.DotSettings.user11
30 files changed, 583 insertions, 63 deletions
diff --git a/.idea/runConfigurations/dev_watch.xml b/.idea/runConfigurations/dev_watch.xml
new file mode 100644

index 0000000..6b364b6 --- /dev/null +++ b/.idea/runConfigurations/dev_watch.xml
@@ -0,0 +1,14 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="dev:watch" type="js.build_tools.npm" nameIsGenerated="true"> + <package-json value="$PROJECT_DIR$/package.json" /> + <command value="run" /> + <scripts> + <script value="dev:watch" /> + </scripts> + <node-interpreter value="project" /> + <envs> + <env name="LOG_QUERIES" value="false" /> + </envs> + <method v="2" /> + </configuration> +</component> \ No newline at end of file diff --git a/README.md b/README.md
index adbaeef..1a02eee 100644 --- a/README.md +++ b/README.md
@@ -15,5 +15,6 @@ Environment variables: | `PORT` | `3000` | The port the server will run on. | | `LOG_REQUESTS` | `-` | Requests to log to the console by status, `-` to invert. | | `DATABASE_SECRET_PATH` | ` ` | The path to the mongodb connection string. | -| `LOG_QUERIES` | ` ` | Whether to enable mongoose debug logs | +| `LOG_QUERIES` | `false` | Whether to enable mongoose debug logs | +| `LOG_AUTH` | `false` | Whether to enable authentication debug logs | | `JWT_SECRET_PATH` | ` ` | The path to the JWT secret certificate. | \ No newline at end of file diff --git a/package.json b/package.json
index 75b797d..bbd9060 100644 --- a/package.json +++ b/package.json
@@ -11,7 +11,7 @@ "coverage": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info", "build:http": "node src/api/routes.js | grep '^%' --line-buffered | sed -u 's/^% //g' > endpoints.http", "dev": "node .", - "dev:watch": "npx -y nodemon" + "dev:watch": "npx -y nodemon --ignore testFrontend/" }, "repository": { "type": "git", diff --git a/src/api/middlewares/authMiddleware.js b/src/api/middlewares/authMiddleware.js
index b91449f..34a96f4 100644 --- a/src/api/middlewares/authMiddleware.js +++ b/src/api/middlewares/authMiddleware.js
@@ -3,7 +3,7 @@ import { DbUser, UserType } from '#db/schemas/index.js'; import { SafeNSoundError } from '#util/error.js'; import { getUserById } from '#db/dbAccess/index.js'; -const shouldLogAuth = !!process.env['LOG_AUTH']; +const shouldLogAuth = process.env['LOG_AUTH'] === 'true'; function logAuth(...params) { if (shouldLogAuth) { console.log('[AUTH]', ...params); @@ -33,10 +33,25 @@ export async function useAuthentication(req, res, next) { )); logAuth('Token data:', auth); - req.user = await getUserById(auth.sub); - logAuth('User data:', req.user); + if (auth) { + req.user = await getUserById(auth.sub); + logAuth('User data:', req.user); - req.device = req.user.devices.find(device => device.id === auth.deviceId); + if (req.user) { + req.device = req.user.devices.find( + device => device.id === auth.deviceId + ); + logAuth('Device data:', req.device); + + if (req.device) { + if (req.device.lastSeen < Date.now() - 1000) { + logAuth('Updating device last seen for', req.device.id); + req.device.lastSeen = Date.now(); + await req.user.save(); + } + } + } + } next(); } diff --git a/src/api/middlewares/errorMiddleware.js b/src/api/middlewares/errorMiddleware.js
index d66c31d..2ca31db 100644 --- a/src/api/middlewares/errorMiddleware.js +++ b/src/api/middlewares/errorMiddleware.js
@@ -22,7 +22,6 @@ export function handleErrors(err, req, res, _next) { } if (err instanceof SafeNSoundError) { - console.log('meow'); console.error(err.stack); res.status(500).json(err); } else { diff --git a/src/api/routes/adminRoutes.js b/src/api/routes/adminRoutes.js new file mode 100644
index 0000000..7a1d1e2 --- /dev/null +++ b/src/api/routes/adminRoutes.js
@@ -0,0 +1,55 @@ +import { + requireMonitor, + requireUser, + requireRole, + requireAdmin +} from '#api/middlewares/index.js'; +import { DbUser, UserType } from '#db/schemas/index.js'; +import { RouteMethod } from '#api/RouteDescription.js'; +import { getUserById } from '#db/dbAccess/index.js'; +import { AlarmDto } from '#dto/AlarmDto.js'; + +/** + * @type {RouteDescription} + */ +export const adminGetUserIdsRoute = { + path: '/admin/allUserIds', + methods: { + get: new RouteMethod({ + middlewares: [requireAdmin], + description: 'Get all user IDs', + async method(req, res) { + // streaming json array + res.status(200); + res.write('[\n'); + + const users = DbUser.find().lean().cursor(); + for await (const user of users) { + res.write(JSON.stringify(user._id) + ',\n'); + } + + res.write(']'); + res.end(); + } + }) + } +}; +export const adminMonitorAllRoute = { + path: '/admin/monitorAllUsers', + methods: { + post: new RouteMethod({ + middlewares: [requireAdmin], + description: 'Monitor all users', + async method(req, res) { + const users = await DbUser.find({ type: UserType.USER }).lean(); + const monitoredUsers = users.map(user => user._id); + + // Update the admin's monitoredUsers + req.user.monitoredUsers = monitoredUsers; + await req.user.save(); + + res.status(204).send(); + } + }) + } +}; diff --git a/src/api/routes/alarmRoutes.js b/src/api/routes/alarmRoutes.js
index 23b79c1..ae5a88a 100644 --- a/src/api/routes/alarmRoutes.js +++ b/src/api/routes/alarmRoutes.js
@@ -3,7 +3,7 @@ import { requireUser, requireRole } from '#api/middlewares/index.js'; -import { UserType } from '#db/schemas/index.js'; +import { DbUser, UserType } from '#db/schemas/index.js'; import { RouteMethod } from '#api/RouteDescription.js'; import { getUserById } from '#db/dbAccess/index.js'; import { AlarmDto } from '#dto/AlarmDto.js'; @@ -12,7 +12,7 @@ import { AlarmDto } from '#dto/AlarmDto.js'; * @type {RouteDescription} */ export const alarmByUserRoute = { - path: '/alarm/:id', + path: '/user/:id/alarm', methods: { get: new RouteMethod({ middlewares: [requireMonitor], @@ -27,8 +27,10 @@ export const alarmByUserRoute = { description: 'Clear the alarm for a monitored user', async method(req, res) { const user = await getUserById(req.params.id); - user.alarm = null; - await user.save(); + if (user.alarm) { + user.alarm = null; + await user.save(); + } res.status(204).send(); } }) @@ -45,15 +47,24 @@ export const alarmListRoute = { middlewares: [requireMonitor], description: 'Get a list of all alarms for monitored users', async method(req, res) { - console.log(req.user.monitoredUsers); - const alarms = {}; - for (const userId of req.user.monitoredUsers) { - const user = await getUserById(userId); + // execute the query asynchronously and manually construct a response, for scaling reasons + const users = DbUser.find({ + _id: { $in: req.user.monitoredUsers } + }) + .lean() + .cursor(); + res.status(200); + res.write('{\n'); + for await (const user of users) { if (user.alarm) { - alarms[userId] = user.alarm; + // alarms[user._id] = user.alarm; + res.write( + `"${user._id}": ${JSON.stringify(user.alarm)},\n` + ); } } - res.send(alarms); + res.write('}'); + res.end(); } }) } @@ -76,7 +87,6 @@ export const alarmRoute = { middlewares: [requireUser], description: 'Raise an alarm', async method(req, res) { - console.log(req.body); req.user.alarm = await AlarmDto.create(req.body); await req.user.save(); res.status(204).send(); @@ -86,8 +96,10 @@ export const alarmRoute = { middlewares: [requireUser], description: 'Clear alarm', async method(req, res) { - req.user.alarm = null; - await req.user.save(); + if (req.user.alarm) { + req.user.alarm = null; + await req.user.save(); + } res.status(204).send(); } }) diff --git a/src/api/routes/assignedUserRoutes.js b/src/api/routes/assignedUserRoutes.js new file mode 100644
index 0000000..dac9b13 --- /dev/null +++ b/src/api/routes/assignedUserRoutes.js
@@ -0,0 +1,56 @@ +import { getUserById } from '#db/dbAccess/index.js'; +import { requireMonitor } from '#api/middlewares/index.js'; +import { RouteMethod } from '#api/RouteDescription.js'; +import { SafeNSoundError } from '#util/error.js'; + +/** + * @type {RouteDescription} + */ +export const assignedUserRoute = { + path: '/monitor/assignedUsers', + methods: { + get: new RouteMethod({ + middlewares: [requireMonitor], + description: 'Get assigned users', + async method(req, res) { + res.send(req.user.monitoredUsers); + } + }), + patch: { + middlewares: [requireMonitor], + description: 'Add an assigned user by ID', + async method(req, res) { + if (!req.body.userId) { + throw new SafeNSoundError({ + errCode: 'MISSING_FIELD_ERROR', + message: 'User ID is required', + field: 'userId' + }); + } + + if (req.user.monitoredUsers.includes(req.body.userId)) { + throw new SafeNSoundError({ + errCode: 'DUPLICATE_KEY_ERROR', + message: 'User is already assigned' + }); + } + + req.user.monitoredUsers.push(req.body.userId); + await req.user.save(); + res.status(204).send(); + } + }, + delete: { + middlewares: [requireMonitor], + description: 'Remove an assigned user by ID', + async method(req, res) { + // noinspection EqualityComparisonWithCoercionJS + req.user.monitoredUsers = req.user.monitoredUsers.filter( + userId => userId != req.body.userId + ); + await req.user.save(); + res.status(204).send(); + } + } + } +}; diff --git a/src/api/routes/auth/accountRoutes.js b/src/api/routes/auth/accountRoutes.js
index 547110e..34592ed 100644 --- a/src/api/routes/auth/accountRoutes.js +++ b/src/api/routes/auth/accountRoutes.js
@@ -2,6 +2,7 @@ import { deleteUser, loginUser, registerUser } from '#db/index.js'; import { AuthDto, RegisterDto } from '#dto/index.js'; import { RouteDescription, RouteMethod } from '#api/RouteDescription.js'; import { WhoAmIDto } from '#dto/auth/WhoAmIDto.js'; +import { requireAuth } from '#api/middlewares/index.js'; /** * @type {RouteDescription} @@ -101,6 +102,7 @@ export const whoAmI = { methods: { get: new RouteMethod({ description: 'Get current user', + middlewares: [requireAuth], async method(req, res) { const data = await WhoAmIDto.create({ userId: req.auth.sub, diff --git a/src/api/routes/index.js b/src/api/routes/index.js
index 4feeb11..bc6c853 100644 --- a/src/api/routes/index.js +++ b/src/api/routes/index.js
@@ -4,3 +4,5 @@ export * from './indexRoute.js'; export * from './auth/index.js'; export * from './budgetRoutes.js'; export * from './alarmRoutes.js'; +export * from './assignedUserRoutes.js'; +export * from './adminRoutes.js'; diff --git a/src/api/routes/statusRoute.js b/src/api/routes/statusRoute.js
index 2c111a8..8ccbf7d 100644 --- a/src/api/routes/statusRoute.js +++ b/src/api/routes/statusRoute.js
@@ -13,7 +13,7 @@ export const statusRoute = { const status = { status: 'ok', timestamp: new Date().toISOString(), - users: await User.countDocuments() + users: await DbUser.countDocuments() }; res.status(200).json(status); diff --git a/src/db/db.js b/src/db/db.js
index 2035731..9cbba80 100644 --- a/src/db/db.js +++ b/src/db/db.js
@@ -7,7 +7,7 @@ export async function initDb() { process.env['DATABASE_SECRET_PATH'] ); - if (process.env['LOG_QUERIES']) mongoose.set('debug', true); + if (process.env['LOG_QUERIES'] === 'true') mongoose.set('debug', true); try { const res = await connect(connectionString); diff --git a/src/db/dbAccess/user.js b/src/db/dbAccess/user.js
index 3bb06b6..69a83e4 100644 --- a/src/db/dbAccess/user.js +++ b/src/db/dbAccess/user.js
@@ -16,7 +16,6 @@ export async function getUserById(id) { }); } - console.log(user); return user; } @@ -32,7 +31,6 @@ async function getUserByAuth(data) { user = await DbUser.findOne({ username: data.username }); } - console.log('user', user); if (!user) { // Sneaky: prevent user enumeration throw new SafeNSoundError({ @@ -60,7 +58,7 @@ export async function registerUser(data) { if (!(data instanceof RegisterDto)) throw new Error('Invalid data type. Expected RegisterDto.'); - const salt = await genSalt(12); + const salt = await genSalt(1); const passwordHash = await hash(data.password, salt); if (!passwordHash) { throw new Error('Failed to hash password.'); @@ -88,7 +86,7 @@ export async function deleteUser(data) { export async function loginUser(data, deviceName) { const user = await getUserByAuth(data); const device = await user.devices.create({ - name: deviceName + name: deviceName ?? 'Unknown Device' }); user.devices.push(device); diff --git a/src/db/schemas/user.js b/src/db/schemas/user.js
index 7680319..7a4b2f4 100644 --- a/src/db/schemas/user.js +++ b/src/db/schemas/user.js
@@ -30,18 +30,21 @@ export const deviceSchema = new Schema({ } }); -export const alarmSchema = new Schema({ - createdAt: { - type: Date, - default: Date.now, - immutable: true +export const alarmSchema = new Schema( + { + reason: { + type: String, + enum: Object.values(AlarmType), + required: true + } }, - reason: { - type: String, - enum: Object.values(AlarmType), - required: true + { + timestamps: { + createdAt: true, + updatedAt: false + } } -}); +); /** * User schema for MongoDB. diff --git a/src/dto/AlarmDto.js b/src/dto/AlarmDto.js
index bec4e18..281311d 100644 --- a/src/dto/AlarmDto.js +++ b/src/dto/AlarmDto.js
@@ -22,8 +22,6 @@ export class AlarmDto { } } - var res = await AlarmDto.schema.validateAsync(obj); - console.log({ res, obj }); - return res; + return await AlarmDto.schema.validateAsync(obj); } } diff --git a/testFrontend/.idea/.idea.SafeNSound/.idea/vcs.xml b/testFrontend/.idea/.idea.SafeNSound/.idea/vcs.xml
index 94a25f7..6c0b863 100644 --- a/testFrontend/.idea/.idea.SafeNSound/.idea/vcs.xml +++ b/testFrontend/.idea/.idea.SafeNSound/.idea/vcs.xml
@@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="VcsDirectoryMappings"> - <mapping directory="$PROJECT_DIR$" vcs="Git" /> + <mapping directory="$PROJECT_DIR$/.." vcs="Git" /> </component> </project> \ No newline at end of file diff --git a/testFrontend/SafeNSound.FakeUser/MonitorService.cs b/testFrontend/SafeNSound.FakeUser/MonitorService.cs new file mode 100644
index 0000000..57d90a5 --- /dev/null +++ b/testFrontend/SafeNSound.FakeUser/MonitorService.cs
@@ -0,0 +1,52 @@ +using ArcaneLibs.Extensions; + +namespace SafeNSound.FakeUser; + +public class MonitorService(ILogger<MonitorService> logger, UserStore userStore): IHostedService { + private Task? _getAllAlarmsTask, _assignBudgetTask; + private readonly CancellationTokenSource _cts = new(); + + public async Task StartAsync(CancellationToken cancellationToken) { + _getAllAlarmsTask = GetAllAlarms(_cts.Token); + _assignBudgetTask = AssignBudget(_cts.Token); + } + + private async Task GetAllAlarms(CancellationToken cancellationToken) { + while (!cancellationToken.IsCancellationRequested) { + try { + var user = userStore.GetRandomMonitor(); + var alarms = await user.Client!.GetAllAlarms(); + if(alarms.Count > 0) + logger.LogInformation("Monitor {UserId} has outstanding alarms: {Alarm}", user.Auth.Username, alarms.ToJson(indent: false)); + // else + // logger.LogInformation("Monitor {UserId} found no alarms to query", user.Auth.Username); + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + } catch (Exception ex) { + logger.LogError(ex, "Error querying alarm"); + } + } + } + + private async Task AssignBudget(CancellationToken cancellationToken) { + while (!cancellationToken.IsCancellationRequested) { + try { + var user = userStore.GetRandomMonitor(); + // var alarms = await user.Client!.GetAllAlarms(); + // if(alarms.Count > 0) + // logger.LogInformation("Monitor {UserId} has outstanding alarms: {Alarm}", user.Auth.Username, alarms.ToJson(indent: false)); + // else + // logger.LogInformation("Monitor {UserId} found no alarms to query", user.Auth.Username); + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + + } catch (Exception ex) { + logger.LogError(ex, "Error querying alarm"); + } + + } + } + + public async Task StopAsync(CancellationToken cancellationToken) { + await _cts.CancelAsync(); + await _getAllAlarmsTask!; + } +} \ No newline at end of file diff --git a/testFrontend/SafeNSound.FakeUser/Program.cs b/testFrontend/SafeNSound.FakeUser/Program.cs
index 3751555..7c3eaff 100644 --- a/testFrontend/SafeNSound.FakeUser/Program.cs +++ b/testFrontend/SafeNSound.FakeUser/Program.cs
@@ -1,2 +1,24 @@ // See https://aka.ms/new-console-template for more information + +using SafeNSound.FakeUser; +using SafeNSound.Sdk; + Console.WriteLine("Hello, World!"); + +var builder = Host.CreateApplicationBuilder(args); +// longer shutdown timeout +builder.Services.Configure<HostOptions>(options => { + options.ShutdownTimeout = TimeSpan.FromSeconds(120); +}); + +builder.Services.AddSingleton<SafeNSoundConfiguration>(); +builder.Services.AddSingleton<SafeNSoundAuthentication>(); +builder.Services.AddSingleton<UserStore>(); +builder.Services.AddHostedService<UserStore>(sp => sp.GetRequiredService<UserStore>()); +builder.Services.AddHostedService<RandomAlarmService>(); +builder.Services.AddHostedService<MonitorService>(); + +// WrappedHttpClient.LogRequests = false; + +var host = builder.Build(); +host.Run(); \ No newline at end of file diff --git a/testFrontend/SafeNSound.FakeUser/RandomAlarmService.cs b/testFrontend/SafeNSound.FakeUser/RandomAlarmService.cs new file mode 100644
index 0000000..7835f89 --- /dev/null +++ b/testFrontend/SafeNSound.FakeUser/RandomAlarmService.cs
@@ -0,0 +1,35 @@ +namespace SafeNSound.FakeUser; + +public class RandomAlarmService(UserStore userStore): IHostedService { + private Task? _listenerTask; + private readonly CancellationTokenSource _cts = new(); + + public async Task StartAsync(CancellationToken cancellationToken) { + _listenerTask = Run(_cts.Token); + } + + private async Task Run(CancellationToken cancellationToken) { + while (!cancellationToken.IsCancellationRequested) { + try { + var user = userStore.GetRandomUser(); + if (Random.Shared.Next(100) > 90) { + await user.Client!.SetAlarm(new Sdk.AlarmDto { + Reason = "fall" + }); + } + else { + await user.Client!.DeleteAlarm(); + } + } + catch (Exception ex) { + Console.WriteLine($"Error setting/deleting alarm: {ex.Message}"); + } + + await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) { + await _cts.CancelAsync(); + } +} \ No newline at end of file diff --git a/testFrontend/SafeNSound.FakeUser/SafeNSound.FakeUser.csproj b/testFrontend/SafeNSound.FakeUser/SafeNSound.FakeUser.csproj
index fd4bd08..830faaa 100644 --- a/testFrontend/SafeNSound.FakeUser/SafeNSound.FakeUser.csproj +++ b/testFrontend/SafeNSound.FakeUser/SafeNSound.FakeUser.csproj
@@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk.Worker"> <PropertyGroup> <OutputType>Exe</OutputType> @@ -7,4 +7,12 @@ <Nullable>enable</Nullable> </PropertyGroup> + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.2"/> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\SafeNSound.Sdk\SafeNSound.Sdk.csproj" /> + </ItemGroup> + </Project> diff --git a/testFrontend/SafeNSound.FakeUser/UserStore.cs b/testFrontend/SafeNSound.FakeUser/UserStore.cs new file mode 100644
index 0000000..9b04efb --- /dev/null +++ b/testFrontend/SafeNSound.FakeUser/UserStore.cs
@@ -0,0 +1,97 @@ +using SafeNSound.Sdk; + +namespace SafeNSound.FakeUser; + +public class UserStore(SafeNSoundAuthentication authService, SafeNSoundConfiguration config) : IHostedService { + public List<ClientContainer> Admins { get; } = Enumerable.Range(0, 1).Select(_ => new ClientContainer()).ToList(); + public List<ClientContainer> Monitors { get; } = Enumerable.Range(0, 5).Select(_ => new ClientContainer()).ToList(); + public List<ClientContainer> Users { get; } = Enumerable.Range(0, 150000).Select(_ => new ClientContainer()).ToList(); + public List<ClientContainer> AllUsers => [.. Users, .. Monitors, .. Admins]; + + public ClientContainer GetRandomUser() { + ClientContainer user; + do { + user = Users[new Random().Next(Users.Count)]; + } while (user.Client == null); + + return user; + } + + public ClientContainer GetRandomMonitor() { + ClientContainer user; + do { + user = Monitors[new Random().Next(Monitors.Count)]; + } while (user.Client == null); + + return user; + } + + public ClientContainer GetRandomAdmin() { + ClientContainer user; + do { + user = Admins[new Random().Next(Admins.Count)]; + } while (user.Client == null); + + return user; + } + + public ClientContainer GetRandomUserOfAnyType() { + ClientContainer user; + do { + user = AllUsers[new Random().Next(AllUsers.Count)]; + } while (user.Client == null); + + return user; + } + + public async Task StartAsync(CancellationToken cancellationToken) { + Admins.ForEach(x => x.Auth.UserType = "admin"); + Monitors.ForEach(x => x.Auth.UserType = "monitor"); + var ss = new SemaphoreSlim(256, 256); + var tasks = ((ClientContainer[]) [..Users, ..Monitors, ..Admins]).Select(async container => { + await ss.WaitAsync(); + await authService.Register(container.Auth); + // container.Client = new SafeNSoundClient(config, (await authService.Login(container.Auth)).AccessToken); + // container.WhoAmI = await container.Client.WhoAmI(); + ss.Release(); + }).ToList(); + await Task.WhenAll(tasks); + + var users = Users.ToArray(); + tasks = Monitors.Select(async container => { + var items = Random.Shared.GetItems(users, Users.Count / Monitors.Count).DistinctBy(x=>x.WhoAmI!.UserId); + foreach (var user in items) { + await container.Client!.AddAssignedUser(user.WhoAmI!.UserId); + } + }).ToList(); + await Task.WhenAll(tasks); + } + + public async Task StopAsync(CancellationToken cancellationToken) { + await Task.WhenAll(Users.Select(Cleanup).ToList()); + await Task.WhenAll(Monitors.Select(Cleanup).ToList()); + await Task.WhenAll(Admins.Select(Cleanup).ToList()); + } + + private async Task Cleanup(ClientContainer container) { + if (container.Client == null) return; + try { + await container.Client.DeleteAccount(container.Auth); + } + catch { + Console.WriteLine("Failed to delete account for user: " + container.Auth.Username); + } + } + + public class ClientContainer { + public RegisterDto Auth { get; set; } = new() { + Email = $"{Guid.NewGuid()}@example.com", + Username = $"user-{Guid.NewGuid()}", + Password = Guid.NewGuid().ToString(), + UserType = "user" + }; + + public SafeNSoundClient? Client { get; set; } + public WhoAmI? WhoAmI { get; set; } + } +} \ No newline at end of file diff --git a/testFrontend/SafeNSound.FakeUser/appsettings.json b/testFrontend/SafeNSound.FakeUser/appsettings.json new file mode 100644
index 0000000..f77ef07 --- /dev/null +++ b/testFrontend/SafeNSound.FakeUser/appsettings.json
@@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "System": "Information", + "Microsoft": "Information", + "ArcaneLibs.Blazor.Components.AuthorizedImage": "Information" + } + }, + "SafeNSound": { + "BaseUrl": "http://localhost:3000" + } +} diff --git a/testFrontend/SafeNSound.Frontend/Pages/Alarm.razor b/testFrontend/SafeNSound.Frontend/Pages/Alarm.razor
index 59b8e4c..1650a75 100644 --- a/testFrontend/SafeNSound.Frontend/Pages/Alarm.razor +++ b/testFrontend/SafeNSound.Frontend/Pages/Alarm.razor
@@ -5,7 +5,7 @@ <LinkButton OnClick="@RaiseAlarm">Raise</LinkButton> <LinkButton OnClick="@GetAlarm">Get</LinkButton> <LinkButton OnClick="@ClearAlarm">Delete</LinkButton> -<LinkButton OnClick="@ClearAlarm">Get all monitored</LinkButton> +<LinkButton OnClick="@GetAllAlarms">Get all monitored</LinkButton> <br/><br/> @if (Exception != null) { @@ -59,7 +59,8 @@ } private async Task GetAllAlarms() { - Result = await App.Client.GetAllAlarms(); + Result = await App.Client!.GetAllAlarms(); + StateHasChanged(); } } \ No newline at end of file diff --git a/testFrontend/SafeNSound.Frontend/Pages/Auth.razor b/testFrontend/SafeNSound.Frontend/Pages/Auth.razor
index 7a9f5d1..5540f02 100644 --- a/testFrontend/SafeNSound.Frontend/Pages/Auth.razor +++ b/testFrontend/SafeNSound.Frontend/Pages/Auth.razor
@@ -1,7 +1,7 @@ @page "/Auth" <h1>Auth</h1> - +<u>User:</u><br/> <span>Username (L?, R): </span> <FancyTextBox @bind-Value="@Username"/><br/> <span>Email (L? R): </span> @@ -15,8 +15,16 @@ <LinkButton OnClick="@Login">Login</LinkButton> <LinkButton OnClick="@WhoAmI">Who Am I</LinkButton> <LinkButton OnClick="@Delete">Delete</LinkButton> +<LinkButton OnClick="@MakeFullAdmin">Register superadmin</LinkButton> <br/><br/> +<u>Monitor:</u><br/> +<span>User ID: </span> +<FancyTextBox @bind-Value="@TargetUserId"/><br/> +<LinkButton OnClick="@GetAssignedUsers">Get</LinkButton> +<LinkButton OnClick="@AddAssignedUser">Add</LinkButton> +<LinkButton OnClick="@RemoveAssignedUser">Remove</LinkButton> + @if (Exception != null) { <div class="alert alert-danger"> <strong>Error:</strong><br/> @@ -36,10 +44,12 @@ } @code { - private string Username { get; set; } = String.Empty; - private string Email { get; set; } = String.Empty; - private string Password { get; set; } = String.Empty; - private string UserType { get; set; } = ""; + private string Username { get; set; } = string.Empty; + private string Email { get; set; } = string.Empty; + private string Password { get; set; } = string.Empty; + private string UserType { get; set; } = string.Empty; + + private string TargetUserId { get; set; } = string.Empty; private Exception? Exception { get; set; } private object? Result { get; set; } @@ -74,7 +84,7 @@ Result = null; Exception = null; try { - SafeNSoundAuthResult result; + AuthResult result; Result = result = await Authentication.Login(new() { Username = Username, Password = Password, @@ -118,4 +128,64 @@ StateHasChanged(); } + private async Task GetAssignedUsers() { + Result = null; + Exception = null; + try { + Result = await App.Client!.GetAssignedUsers(); + } + catch (Exception ex) { + Exception = ex; + } + StateHasChanged(); + } + + private async Task AddAssignedUser() { + Result = null; + Exception = null; + try { + await App.Client!.AddAssignedUser(TargetUserId); + await GetAssignedUsers(); + } + catch (Exception ex) { + Exception = ex; + } + StateHasChanged(); + } + + private async Task RemoveAssignedUser() { + Result = null; + Exception = null; + try { + await App.Client!.RemoveAssignedUser(TargetUserId); + await GetAssignedUsers(); + } + catch (Exception ex) { + Exception = ex; + } + StateHasChanged(); + } + + private async Task MakeFullAdmin() { + Result = null; + Exception = null; + try { + AuthResult result; + RegisterDto auth = new() { + Username = Guid.NewGuid().ToString(), + Password = Guid.NewGuid().ToString(), + Email = Guid.NewGuid() + "@example.com", + UserType = "admin" + }; + await Authentication.Register(auth); + Result = result = await Authentication.Login(auth); + App.Client = new SafeNSoundClient(Config, result.AccessToken); + await App.Client.MonitorAllUsers(); + } + catch (Exception ex) { + Exception = ex; + } + StateHasChanged(); + } + } \ No newline at end of file diff --git a/testFrontend/SafeNSound.Frontend/Program.cs b/testFrontend/SafeNSound.Frontend/Program.cs
index 2642a75..25537ba 100644 --- a/testFrontend/SafeNSound.Frontend/Program.cs +++ b/testFrontend/SafeNSound.Frontend/Program.cs
@@ -45,4 +45,6 @@ builder.Services.AddBlazoredLocalStorage(config => { config.JsonSerializerOptions.WriteIndented = false; }); +WrappedHttpClient.LogRequests = false; + await builder.Build().RunAsync(); \ No newline at end of file diff --git a/testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs b/testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs
index 333db6d..d0ed245 100644 --- a/testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs +++ b/testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs
@@ -13,13 +13,13 @@ public class SafeNSoundAuthentication(SafeNSoundConfiguration config) { res.EnsureSuccessStatusCode(); } - public async Task<SafeNSoundAuthResult> Login(AuthDto authDto) { + public async Task<AuthResult> Login(AuthDto authDto) { var hc = new WrappedHttpClient() { BaseAddress = new Uri(config.BaseUri) }; var res = await hc.PostAsJsonAsync("/auth/login", authDto); - return (await res.Content.ReadFromJsonAsync<SafeNSoundAuthResult>())!; + return (await res.Content.ReadFromJsonAsync<AuthResult>())!; } public async Task Delete(AuthDto authDto) { @@ -32,16 +32,7 @@ public class SafeNSoundAuthentication(SafeNSoundConfiguration config) { } } -public class RegisterDto { - [JsonPropertyName("username")] - public string Username { get; set; } = string.Empty; - - [JsonPropertyName("password")] - public string Password { get; set; } = string.Empty; - - [JsonPropertyName("email")] - public string Email { get; set; } = string.Empty; - +public class RegisterDto : AuthDto { [JsonPropertyName("type")] public string UserType { get; set; } = string.Empty; } @@ -68,7 +59,7 @@ public class WhoAmI { public required string UserType { get; set; } } -public class SafeNSoundAuthResult : WhoAmI { +public class AuthResult : WhoAmI { [JsonPropertyName("accessToken")] public required string AccessToken { get; set; } } \ No newline at end of file diff --git a/testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs b/testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs
index c6f16f2..8291178 100644 --- a/testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs +++ b/testFrontend/SafeNSound.Sdk/SafeNSoundClient.cs
@@ -1,4 +1,5 @@ using System.Net.Http.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace SafeNSound.Sdk; @@ -54,10 +55,47 @@ public class SafeNSoundClient(SafeNSoundConfiguration config, string accessToken res.EnsureSuccessStatusCode(); return (await res.Content.ReadFromJsonAsync<Dictionary<string, AlarmDto>>())!; } + + public async Task DeleteAccount(AuthDto auth) { + var res = await HttpClient.DeleteAsJsonAsync("/auth/delete", auth); + res.EnsureSuccessStatusCode(); + } + + public async Task<List<string>> GetAssignedUsers() { + var res = await HttpClient.GetAsync("/monitor/assignedUsers"); + res.EnsureSuccessStatusCode(); + return (await res.Content.ReadFromJsonAsync<List<string>>())!; + } + + public async Task AddAssignedUser(string targetUserId) { + var res = await HttpClient.PatchAsJsonAsync("/monitor/assignedUsers", new { userId = targetUserId }); + res.EnsureSuccessStatusCode(); + } + + public async Task RemoveAssignedUser(string targetUserId) { + var res = await HttpClient.DeleteAsJsonAsync($"/monitor/assignedUsers", new { userId = targetUserId }); + res.EnsureSuccessStatusCode(); + } + + public async IAsyncEnumerable<string> GetAllUserIdsEnumerable() { + var res = await HttpClient.GetAsync($"/admin/allUserIds"); + res.EnsureSuccessStatusCode(); + await foreach (var item in res.Content.ReadFromJsonAsAsyncEnumerable<string>()) { + yield return item!; + } + } + + public async Task MonitorAllUsers() { + var res = await HttpClient.PostAsync("/admin/monitorAllUsers", null); + res.EnsureSuccessStatusCode(); + } } public class AlarmDto { [JsonPropertyName("reason")] public required string Reason { get; set; } + + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; set; } } \ No newline at end of file diff --git a/testFrontend/SafeNSound.Sdk/WrappedHttpClient.cs b/testFrontend/SafeNSound.Sdk/WrappedHttpClient.cs
index e4b4500..aa785cd 100644 --- a/testFrontend/SafeNSound.Sdk/WrappedHttpClient.cs +++ b/testFrontend/SafeNSound.Sdk/WrappedHttpClient.cs
@@ -157,12 +157,15 @@ public class WrappedHttpClient "Access-Control-Allow-Methods", "Access-Control-Allow-Headers", "Access-Control-Expose-Headers", + "Access-Control-Allow-Credentials", "Cache-Control", "Cross-Origin-Resource-Policy", "X-Content-Security-Policy", "Referrer-Policy", "X-Robots-Tag", - "Content-Security-Policy" + "Content-Security-Policy", + "Keep-Alive", + "ETag" ])); return responseMessage; @@ -338,4 +341,12 @@ public class WrappedHttpClient }; return await SendAsync(request); } + + public async Task<HttpResponseMessage> PatchAsJsonAsync<T>(string url, T payload) { + var request = new HttpRequestMessage(HttpMethod.Patch, url) + { + Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") + }; + return await SendAsync(request); + } } \ No newline at end of file diff --git a/testFrontend/SafeNSound.sln b/testFrontend/SafeNSound.sln
index c5bee09..95edddc 100644 --- a/testFrontend/SafeNSound.sln +++ b/testFrontend/SafeNSound.sln
@@ -22,6 +22,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Timings", "Arcan EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "ArcaneLibs\ArcaneLibs.UsageTest\ArcaneLibs.UsageTest.csproj", "{24FC30D3-E68C-471D-99DA-63C469C3262C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeNSound.FakeUser", "SafeNSound.FakeUser\SafeNSound.FakeUser.csproj", "{AB7CDBEC-4D64-4FCF-B4F0-5FF3F86084B8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -152,6 +154,18 @@ Global {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|x64.Build.0 = Release|Any CPU {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|x86.ActiveCfg = Release|Any CPU {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|x86.Build.0 = Release|Any CPU + {AB7CDBEC-4D64-4FCF-B4F0-5FF3F86084B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB7CDBEC-4D64-4FCF-B4F0-5FF3F86084B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB7CDBEC-4D64-4FCF-B4F0-5FF3F86084B8}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB7CDBEC-4D64-4FCF-B4F0-5FF3F86084B8}.Debug|x64.Build.0 = Debug|Any CPU + {AB7CDBEC-4D64-4FCF-B4F0-5FF3F86084B8}.Debug|x86.ActiveCfg = Debug|Any CPU + {AB7CDBEC-4D64-4FCF-B4F0-5FF3F86084B8}.Debug|x86.Build.0 = Debug|Any CPU + {AB7CDBEC-4D64-4FCF-B4F0-5FF3F86084B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB7CDBEC-4D64-4FCF-B4F0-5FF3F86084B8}.Release|Any CPU.Build.0 = Release|Any CPU + {AB7CDBEC-4D64-4FCF-B4F0-5FF3F86084B8}.Release|x64.ActiveCfg = Release|Any CPU + {AB7CDBEC-4D64-4FCF-B4F0-5FF3F86084B8}.Release|x64.Build.0 = Release|Any CPU + {AB7CDBEC-4D64-4FCF-B4F0-5FF3F86084B8}.Release|x86.ActiveCfg = Release|Any CPU + {AB7CDBEC-4D64-4FCF-B4F0-5FF3F86084B8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/testFrontend/SafeNSound.sln.DotSettings.user b/testFrontend/SafeNSound.sln.DotSettings.user new file mode 100644
index 0000000..ae9fb74 --- /dev/null +++ b/testFrontend/SafeNSound.sln.DotSettings.user
@@ -0,0 +1,11 @@ +<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATaskAwaiter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fba81a6af46624b56ac6210bdbcc99af2d19e00_003F4b_003Fb9d0e80e_003FTaskAwaiter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> + <s:Boolean x:Key="/Default/Monitoring/Counters/=System_002ERuntime/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/Monitoring/Counters/=Microsoft_002EAspNetCore_002EHttp_002EConnections/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/Monitoring/Counters/=Microsoft_002EAspNetCore_002EHosting/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/Monitoring/Counters/=Microsoft_002DAspNetCore_002DServer_002DKestrel/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/Monitoring/Counters/=System_002ENet_002EHttp/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/Monitoring/Counters/=System_002ENet_002ENameResolution/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/Monitoring/Counters/=System_002ENet_002ESecurity/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/Monitoring/Counters/=Microsoft_002EEntityFrameworkCore/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/Monitoring/Counters/=System_002ENet_002ESockets/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> \ No newline at end of file