Skip to content

Commit

Permalink
make keybind generator reuse api tokens, allow explicitly regeneratin…
Browse files Browse the repository at this point in the history
…g them
  • Loading branch information
dominikks committed Nov 13, 2023
1 parent 5fb2990 commit d3781fb
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 60 deletions.
44 changes: 32 additions & 12 deletions backend/src/api/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>")]
created_at: SystemTime,
}

impl From<models::AuthToken> 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<String, AuthError> {
async fn create_auth_token(user: UserId, db: DbConn) -> Result<Json<AuthToken>, 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<String, AuthError> {
async fn get_auth_token(user: UserId, db: DbConn) -> Result<Json<AuthToken>, AuthError> {
let uid = BigDecimal::from_u64(user.0).ok_or_else(AuthError::bigdecimal_error)?;

let token = db
Expand All @@ -497,5 +517,5 @@ async fn get_auth_token(user: UserId, db: DbConn) -> Result<String, AuthError> {
}
})?;

Ok(token.token)
Ok(Json(token.into()))
}
36 changes: 19 additions & 17 deletions backend/src/api/sounds.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Route> {
routes![
list_sounds,
Expand Down Expand Up @@ -114,7 +116,7 @@ struct Sound {
#[serde_as(as = "TimestampSeconds<String>")]
created_at: SystemTime,
volume_adjustment: Option<f32>,
soundfile: Option<Soundfile>,
sound_file: Option<Soundfile>,
}

#[serde_as]
Expand Down Expand Up @@ -144,7 +146,7 @@ impl TryFrom<(models::Sound, Option<models::Soundfile>)> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,19 @@
automatically play sounds when a key combination on your computer is pressed.</p
>
<p
>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.</p
>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.</p
>
<p class="auth-token-row" [ngSwitch]="authToken() != null">
<ng-container *ngSwitchCase="false">
<span>No auth token generated.</span>
<button mat-button (click)="regenerateToken()"><mat-icon>autorenew</mat-icon> Generate token</button>
</ng-container>
<ng-container *ngSwitchCase="true">
<span>Auth token generated on {{ authToken().createdAt * 1000 | date : 'short' }}</span>
<button mat-button (click)="regenerateToken()"><mat-icon>autorenew</mat-icon> Regenerate token</button>
</ng-container>
</p>
<div class="table-wrapper mat-elevation-z8">
<mat-table [dataSource]="keybinds" cdkDropList [cdkDropListData]="keybinds" (cdkDropListDropped)="onDrop($event)">
<ng-container matColumnDef="dragDrop">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ mat-toolbar {
}
}

.auth-token-row {
display: flex;
align-items: center;
gap: 8px;
}

.table-wrapper {
margin-bottom: 16px;
overflow-x: auto;
Expand Down
48 changes: 37 additions & 11 deletions frontend/src/app/keybind-generator/keybind-generator.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -29,10 +30,16 @@ export class KeybindGeneratorComponent {

readonly user = this.apiService.user();

readonly data$ = this.soundsService.loadSounds();
readonly dataLoaded$ = new Subject<Sound[]>();
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<Sound>, AuthToken | null]>();

readonly sounds = signal<Sound[]>(null);
readonly authToken = signal<AuthToken | null>(null);
keybinds: Keybind[];

constructor(
Expand All @@ -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);
});
}

Expand All @@ -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;
}
}
}
Expand All @@ -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 => {
Expand Down Expand Up @@ -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';
Expand All @@ -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';
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/app/services/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export interface RandomInfix {
displayName: string;
}

export interface AuthToken {
token: string;
createdAt: number;
}

@Injectable({
providedIn: 'root',
})
Expand Down Expand Up @@ -71,10 +76,10 @@ export class ApiService {
}

generateAuthToken() {
return this.http.post('/api/auth/token', {}, { responseType: 'text' });
return this.http.post<AuthToken>('/api/auth/token', {});
}

getAuthToken() {
return this.http.get('/api/auth/token', { responseType: 'text' });
return this.http.get<AuthToken>('/api/auth/token');
}
}
6 changes: 3 additions & 3 deletions frontend/src/app/services/sounds.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface ApiSound {
category: string;
createdAt: number;
volumeAdjustment?: number;
soundfile?: Readonly<SoundFile>;
soundFile?: Readonly<SoundFile>;
}

export interface SoundFile {
Expand All @@ -28,7 +28,7 @@ export class Sound implements ApiSound {
category: string;
createdAt: number;
volumeAdjustment?: number;
soundfile?: Readonly<SoundFile>;
soundFile?: Readonly<SoundFile>;

constructor(base: ApiSound) {
this.id = base.id;
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<mat-expansion-panel-header>
<mat-panel-title>
{{ sound().name }}{{ soundEntry.hasChanges() ? '*' : '' }}
<mat-icon *ngIf="sound().soundfile == null" matTooltip="No sound file uploaded. Click the upload button below."
<mat-icon *ngIf="sound().soundFile == null" matTooltip="No sound file uploaded. Click the upload button below."
>error_outline</mat-icon
>
</mat-panel-title>
Expand Down Expand Up @@ -53,25 +53,25 @@ <h3>Statistics</h3>
<div>Created</div>
<div timeago [date]="sound().createdAt * 1000" [matTooltip]="sound().createdAt * 1000 | date : 'medium'"></div>
</div>
<ng-container *ngIf="sound().soundfile != null; else noFile">
<ng-container *ngIf="sound().soundFile != null; else noFile">
<div>
<div>Max Volume</div>
<div>{{ sound().soundfile.maxVolume | number : '1.1-1' }} dB</div>
<div>{{ sound().soundFile.maxVolume | number : '1.1-1' }} dB</div>
</div>
<div>
<div>Mean Volume</div>
<div>{{ sound().soundfile.meanVolume | number : '1.1-1' }} dB</div>
<div>{{ sound().soundFile.meanVolume | number : '1.1-1' }} dB</div>
</div>
<div>
<div>Length</div>
<div>{{ sound().soundfile.length | number : '1.3-3' }} s</div>
<div>{{ sound().soundFile.length | number : '1.3-3' }} s</div>
</div>
<div>
<div>Uploaded</div>
<div
timeago
[date]="sound().soundfile.uploadedAt * 1000"
[matTooltip]="sound().soundfile.uploadedAt * 1000 | date : 'medium'"
[date]="sound().soundFile.uploadedAt * 1000"
[matTooltip]="sound().soundFile.uploadedAt * 1000 | date : 'medium'"
></div>
</div>
</ng-container>
Expand All @@ -89,7 +89,7 @@ <h3>Statistics</h3>
</button>
<div class="spinner-button-container">
<button mat-stroked-button [disabled]="isBusy" (click)="!isBusy && fileImport.click()"
><mat-icon>file_upload</mat-icon> {{ sound().soundfile == null ? 'Upload' : 'Replace' }} sound</button
><mat-icon>file_upload</mat-icon> {{ sound().soundFile == null ? 'Upload' : 'Replace' }} sound</button
>
<div class="spinner-container" *ngIf="isBusy">
<mat-spinner diameter="24"></mat-spinner>
Expand Down
Loading

0 comments on commit d3781fb

Please sign in to comment.