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. + + + + Auth token generated on {{ authToken().createdAt * 1000 | date : 'short' }} + + +

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
Uploaded
@@ -89,7 +89,7 @@

Statistics

file_upload {{ sound().soundFile == null ? 'Upload' : 'Replace' }} sound
diff --git a/frontend/src/app/settings/sound-manager/sound-manager.component.ts b/frontend/src/app/settings/sound-manager/sound-manager.component.ts index d7d4393..67dbbda 100644 --- a/frontend/src/app/settings/sound-manager/sound-manager.component.ts +++ b/frontend/src/app/settings/sound-manager/sound-manager.component.ts @@ -139,7 +139,7 @@ export class SoundManagerComponent { mergeMap(sound => this.soundsService .uploadSound(sound, file) - .pipe(map(soundfile => new SoundEntry(this.soundsService, new Sound({ ...sound, soundfile })))) + .pipe(map(soundFile => new SoundEntry(this.soundsService, new Sound({ ...sound, soundFile })))) ) ); }, 5), @@ -231,7 +231,7 @@ export class SoundEntry { } replaceSoundFile(soundFile: SoundFile) { - this.sound.mutate(sound => (sound.soundfile = soundFile)); - this.internalSound.mutate(internalSound => (internalSound.soundfile = soundFile)); + this.sound.mutate(sound => (sound.soundFile = soundFile)); + this.internalSound.mutate(internalSound => (internalSound.soundFile = soundFile)); } } diff --git a/frontend/src/app/soundboard/soundboard.component.ts b/frontend/src/app/soundboard/soundboard.component.ts index ded65f4..892c82e 100644 --- a/frontend/src/app/soundboard/soundboard.component.ts +++ b/frontend/src/app/soundboard/soundboard.component.ts @@ -85,8 +85,8 @@ export class SoundboardComponent { next: () => { if (this.settings.debug()) { let volString = - sound.soundfile != null - ? `Volume: Max ${sound.soundfile.maxVolume.toFixed(1)} dB, Average ${sound.soundfile.meanVolume.toFixed(1)} dB, ` + sound.soundFile != null + ? `Volume: Max ${sound.soundFile.maxVolume.toFixed(1)} dB, Average ${sound.soundFile.meanVolume.toFixed(1)} dB, ` : ''; volString += sound.volumeAdjustment != null ? `Manual adjustment ${sound.volumeAdjustment} dB` : 'Automatic adjustment'; this.snackBar.open(volString, 'Ok');