diff --git a/backend/src/api/auth.rs b/backend/src/api/auth.rs
index 1622059..305e4c6 100644
--- a/backend/src/api/auth.rs
+++ b/backend/src/api/auth.rs
@@ -445,41 +445,61 @@ fn logout(cookies: &CookieJar<'_>) -> String {
String::from("User logged out")
}
+#[serde_as]
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct AuthToken {
+ token: String,
+ #[serde_as(as = "TimestampSeconds")]
+ created_at: SystemTime,
+}
+
+impl From for AuthToken {
+ fn from(value: models::AuthToken) -> Self {
+ Self {
+ token: value.token,
+ created_at: value.creation_time,
+ }
+ }
+}
+
/// Beware: this replaces the current auth token with a new one. The old one becomes invalid.
#[post("/auth/token")]
-async fn create_auth_token(user: UserId, db: DbConn) -> Result {
+async fn create_auth_token(user: UserId, db: DbConn) -> Result, AuthError> {
let uid = BigDecimal::from_u64(user.0).ok_or_else(AuthError::bigdecimal_error)?;
- let auth_token: String = iter::repeat(())
+ let random_token: String = iter::repeat(())
.map(|_| OsRng.sample(Alphanumeric))
.map(char::from)
.take(32)
.collect();
+ let auth_token = models::AuthToken {
+ user_id: uid,
+ token: random_token.clone(),
+ creation_time: SystemTime::now(),
+ };
+
{
- let auth_token = models::AuthToken {
- user_id: uid,
- token: auth_token.clone(),
- creation_time: SystemTime::now(),
- };
+ let auth_token = auth_token.clone();
db.run(move |c| {
use crate::db::schema::authtokens::dsl::*;
diesel::insert_into(authtokens)
- .values(&auth_token.clone())
+ .values(&auth_token)
.on_conflict(user_id)
.do_update()
- .set(auth_token)
+ .set(&auth_token)
.execute(c)
})
.await?;
}
- Ok(auth_token)
+ Ok(Json(auth_token.into()))
}
#[get("/auth/token")]
-async fn get_auth_token(user: UserId, db: DbConn) -> Result {
+async fn get_auth_token(user: UserId, db: DbConn) -> Result, AuthError> {
let uid = BigDecimal::from_u64(user.0).ok_or_else(AuthError::bigdecimal_error)?;
let token = db
@@ -497,5 +517,5 @@ async fn get_auth_token(user: UserId, db: DbConn) -> Result {
}
})?;
- Ok(token.token)
+ Ok(Json(token.into()))
}
diff --git a/backend/src/api/sounds.rs b/backend/src/api/sounds.rs
index d0ab043..c80bda2 100644
--- a/backend/src/api/sounds.rs
+++ b/backend/src/api/sounds.rs
@@ -1,14 +1,8 @@
-use crate::api::auth::UserId;
-use crate::api::Snowflake;
-use crate::audio_utils;
-use crate::db::models;
-use crate::db::DbConn;
-use crate::discord::management::check_guild_moderator;
-use crate::discord::management::check_guild_user;
-use crate::discord::management::get_guilds_for_user;
-use crate::discord::management::PermissionError;
-use crate::file_handling;
-use crate::CacheHttp;
+use std::convert::TryFrom;
+use std::num::TryFromIntError;
+use std::path::PathBuf;
+use std::time::SystemTime;
+
use bigdecimal::BigDecimal;
use bigdecimal::FromPrimitive;
use bigdecimal::ToPrimitive;
@@ -26,12 +20,20 @@ use serde::Serialize;
use serde_with::serde_as;
use serde_with::TimestampSeconds;
use serenity::model::id::GuildId;
-use std::convert::TryFrom;
-use std::num::TryFromIntError;
-use std::path::PathBuf;
-use std::time::SystemTime;
use tokio::fs;
+use crate::api::auth::UserId;
+use crate::api::Snowflake;
+use crate::audio_utils;
+use crate::db::models;
+use crate::db::DbConn;
+use crate::discord::management::check_guild_moderator;
+use crate::discord::management::check_guild_user;
+use crate::discord::management::get_guilds_for_user;
+use crate::discord::management::PermissionError;
+use crate::file_handling;
+use crate::CacheHttp;
+
pub fn get_routes() -> Vec {
routes![
list_sounds,
@@ -114,7 +116,7 @@ struct Sound {
#[serde_as(as = "TimestampSeconds")]
created_at: SystemTime,
volume_adjustment: Option,
- soundfile: Option,
+ sound_file: Option,
}
#[serde_as]
@@ -144,7 +146,7 @@ impl TryFrom<(models::Sound, Option)> for Sound {
category: s.category,
created_at: s.created_at,
volume_adjustment: s.volume_adjustment,
- soundfile: f.map(|f| Soundfile {
+ sound_file: f.map(|f| Soundfile {
max_volume: f.max_volume,
mean_volume: f.mean_volume,
length: f.length,
diff --git a/frontend/src/app/keybind-generator/keybind-generator.component.html b/frontend/src/app/keybind-generator/keybind-generator.component.html
index b63aa4c..bc43c02 100644
--- a/frontend/src/app/keybind-generator/keybind-generator.component.html
+++ b/frontend/src/app/keybind-generator/keybind-generator.component.html
@@ -14,9 +14,19 @@
automatically play sounds when a key combination on your computer is pressed.
The generated script contains a personal auth token for your account. Do not share it with others. It expires after a week, at which
- point you will have to re-download the AutoHotkey script.
The generated script contains a personal auth token for your account. Do not share it with others. You can regenerate it manually,
+ making all previously downloaded scripts invalid.
+
+
+ No auth token generated.
+ autorenew Generate token
+
+
+ Auth token generated on {{ authToken().createdAt * 1000 | date : 'short' }}
+ autorenew Regenerate token
+
+
diff --git a/frontend/src/app/keybind-generator/keybind-generator.component.scss b/frontend/src/app/keybind-generator/keybind-generator.component.scss
index cd17686..187ee26 100644
--- a/frontend/src/app/keybind-generator/keybind-generator.component.scss
+++ b/frontend/src/app/keybind-generator/keybind-generator.component.scss
@@ -35,6 +35,12 @@ mat-toolbar {
}
}
+.auth-token-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
.table-wrapper {
margin-bottom: 16px;
overflow-x: auto;
diff --git a/frontend/src/app/keybind-generator/keybind-generator.component.ts b/frontend/src/app/keybind-generator/keybind-generator.component.ts
index 63c3956..81c871f 100644
--- a/frontend/src/app/keybind-generator/keybind-generator.component.ts
+++ b/frontend/src/app/keybind-generator/keybind-generator.component.ts
@@ -2,10 +2,11 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, signal } from '@
import { MatSnackBar } from '@angular/material/snack-bar';
import { pull } from 'lodash-es';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
-import { Subject } from 'rxjs';
+import { catchError, forkJoin, of, Subject, tap, throwError } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { HttpErrorResponse } from '@angular/common/http';
import { AppSettingsService } from '../services/app-settings.service';
-import { ApiService } from '../services/api.service';
+import { ApiService, AuthToken } from '../services/api.service';
import { Sound, SoundsService } from '../services/sounds.service';
import { KeyCombination } from './keycombination-input/key-combination-input.component';
@@ -29,10 +30,16 @@ export class KeybindGeneratorComponent {
readonly user = this.apiService.user();
- readonly data$ = this.soundsService.loadSounds();
- readonly dataLoaded$ = new Subject();
+ readonly data$ = forkJoin([
+ this.soundsService.loadSounds(),
+ this.apiService
+ .getAuthToken()
+ .pipe(catchError(error => (error instanceof HttpErrorResponse && error.status === 404 ? of(null) : throwError(() => error)))),
+ ]);
+ readonly dataLoaded$ = new Subject<[Array, AuthToken | null]>();
readonly sounds = signal(null);
+ readonly authToken = signal(null);
keybinds: Keybind[];
constructor(
@@ -51,9 +58,10 @@ export class KeybindGeneratorComponent {
}
this.keybinds = initialKeybinds;
- this.dataLoaded$.pipe(takeUntilDestroyed()).subscribe(sounds => {
+ this.dataLoaded$.pipe(takeUntilDestroyed()).subscribe(([sounds, authToken]) => {
this.cleanupKeybinds(this.keybinds, sounds);
this.sounds.set(sounds);
+ this.authToken.set(authToken);
});
}
@@ -64,6 +72,8 @@ export class KeybindGeneratorComponent {
const sound = sounds.find(s => keybind.command && typeof keybind.command === 'object' && s.id === keybind.command.id);
if (sound != null) {
keybind.command = sound;
+ } else if (typeof keybind.command !== 'string') {
+ keybind.command = null;
}
}
}
@@ -90,10 +100,26 @@ export class KeybindGeneratorComponent {
this.downloadText(JSON.stringify(this.keybinds), 'keybinds.json');
}
- generateAutohotkey() {
+ regenerateToken() {
this.apiService.generateAuthToken().subscribe({
- next: authtoken => {
- const script = this.generateAutohotkeyScript(authtoken, this.keybinds);
+ next: newToken => this.authToken.set(newToken),
+ error: error => {
+ console.error(error);
+ this.snackBar.open('Failed to generate new auth token.', 'Damn');
+ },
+ });
+ }
+
+ generateAutohotkey() {
+ const authToken = this.authToken();
+
+ const observable = authToken
+ ? of(authToken)
+ : this.apiService.generateAuthToken().pipe(tap(authToken => this.authToken.set(authToken)));
+
+ observable.subscribe({
+ next: authToken => {
+ const script = this.generateAutohotkeyScript(authToken.token, this.keybinds);
this.downloadText(script, 'soundboard.ahk');
},
error: error => {
@@ -124,9 +150,9 @@ export class KeybindGeneratorComponent {
};
}
- private generateAutohotkeyScript(authtoken: string, keybinds: Keybind[]) {
+ private generateAutohotkeyScript(authToken: string, keybinds: Keybind[]) {
let script = 'PlaySound(server, id) {\n';
- script += `command := "curl -X POST -H ""Authorization: Bearer ${authtoken}"" `;
+ script += `command := "curl -X POST -H ""Authorization: Bearer ${authToken}"" `;
// eslint-disable-next-line max-len
script += `""${window.location.protocol}//${window.location.hostname}:${window.location.port}/api/guilds/" . server . "/play/" . id . """"\n`;
script += 'shell := ComObjCreate("WScript.Shell")\n';
@@ -135,7 +161,7 @@ export class KeybindGeneratorComponent {
script += '}\n';
script += 'ExecCommand(server, cmd) {\n';
- script += `command := "curl -X POST -H ""Authorization: Bearer ${authtoken}"" `;
+ script += `command := "curl -X POST -H ""Authorization: Bearer ${authToken}"" `;
// eslint-disable-next-line max-len
script += `""${window.location.protocol}//${window.location.hostname}:${window.location.port}/api/guilds/" . server . "/" . cmd . """"\n`;
script += 'shell := ComObjCreate("WScript.Shell")\n';
diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts
index 497c693..fd8066e 100644
--- a/frontend/src/app/services/api.service.ts
+++ b/frontend/src/app/services/api.service.ts
@@ -35,6 +35,11 @@ export interface RandomInfix {
displayName: string;
}
+export interface AuthToken {
+ token: string;
+ createdAt: number;
+}
+
@Injectable({
providedIn: 'root',
})
@@ -71,10 +76,10 @@ export class ApiService {
}
generateAuthToken() {
- return this.http.post('/api/auth/token', {}, { responseType: 'text' });
+ return this.http.post('/api/auth/token', {});
}
getAuthToken() {
- return this.http.get('/api/auth/token', { responseType: 'text' });
+ return this.http.get('/api/auth/token');
}
}
diff --git a/frontend/src/app/services/sounds.service.ts b/frontend/src/app/services/sounds.service.ts
index 311ac3a..e59ffc4 100644
--- a/frontend/src/app/services/sounds.service.ts
+++ b/frontend/src/app/services/sounds.service.ts
@@ -11,7 +11,7 @@ interface ApiSound {
category: string;
createdAt: number;
volumeAdjustment?: number;
- soundfile?: Readonly;
+ soundFile?: Readonly;
}
export interface SoundFile {
@@ -28,7 +28,7 @@ export class Sound implements ApiSound {
category: string;
createdAt: number;
volumeAdjustment?: number;
- soundfile?: Readonly;
+ soundFile?: Readonly;
constructor(base: ApiSound) {
this.id = base.id;
@@ -37,7 +37,7 @@ export class Sound implements ApiSound {
this.category = base.category;
this.createdAt = base.createdAt;
this.volumeAdjustment = base.volumeAdjustment;
- this.soundfile = base.soundfile;
+ this.soundFile = base.soundFile;
}
getDownloadUrl() {
diff --git a/frontend/src/app/settings/sound-manager/sound-details/sound-details.component.html b/frontend/src/app/settings/sound-manager/sound-details/sound-details.component.html
index ad51053..ac30830 100644
--- a/frontend/src/app/settings/sound-manager/sound-details/sound-details.component.html
+++ b/frontend/src/app/settings/sound-manager/sound-details/sound-details.component.html
@@ -2,7 +2,7 @@
{{ sound().name }}{{ soundEntry.hasChanges() ? '*' : '' }}
- error_outline
@@ -53,25 +53,25 @@ Statistics
Created
-
+
Max Volume
-
{{ sound().soundfile.maxVolume | number : '1.1-1' }} dB
+
{{ sound().soundFile.maxVolume | number : '1.1-1' }} dB
Mean Volume
-
{{ sound().soundfile.meanVolume | number : '1.1-1' }} dB
+
{{ sound().soundFile.meanVolume | number : '1.1-1' }} dB
Length
-
{{ sound().soundfile.length | number : '1.3-3' }} s
+
{{ sound().soundFile.length | number : '1.3-3' }} s
@@ -89,7 +89,7 @@ Statistics