Shares #109
82 changed files with 1875 additions and 510 deletions
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
||||||
Copyright (c) 2024 Daniel Ponte.
|
Copyright (c) 2024, 2025 Daniel Ponte.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ protobuf best practices (i.e. not changing field numbers).
|
||||||
|
|
||||||
## License and Copyright
|
## License and Copyright
|
||||||
|
|
||||||
© 2024, Daniel Ponte <dan AT dynatron DOT me>
|
© 2024, 2025 Daniel Ponte <dan AT dynatron DOT me>
|
||||||
|
|
||||||
Licensed under the 3-clause BSD license. See LICENSE for details.
|
Licensed under the 3-clause BSD license. See LICENSE for details.
|
||||||
|
|
||||||
|
|
|
@ -26,5 +26,16 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"navigationUrls": [
|
||||||
|
"/**",
|
||||||
|
"!/**/*.*",
|
||||||
|
"!/**/****",
|
||||||
|
"!/**/****/**",
|
||||||
|
"!/tgstats",
|
||||||
|
"!/tgstats/**",
|
||||||
|
"!/api/**",
|
||||||
|
"!/share/**",
|
||||||
|
"!/testnotify"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="centerNav"></div>
|
<div class="centerNav"></div>
|
||||||
<div class="rightNav">
|
<div class="rightNav">
|
||||||
@if (auth.loggedIn) {
|
@if (auth.isAuth()) {
|
||||||
<button class="ybtn sbButton">
|
<button class="ybtn sbButton">
|
||||||
<a (click)="logout()" class="logout">Logout</a>
|
<a (click)="logout()" class="logout">Logout</a>
|
||||||
</button>
|
</button>
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@if (auth.loggedIn) {
|
@if (auth.isAuth()) {
|
||||||
<app-navigation [events]="toggleNavSubject.asObservable()"></app-navigation>
|
<app-navigation [events]="toggleNavSubject.asObservable()"></app-navigation>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
|
@ -29,7 +29,7 @@ export function authIntercept(
|
||||||
next: HttpHandlerFn,
|
next: HttpHandlerFn,
|
||||||
): Observable<HttpEvent<unknown>> {
|
): Observable<HttpEvent<unknown>> {
|
||||||
let authSvc: AuthService = inject(AuthService);
|
let authSvc: AuthService = inject(AuthService);
|
||||||
if (authSvc.loggedIn) {
|
if (authSvc.isAuth()) {
|
||||||
req = req.clone({
|
req = req.clone({
|
||||||
setHeaders: {
|
setHeaders: {
|
||||||
Authorization: `Bearer ${authSvc.getToken()}`,
|
Authorization: `Bearer ${authSvc.getToken()}`,
|
||||||
|
|
|
@ -8,6 +8,11 @@ export const routes: Routes = [
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./login/login.component').then((m) => m.LoginComponent),
|
import('./login/login.component').then((m) => m.LoginComponent),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 's/:id',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./share/share.component').then((m) => m.ShareComponent),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
canActivateChild: [AuthGuard],
|
canActivateChild: [AuthGuard],
|
||||||
|
@ -71,6 +76,12 @@ export const routes: Routes = [
|
||||||
import('./alerts/alerts.component').then((m) => m.AlertsComponent),
|
import('./alerts/alerts.component').then((m) => m.AlertsComponent),
|
||||||
data: { title: 'Alerts' },
|
data: { title: 'Alerts' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'shares',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./shares/shares.component').then((m) => m.SharesComponent),
|
||||||
|
data: { title: 'Shares' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { inject } from '@angular/core';
|
||||||
export const AuthGuard: CanActivateFn = (route, state) => {
|
export const AuthGuard: CanActivateFn = (route, state) => {
|
||||||
const router: Router = inject(Router);
|
const router: Router = inject(Router);
|
||||||
const authSvc: AuthService = inject(AuthService);
|
const authSvc: AuthService = inject(AuthService);
|
||||||
if (localStorage.getItem('jwt') == null) {
|
if (authSvc.token() === null) {
|
||||||
let success = false;
|
let success = false;
|
||||||
authSvc.refresh().subscribe({
|
authSvc.refresh().subscribe({
|
||||||
next: (event) => {
|
next: (event) => {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
export interface CallRecord {
|
export interface CallRecord {
|
||||||
id: string;
|
id: string;
|
||||||
call_date: Date;
|
callDate: Date;
|
||||||
|
audioURL: string | null;
|
||||||
duration: number;
|
duration: number;
|
||||||
system_id: number;
|
systemId: number;
|
||||||
tgid: number;
|
tgid: number;
|
||||||
incidents: number; // in incident
|
incidents: number; // in incident
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<mat-card class="callInfo" appearance="outlined">
|
||||||
|
<div class="cardHdr">
|
||||||
|
<h1>
|
||||||
|
{{ call | talkgroup: "alpha" | async }} @
|
||||||
|
{{ call.callDate | grabDate }} {{ call.callDate | time: true }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="call-heading">
|
||||||
|
<div class="field field-label">Talkgroup</div>
|
||||||
|
<div class="field field-data">{{ call | talkgroup: "name" | async }}</div>
|
||||||
|
<div class="field field-label">Group</div>
|
||||||
|
<div class="field field-data">{{ call | talkgroup: "group" | async }}</div>
|
||||||
|
</div>
|
||||||
|
<audio controls [src]="call.audioURL! | safe: 'resourceUrl'"></audio>
|
||||||
|
</mat-card>
|
|
@ -0,0 +1,63 @@
|
||||||
|
.callInfo {
|
||||||
|
margin: 0px 50px 50px 50px;
|
||||||
|
padding: 50px 50px 50px 50px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call-heading {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex: 0 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardHdr {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
flex: 1 0;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
font-weight: bolder;
|
||||||
|
flex-grow: 0;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
.field-label::after {
|
||||||
|
content: ":";
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardHdr h1 {
|
||||||
|
flex: 1 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardHdr a {
|
||||||
|
flex: 0 0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
form mat-form-field {
|
||||||
|
width: 60rem;
|
||||||
|
flex: 0 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callRecord {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CallInfoComponent } from './call-info.component';
|
||||||
|
|
||||||
|
describe('CallInfoComponent', () => {
|
||||||
|
let component: CallInfoComponent;
|
||||||
|
let fixture: ComponentFixture<CallInfoComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CallInfoComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CallInfoComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { CallRecord } from '../../calls';
|
||||||
|
import { Share } from '../../shares';
|
||||||
|
import { DatePipe, SafePipe, TalkgroupPipe, TimePipe } from '../calls.service';
|
||||||
|
import { AsyncPipe } from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-call-info',
|
||||||
|
imports: [
|
||||||
|
MatCardModule,
|
||||||
|
TimePipe,
|
||||||
|
DatePipe,
|
||||||
|
TalkgroupPipe,
|
||||||
|
AsyncPipe,
|
||||||
|
SafePipe,
|
||||||
|
],
|
||||||
|
templateUrl: './call-info.component.html',
|
||||||
|
styleUrl: './call-info.component.scss',
|
||||||
|
})
|
||||||
|
export class CallInfoComponent {
|
||||||
|
@Input() share!: Share;
|
||||||
|
call!: CallRecord;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.call = this.share.sharedItem as CallRecord;
|
||||||
|
}
|
||||||
|
}
|
|
@ -135,13 +135,13 @@
|
||||||
<ng-container matColumnDef="date">
|
<ng-container matColumnDef="date">
|
||||||
<th mat-header-cell *matHeaderCellDef>Date</th>
|
<th mat-header-cell *matHeaderCellDef>Date</th>
|
||||||
<td mat-cell *matCellDef="let call">
|
<td mat-cell *matCellDef="let call">
|
||||||
{{ call.call_date | grabDate }}
|
{{ call.callDate | grabDate }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="time">
|
<ng-container matColumnDef="time">
|
||||||
<th mat-header-cell *matHeaderCellDef>Time</th>
|
<th mat-header-cell *matHeaderCellDef>Time</th>
|
||||||
<td [title]="call.call_date" mat-cell *matCellDef="let call">
|
<td [title]="call.callDate" mat-cell *matCellDef="let call">
|
||||||
{{ call.call_date | time }}
|
{{ call.callDate | time }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="system">
|
<ng-container matColumnDef="system">
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
import {
|
import { Component, inject, ViewChild } from '@angular/core';
|
||||||
Component,
|
|
||||||
inject,
|
|
||||||
Pipe,
|
|
||||||
PipeTransform,
|
|
||||||
ViewChild,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { CommonModule, AsyncPipe } from '@angular/common';
|
import { CommonModule, AsyncPipe } from '@angular/common';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
@ -17,13 +11,20 @@ import { PrefsService } from '../prefs/prefs.service';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { SelectionModel } from '@angular/cdk/collections';
|
import { SelectionModel } from '@angular/cdk/collections';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
import { BehaviorSubject, Subject, Subscription } from 'rxjs';
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
import { switchMap } from 'rxjs/operators';
|
||||||
import { CallsListParams, CallsService } from './calls.service';
|
import {
|
||||||
|
CallsListParams,
|
||||||
|
CallsService,
|
||||||
|
DatePipe,
|
||||||
|
DownloadURLPipe,
|
||||||
|
FixedPointPipe,
|
||||||
|
TalkgroupPipe,
|
||||||
|
TimePipe,
|
||||||
|
} from './calls.service';
|
||||||
import { CallRecord } from '../calls';
|
import { CallRecord } from '../calls';
|
||||||
|
|
||||||
import { TalkgroupService } from '../talkgroups/talkgroups.service';
|
import { TalkgroupService } from '../talkgroups/talkgroups.service';
|
||||||
import { Talkgroup } from '../talkgroup';
|
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
|
@ -49,94 +50,6 @@ import {
|
||||||
import { IncidentRecord } from '../incidents';
|
import { IncidentRecord } from '../incidents';
|
||||||
import { SelectIncidentDialogComponent } from '../incidents/select-incident-dialog/select-incident-dialog.component';
|
import { SelectIncidentDialogComponent } from '../incidents/select-incident-dialog/select-incident-dialog.component';
|
||||||
|
|
||||||
@Pipe({
|
|
||||||
name: 'grabDate',
|
|
||||||
standalone: true,
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class DatePipe implements PipeTransform {
|
|
||||||
transform(ts: string, args?: any): string {
|
|
||||||
const timestamp = new Date(ts);
|
|
||||||
return timestamp.getMonth() + 1 + '/' + timestamp.getDate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Pipe({
|
|
||||||
name: 'time',
|
|
||||||
standalone: true,
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class TimePipe implements PipeTransform {
|
|
||||||
transform(ts: string, args?: any): string {
|
|
||||||
const timestamp = new Date(ts);
|
|
||||||
return timestamp.toLocaleTimeString(navigator.language, {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hourCycle: 'h23',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Pipe({
|
|
||||||
name: 'talkgroup',
|
|
||||||
standalone: true,
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class TalkgroupPipe implements PipeTransform {
|
|
||||||
constructor(private tgService: TalkgroupService) {}
|
|
||||||
|
|
||||||
transform(call: CallRecord, field: string): Observable<string> {
|
|
||||||
return this.tgService.getTalkgroup(call.system_id, call.tgid).pipe(
|
|
||||||
map((tg: Talkgroup) => {
|
|
||||||
switch (field) {
|
|
||||||
case 'alpha': {
|
|
||||||
return tg.alpha_tag ?? call.tgid;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'group': {
|
|
||||||
return tg.tg_group ?? '\u2014';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'system': {
|
|
||||||
return tg.system?.name ?? tg.system_id.toString();
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return tg.name ?? '\u2014';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Pipe({
|
|
||||||
name: 'fixedPoint',
|
|
||||||
standalone: true,
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class FixedPointPipe implements PipeTransform {
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
transform(quant: number, divisor: number, places: number): string {
|
|
||||||
const seconds = quant / divisor;
|
|
||||||
return seconds.toFixed(places);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Pipe({
|
|
||||||
name: 'audioDownloadURL',
|
|
||||||
standalone: true,
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class DownloadURLPipe implements PipeTransform {
|
|
||||||
constructor(private callsSvc: CallsService) {}
|
|
||||||
|
|
||||||
transform(call: CallRecord, args?: any): string {
|
|
||||||
return this.callsSvc.callAudioDownloadURL(call.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reqPageSize = 200;
|
const reqPageSize = 200;
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-calls',
|
selector: 'app-calls',
|
||||||
|
@ -144,8 +57,8 @@ const reqPageSize = 200;
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
FixedPointPipe,
|
FixedPointPipe,
|
||||||
TalkgroupPipe,
|
TalkgroupPipe,
|
||||||
DatePipe,
|
|
||||||
TimePipe,
|
TimePipe,
|
||||||
|
DatePipe,
|
||||||
MatPaginatorModule,
|
MatPaginatorModule,
|
||||||
MatTableModule,
|
MatTableModule,
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
|
@ -329,6 +242,7 @@ export class CallsComponent {
|
||||||
this.subscriptions.add(
|
this.subscriptions.add(
|
||||||
this.fetchCalls
|
this.fetchCalls
|
||||||
.pipe(
|
.pipe(
|
||||||
|
debounceTime(500),
|
||||||
switchMap((params) => {
|
switchMap((params) => {
|
||||||
return this.callsSvc.getCalls(params);
|
return this.callsSvc.getCalls(params);
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,8 +1,155 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable, Pipe, PipeTransform } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { map, Observable } from 'rxjs';
|
||||||
import { CallRecord } from '../calls';
|
import { CallRecord } from '../calls';
|
||||||
import { environment } from '.././../environments/environment';
|
import { environment } from '.././../environments/environment';
|
||||||
|
import { TalkgroupService } from '../talkgroups/talkgroups.service';
|
||||||
|
import { Talkgroup } from '../talkgroup';
|
||||||
|
import { Share } from '../shares';
|
||||||
|
import {
|
||||||
|
DomSanitizer,
|
||||||
|
SafeHtml,
|
||||||
|
SafeResourceUrl,
|
||||||
|
SafeScript,
|
||||||
|
SafeStyle,
|
||||||
|
SafeUrl,
|
||||||
|
} from '@angular/platform-browser';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'grabDate',
|
||||||
|
standalone: true,
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class DatePipe implements PipeTransform {
|
||||||
|
transform(ts: string | Date, args?: any): string {
|
||||||
|
const timestamp = new Date(ts);
|
||||||
|
return timestamp.getMonth() + 1 + '/' + timestamp.getDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'time',
|
||||||
|
standalone: true,
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class TimePipe implements PipeTransform {
|
||||||
|
transform(ts: string | Date, haveSecond: boolean = false): string {
|
||||||
|
const timestamp = new Date(ts);
|
||||||
|
return timestamp.toLocaleTimeString(navigator.language, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: haveSecond ? '2-digit' : undefined,
|
||||||
|
hourCycle: 'h23',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'talkgroup',
|
||||||
|
standalone: true,
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class TalkgroupPipe implements PipeTransform {
|
||||||
|
constructor(private tgService: TalkgroupService) {}
|
||||||
|
|
||||||
|
transform(
|
||||||
|
call: CallRecord,
|
||||||
|
field: string,
|
||||||
|
share: Share | null = null,
|
||||||
|
): Observable<string> {
|
||||||
|
return this.tgService.getTalkgroup(call.systemId, call.tgid).pipe(
|
||||||
|
map((tg: Talkgroup) => {
|
||||||
|
switch (field) {
|
||||||
|
case 'alpha': {
|
||||||
|
return tg.alphaTag ?? call.tgid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'group': {
|
||||||
|
return tg.tgGroup ?? '\u2014';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'system': {
|
||||||
|
return tg.system?.name ?? tg.systemId.toString();
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return tg.name ?? '\u2014';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'fixedPoint',
|
||||||
|
standalone: true,
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class FixedPointPipe implements PipeTransform {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
transform(quant: number, divisor: number, places: number): string {
|
||||||
|
const seconds = quant / divisor;
|
||||||
|
return seconds.toFixed(places);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize HTML
|
||||||
|
*/
|
||||||
|
@Pipe({
|
||||||
|
name: 'safe',
|
||||||
|
})
|
||||||
|
export class SafePipe implements PipeTransform {
|
||||||
|
/**
|
||||||
|
* Pipe Constructor
|
||||||
|
*
|
||||||
|
* @param _sanitizer: DomSanitezer
|
||||||
|
*/
|
||||||
|
// tslint:disable-next-line
|
||||||
|
constructor(protected _sanitizer: DomSanitizer) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform
|
||||||
|
*
|
||||||
|
* @param value: string
|
||||||
|
* @param type: string
|
||||||
|
*/
|
||||||
|
transform(
|
||||||
|
value: string,
|
||||||
|
type: string,
|
||||||
|
): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl {
|
||||||
|
switch (type) {
|
||||||
|
case 'html':
|
||||||
|
return this._sanitizer.bypassSecurityTrustHtml(value);
|
||||||
|
case 'style':
|
||||||
|
return this._sanitizer.bypassSecurityTrustStyle(value);
|
||||||
|
case 'script':
|
||||||
|
return this._sanitizer.bypassSecurityTrustScript(value);
|
||||||
|
case 'url':
|
||||||
|
return this._sanitizer.bypassSecurityTrustUrl(value);
|
||||||
|
case 'resourceUrl':
|
||||||
|
let res = this._sanitizer.bypassSecurityTrustResourceUrl(value);
|
||||||
|
return res;
|
||||||
|
default:
|
||||||
|
return this._sanitizer.bypassSecurityTrustHtml(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'audioDownloadURL',
|
||||||
|
standalone: true,
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class DownloadURLPipe implements PipeTransform {
|
||||||
|
constructor(private callsSvc: CallsService) {}
|
||||||
|
|
||||||
|
transform(call: CallRecord, args?: any): string {
|
||||||
|
return this.callsSvc.callAudioDownloadURL(call.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface CallsListParams {
|
export interface CallsListParams {
|
||||||
start: Date | null;
|
start: Date | null;
|
||||||
|
|
|
@ -30,7 +30,11 @@ export class CallPlayerComponent {
|
||||||
this.playSub.unsubscribe();
|
this.playSub.unsubscribe();
|
||||||
});
|
});
|
||||||
this.playing = true;
|
this.playing = true;
|
||||||
|
if (this.call.audioURL != null) {
|
||||||
|
this.au.src = this.call.audioURL;
|
||||||
|
} else {
|
||||||
this.au.src = this.callsSvc.callAudioURL(this.call.id);
|
this.au.src = this.callsSvc.callAudioURL(this.call.id);
|
||||||
|
}
|
||||||
this.au.load();
|
this.au.load();
|
||||||
this.au.play().then(null, (reason) => {
|
this.au.play().then(null, (reason) => {
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
|
|
|
@ -3,10 +3,16 @@
|
||||||
<div class="cardHdr">
|
<div class="cardHdr">
|
||||||
<h1>
|
<h1>
|
||||||
{{ inc?.name }}
|
{{ inc?.name }}
|
||||||
<a [href]="'/api/incident/' + incID + '.m3u'"
|
<a
|
||||||
|
[href]="
|
||||||
|
share
|
||||||
|
? '/share/' + share.id + '/m3u'
|
||||||
|
: '/api/incident/' + incID + '.m3u'
|
||||||
|
"
|
||||||
><mat-icon>playlist_play</mat-icon></a
|
><mat-icon>playlist_play</mat-icon></a
|
||||||
>
|
>
|
||||||
</h1>
|
</h1>
|
||||||
|
@if (share == null) {
|
||||||
<button mat-icon-button (click)="editIncident(incID)">
|
<button mat-icon-button (click)="editIncident(incID)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
@ -14,10 +20,15 @@
|
||||||
<mat-icon>more_vert</mat-icon>
|
<mat-icon>more_vert</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #moreMenu="matMenu">
|
<mat-menu #moreMenu="matMenu">
|
||||||
<button class="deleteItem" mat-menu-item (click)="deleteIncident(incID)">
|
<button
|
||||||
|
class="deleteItem"
|
||||||
|
mat-menu-item
|
||||||
|
(click)="deleteIncident(incID)"
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="inc-heading">
|
<div class="inc-heading">
|
||||||
<div class="field field-start field-label">Start</div>
|
<div class="field field-start field-label">Start</div>
|
||||||
|
@ -64,25 +75,25 @@
|
||||||
<ng-container matColumnDef="date">
|
<ng-container matColumnDef="date">
|
||||||
<th mat-header-cell *matHeaderCellDef>Date</th>
|
<th mat-header-cell *matHeaderCellDef>Date</th>
|
||||||
<td mat-cell *matCellDef="let call">
|
<td mat-cell *matCellDef="let call">
|
||||||
{{ call.call_date | grabDate }}
|
{{ call.callDate | grabDate }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="time">
|
<ng-container matColumnDef="time">
|
||||||
<th mat-header-cell *matHeaderCellDef>Time</th>
|
<th mat-header-cell *matHeaderCellDef>Time</th>
|
||||||
<td [title]="call.call_date" mat-cell *matCellDef="let call">
|
<td [title]="call.callDate" mat-cell *matCellDef="let call">
|
||||||
{{ call.call_date | time }}
|
{{ call.callDate | time }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="system">
|
<ng-container matColumnDef="system">
|
||||||
<th mat-header-cell *matHeaderCellDef>System</th>
|
<th mat-header-cell *matHeaderCellDef>System</th>
|
||||||
<td mat-cell *matCellDef="let call">
|
<td mat-cell *matCellDef="let call">
|
||||||
{{ call | talkgroup: "system" | async }}
|
{{ call | talkgroup: "system" : share | async }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="group">
|
<ng-container matColumnDef="group">
|
||||||
<th mat-header-cell *matHeaderCellDef>Group</th>
|
<th mat-header-cell *matHeaderCellDef>Group</th>
|
||||||
<td mat-cell *matCellDef="let call">
|
<td mat-cell *matCellDef="let call">
|
||||||
{{ call | talkgroup: "group" | async }}
|
{{ call | talkgroup: "group" : share | async }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="talkgroup">
|
<ng-container matColumnDef="talkgroup">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.incident {
|
.incident {
|
||||||
margin: 50px 50px 50px 50px;
|
margin: 0px 50px 50px 50px;
|
||||||
padding: 50px 50px 50px 50px;
|
padding: 50px 50px 50px 50px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
|
@ -7,12 +7,6 @@
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media not screen and (max-width: 768px) {
|
|
||||||
.incident {
|
|
||||||
width: 75%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.inc-heading {
|
.inc-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject, Input } from '@angular/core';
|
||||||
import { tap } from 'rxjs/operators';
|
import { tap } from 'rxjs/operators';
|
||||||
import { CommonModule, Location } from '@angular/common';
|
import { CommonModule, Location } from '@angular/common';
|
||||||
import { BehaviorSubject, merge, Subscription } from 'rxjs';
|
import { BehaviorSubject, merge, Subject, Subscription } from 'rxjs';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
@ -35,10 +35,13 @@ import {
|
||||||
TimePipe,
|
TimePipe,
|
||||||
DatePipe,
|
DatePipe,
|
||||||
DownloadURLPipe,
|
DownloadURLPipe,
|
||||||
} from '../../calls/calls.component';
|
} from '../../calls/calls.service';
|
||||||
import { CallPlayerComponent } from '../../calls/player/call-player/call-player.component';
|
import { CallPlayerComponent } from '../../calls/player/call-player/call-player.component';
|
||||||
import { FmtDatePipe } from '../incidents.component';
|
import { FmtDatePipe } from '../incidents.component';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { Share } from '../../shares';
|
||||||
|
import { ShareService } from '../../share/share.service';
|
||||||
|
import { TalkgroupService } from '../../talkgroups/talkgroups.service';
|
||||||
|
|
||||||
export interface EditDialogData {
|
export interface EditDialogData {
|
||||||
incID: string;
|
incID: string;
|
||||||
|
@ -151,8 +154,9 @@ export class IncidentEditDialogComponent {
|
||||||
styleUrl: './incident.component.scss',
|
styleUrl: './incident.component.scss',
|
||||||
})
|
})
|
||||||
export class IncidentComponent {
|
export class IncidentComponent {
|
||||||
incPrime = new BehaviorSubject<IncidentRecord>(<IncidentRecord>{});
|
incPrime = new Subject<IncidentRecord>();
|
||||||
inc$!: Observable<IncidentRecord>;
|
inc$!: Observable<IncidentRecord>;
|
||||||
|
@Input() share?: Share;
|
||||||
subscriptions: Subscription = new Subscription();
|
subscriptions: Subscription = new Subscription();
|
||||||
dialog = inject(MatDialog);
|
dialog = inject(MatDialog);
|
||||||
incID!: string;
|
incID!: string;
|
||||||
|
@ -174,15 +178,30 @@ export class IncidentComponent {
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private incSvc: IncidentsService,
|
private incSvc: IncidentsService,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
|
private tgSvc: TalkgroupService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
saveIncName(ev: Event) {}
|
saveIncName(ev: Event) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
if (this.share) {
|
||||||
|
this.tgSvc.setShare(this.share);
|
||||||
|
}
|
||||||
|
let incOb: Observable<IncidentRecord>;
|
||||||
|
if (this.route.component === this.constructor) {
|
||||||
|
// loaded by route
|
||||||
this.incID = this.route.snapshot.paramMap.get('id')!;
|
this.incID = this.route.snapshot.paramMap.get('id')!;
|
||||||
this.inc$ = merge(this.incSvc.getIncident(this.incID), this.incPrime).pipe(
|
incOb = this.incSvc.getIncident(this.incID);
|
||||||
|
} else {
|
||||||
|
if (!this.share) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.incID = (this.share.sharedItem as IncidentRecord).id;
|
||||||
|
incOb = new BehaviorSubject(this.share.sharedItem as IncidentRecord);
|
||||||
|
}
|
||||||
|
this.inc$ = merge(incOb, this.incPrime).pipe(
|
||||||
tap((inc) => {
|
tap((inc) => {
|
||||||
if (inc.calls) {
|
if (inc && inc.calls) {
|
||||||
this.callsResult.data = inc.calls;
|
this.callsResult.data = inc.calls;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,56 +1,95 @@
|
||||||
import { Injectable } from '@angular/core';
|
import {
|
||||||
|
Injectable,
|
||||||
|
signal,
|
||||||
|
computed,
|
||||||
|
effect,
|
||||||
|
inject,
|
||||||
|
DestroyRef,
|
||||||
|
} from '@angular/core';
|
||||||
import { HttpClient, HttpResponse } from '@angular/common/http';
|
import { HttpClient, HttpResponse } from '@angular/common/http';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import { tap } from 'rxjs/operators';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
export class Jwt {
|
export class Jwt {
|
||||||
constructor(public jwt: string) {}
|
constructor(public jwt: string) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthState = {
|
||||||
|
user: string | null;
|
||||||
|
token: string | null;
|
||||||
|
is_auth: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
loggedIn: boolean = false;
|
private _accessTokenKey = 'jwt';
|
||||||
|
private _storedToken = localStorage.getItem(this._accessTokenKey);
|
||||||
|
destroyed = inject(DestroyRef);
|
||||||
|
|
||||||
|
private _state = signal<AuthState>({
|
||||||
|
user: null,
|
||||||
|
token: this._storedToken,
|
||||||
|
is_auth: this._storedToken !== null,
|
||||||
|
});
|
||||||
|
loginFailed = signal<boolean>(false);
|
||||||
|
token = computed(() => this._state().token);
|
||||||
|
isAuth = computed(() => this._state().is_auth);
|
||||||
|
user = computed(() => this._state().user);
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private _router: Router,
|
private _router: Router,
|
||||||
) {
|
) {
|
||||||
let ssJWT = localStorage.getItem('jwt');
|
effect(() => {
|
||||||
if (ssJWT) {
|
const token = this.token();
|
||||||
this.loggedIn = true;
|
if (token !== null) {
|
||||||
|
localStorage.setItem(this._accessTokenKey, token);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(this._accessTokenKey);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
login(username: string, password: string): Observable<HttpResponse<Jwt>> {
|
login(username: string, password: string) {
|
||||||
return this.http
|
return this.http
|
||||||
.post<Jwt>(
|
.post<Jwt>('/api/login', { username: username, password: password })
|
||||||
'/api/login',
|
.pipe(takeUntilDestroyed(this.destroyed))
|
||||||
{ username: username, password: password },
|
.subscribe({
|
||||||
{ observe: 'response' },
|
next: (res) => {
|
||||||
)
|
let state = <AuthState>{
|
||||||
.pipe(
|
user: username,
|
||||||
tap((event) => {
|
token: res.jwt,
|
||||||
if (event.status == 200) {
|
is_auth: true,
|
||||||
localStorage.setItem('jwt', event.body?.jwt.toString() ?? '');
|
};
|
||||||
this.loggedIn = true;
|
this._state.set(state);
|
||||||
this._router.navigateByUrl('/home');
|
this.loginFailed.update(() => false);
|
||||||
|
this._router.navigateByUrl('/');
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.loginFailed.update(() => true);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
);
|
_clearState() {
|
||||||
|
this._state.set(<AuthState>{
|
||||||
|
is_auth: false,
|
||||||
|
token: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
this.http
|
this.http.get('/api/logout', { withCredentials: true }).subscribe({
|
||||||
.get('/api/logout', { withCredentials: true, observe: 'response' })
|
next: (event) => {
|
||||||
.subscribe((event) => {
|
this._clearState();
|
||||||
if (event.status == 200) {
|
},
|
||||||
this.loggedIn = false;
|
error: (err) => {
|
||||||
}
|
this._clearState();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
localStorage.removeItem('jwt');
|
|
||||||
this.loggedIn = false;
|
|
||||||
this._router.navigateByUrl('/login');
|
this._router.navigateByUrl('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,14 +99,20 @@ export class AuthService {
|
||||||
.pipe(
|
.pipe(
|
||||||
tap((event) => {
|
tap((event) => {
|
||||||
if (event.status == 200) {
|
if (event.status == 200) {
|
||||||
localStorage.setItem('jwt', event.body?.jwt.toString() ?? '');
|
let ost = this._state();
|
||||||
this.loggedIn = true;
|
let tok = event.body?.jwt.toString();
|
||||||
|
let state = <AuthState>{
|
||||||
|
user: ost.user,
|
||||||
|
token: tok ? tok : null,
|
||||||
|
is_auth: true,
|
||||||
|
};
|
||||||
|
this._state.set(state);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken(): string | null {
|
getToken(): string | null {
|
||||||
return localStorage.getItem('jwt');
|
return localStorage.getItem(this._accessTokenKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
<button class="login sbButton" (click)="onSubmit()">Login</button>
|
<button class="login sbButton" (click)="onSubmit()">Login</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@if (failed) {
|
@if (failed()) {
|
||||||
<div role="alert">
|
<div role="alert">
|
||||||
<span>Login Failed!</span>
|
<span>Login Failed!</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject, signal } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { AuthService } from '../login/auth.service';
|
import { AuthService } from '../login/auth.service';
|
||||||
import { catchError, of, Subscription } from 'rxjs';
|
import { catchError, of, Subscription } from 'rxjs';
|
||||||
|
@ -17,31 +17,9 @@ export class LoginComponent {
|
||||||
router: Router = inject(Router);
|
router: Router = inject(Router);
|
||||||
username: string = '';
|
username: string = '';
|
||||||
password: string = '';
|
password: string = '';
|
||||||
failed: boolean = false;
|
failed = this.apiService.loginFailed;
|
||||||
private subscriptions = new Subscription();
|
|
||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.failed = false;
|
this.apiService.login(this.username, this.password);
|
||||||
this.subscriptions.add(
|
|
||||||
this.apiService
|
|
||||||
.login(this.username, this.password)
|
|
||||||
.pipe(
|
|
||||||
catchError(() => {
|
|
||||||
this.failed = true;
|
|
||||||
return of(null);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.subscribe((event) => {
|
|
||||||
if (event?.status == 200) {
|
|
||||||
this.router.navigateByUrl('/');
|
|
||||||
} else {
|
|
||||||
this.failed = true;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.subscriptions.unsubscribe();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,6 +101,11 @@ export class NavigationComponent {
|
||||||
url: '/alerts',
|
url: '/alerts',
|
||||||
icon: 'notifications',
|
icon: 'notifications',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Shares',
|
||||||
|
url: '/shares',
|
||||||
|
icon: 'share',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
toggleFilterPanel() {
|
toggleFilterPanel() {
|
||||||
|
|
17
client/stillbox/src/app/share/share.component.html
Normal file
17
client/stillbox/src/app/share/share.component.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
@let sh = share | async;
|
||||||
|
@switch (sh?.type) {
|
||||||
|
@case ("incident") {
|
||||||
|
<app-incident [share]="sh!"></app-incident>
|
||||||
|
}
|
||||||
|
@case ("call") {
|
||||||
|
<app-call-info [share]="sh!" player="true"></app-call-info>
|
||||||
|
}
|
||||||
|
@case (null) {
|
||||||
|
<div class="spinner">
|
||||||
|
<mat-spinner></mat-spinner>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@default {
|
||||||
|
<h1 class="error">Share type {{ sh?.type }} unknown</h1>
|
||||||
|
}
|
||||||
|
}
|
0
client/stillbox/src/app/share/share.component.scss
Normal file
0
client/stillbox/src/app/share/share.component.scss
Normal file
22
client/stillbox/src/app/share/share.component.spec.ts
Normal file
22
client/stillbox/src/app/share/share.component.spec.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ShareComponent } from './share.component';
|
||||||
|
|
||||||
|
describe('ShareComponent', () => {
|
||||||
|
let component: ShareComponent;
|
||||||
|
let fixture: ComponentFixture<ShareComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ShareComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ShareComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
41
client/stillbox/src/app/share/share.component.ts
Normal file
41
client/stillbox/src/app/share/share.component.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ShareService } from './share.service';
|
||||||
|
import { Share } from '../shares';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { Observable, Subscription, switchMap } from 'rxjs';
|
||||||
|
import { IncidentComponent } from '../incidents/incident/incident.component';
|
||||||
|
import { AsyncPipe } from '@angular/common';
|
||||||
|
import { CallInfoComponent } from '../calls/call-info/call-info.component';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-share',
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
IncidentComponent,
|
||||||
|
CallInfoComponent,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
],
|
||||||
|
templateUrl: './share.component.html',
|
||||||
|
styleUrl: './share.component.scss',
|
||||||
|
})
|
||||||
|
export class ShareComponent {
|
||||||
|
shareID!: string;
|
||||||
|
share!: Observable<Share>;
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private shareSvc: ShareService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
let shareParam = this.route.snapshot.paramMap.get('id');
|
||||||
|
if (shareParam == null) {
|
||||||
|
// TODO: error
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shareID = shareParam;
|
||||||
|
|
||||||
|
this.share = this.shareSvc.getShare(this.shareID);
|
||||||
|
}
|
||||||
|
}
|
16
client/stillbox/src/app/share/share.service.spec.ts
Normal file
16
client/stillbox/src/app/share/share.service.spec.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ShareService } from './share.service';
|
||||||
|
|
||||||
|
describe('ShareService', () => {
|
||||||
|
let service: ShareService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ShareService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
81
client/stillbox/src/app/share/share.service.ts
Normal file
81
client/stillbox/src/app/share/share.service.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { map, Observable, switchMap } from 'rxjs';
|
||||||
|
import { IncidentRecord } from '../incidents';
|
||||||
|
import { CallRecord } from '../calls';
|
||||||
|
import { Share, ShareType } from '../shares';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
|
export interface ShareRecord {
|
||||||
|
id: string;
|
||||||
|
entityType: string;
|
||||||
|
entityDate: Date;
|
||||||
|
owner: string;
|
||||||
|
entityID: string;
|
||||||
|
expiration: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShareListParams {
|
||||||
|
page: number | null;
|
||||||
|
perPage: number | null;
|
||||||
|
dir: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Shares {
|
||||||
|
shares: ShareRecord[];
|
||||||
|
totalCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ShareService {
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
inShare(): string | null {
|
||||||
|
if (this.router.url.startsWith('/s/')) {
|
||||||
|
return this.route.snapshot.paramMap.get('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getShare(id: string): Observable<Share> {
|
||||||
|
return this.http.get<Share>(`/share/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteShare(id: string): Observable<void> {
|
||||||
|
return this.http.delete<void>(`/api/share/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getShares(p: ShareListParams): Observable<Shares> {
|
||||||
|
return this.http.post<Shares>('/api/share/', p);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSharedItem(s: Observable<Share>): Observable<ShareType> {
|
||||||
|
return s.pipe(
|
||||||
|
map((res) => {
|
||||||
|
switch (res.type) {
|
||||||
|
case 'call':
|
||||||
|
return <CallRecord>res.sharedItem;
|
||||||
|
case 'incident':
|
||||||
|
return <IncidentRecord>res.sharedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCallAudio(s: Observable<CallRecord>): Observable<ArrayBuffer> {
|
||||||
|
return s.pipe(
|
||||||
|
switchMap((res) => {
|
||||||
|
return this.http.get<ArrayBuffer>(res.audioURL!);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
8
client/stillbox/src/app/shares.ts
Normal file
8
client/stillbox/src/app/shares.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { IncidentRecord } from './incidents';
|
||||||
|
import { CallRecord } from './calls';
|
||||||
|
export type ShareType = IncidentRecord | CallRecord | null;
|
||||||
|
export interface Share {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
sharedItem: ShareType;
|
||||||
|
}
|
82
client/stillbox/src/app/shares/shares.component.html
Normal file
82
client/stillbox/src/app/shares/shares.component.html
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<div class="tabContainer" *ngIf="!isLoading; else spinner">
|
||||||
|
<table class="sharesTable" mat-table [dataSource]="sharesResult">
|
||||||
|
<ng-container matColumnDef="select">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>
|
||||||
|
<mat-checkbox
|
||||||
|
(change)="$event ? masterToggle() : null"
|
||||||
|
[checked]="selection.hasValue() && isAllSelected()"
|
||||||
|
[indeterminate]="selection.hasValue() && !isAllSelected()"
|
||||||
|
>
|
||||||
|
</mat-checkbox>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let row">
|
||||||
|
<mat-checkbox
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
(change)="$event ? selection.toggle(row) : null"
|
||||||
|
[checked]="selection.isSelected(row)"
|
||||||
|
>
|
||||||
|
</mat-checkbox>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="type">
|
||||||
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
|
<td mat-cell *matCellDef="let share">
|
||||||
|
@switch (share.entityType) {
|
||||||
|
@case ("incident") {
|
||||||
|
<mat-icon>newspaper</mat-icon>
|
||||||
|
}
|
||||||
|
@case ("call") {
|
||||||
|
<mat-icon>campaign</mat-icon>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="link">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Link</th>
|
||||||
|
<td mat-cell *matCellDef="let share">
|
||||||
|
<a [href]="'/s/' + share.id"><mat-icon>link</mat-icon></a>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="date">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Date</th>
|
||||||
|
<td mat-cell *matCellDef="let share">
|
||||||
|
{{ share.entityDate | fmtDate }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="owner">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Owner</th>
|
||||||
|
<td mat-cell *matCellDef="let share">
|
||||||
|
{{ share.owner }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="delete">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Delete</th>
|
||||||
|
<td mat-cell *matCellDef="let share">
|
||||||
|
<a (click)="deleteShare(share.id)">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr>
|
||||||
|
<tr mat-row *matRowDef="let myRowData; columns: columns"></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="pagFoot">
|
||||||
|
<mat-paginator
|
||||||
|
#paginator
|
||||||
|
class="paginator"
|
||||||
|
(page)="setPage($event)"
|
||||||
|
[length]="count"
|
||||||
|
showFirstLastButtons="true"
|
||||||
|
[pageSize]="curPage.pageSize"
|
||||||
|
[pageSizeOptions]="pageSizeOptions"
|
||||||
|
[pageIndex]="curPage.pageIndex"
|
||||||
|
aria-label="Select page"
|
||||||
|
>
|
||||||
|
</mat-paginator>
|
||||||
|
</div>
|
||||||
|
<ng-template #spinner>
|
||||||
|
<div class="spinner">
|
||||||
|
<mat-spinner></mat-spinner>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
0
client/stillbox/src/app/shares/shares.component.scss
Normal file
0
client/stillbox/src/app/shares/shares.component.scss
Normal file
22
client/stillbox/src/app/shares/shares.component.spec.ts
Normal file
22
client/stillbox/src/app/shares/shares.component.spec.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SharesComponent } from './shares.component';
|
||||||
|
|
||||||
|
describe('SharesComponent', () => {
|
||||||
|
let component: SharesComponent;
|
||||||
|
let fixture: ComponentFixture<SharesComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SharesComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SharesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
192
client/stillbox/src/app/shares/shares.component.ts
Normal file
192
client/stillbox/src/app/shares/shares.component.ts
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
import { Component, Pipe, PipeTransform, ViewChild } from '@angular/core';
|
||||||
|
import { CommonModule, AsyncPipe } from '@angular/common';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import {
|
||||||
|
MatPaginator,
|
||||||
|
MatPaginatorModule,
|
||||||
|
PageEvent,
|
||||||
|
} from '@angular/material/paginator';
|
||||||
|
import { PrefsService } from '../prefs/prefs.service';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { SelectionModel } from '@angular/cdk/collections';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
|
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||||
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { TalkgroupService } from '../talkgroups/talkgroups.service';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormGroup,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { debounceTime } from 'rxjs/operators';
|
||||||
|
import { ToolbarContextService } from '../navigation/toolbar-context.service';
|
||||||
|
import {
|
||||||
|
ShareListParams,
|
||||||
|
ShareRecord,
|
||||||
|
ShareService,
|
||||||
|
} from '../share/share.service';
|
||||||
|
import { FmtDatePipe } from '../incidents/incidents.component';
|
||||||
|
|
||||||
|
const reqPageSize = 200;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-shares',
|
||||||
|
imports: [
|
||||||
|
MatIconModule,
|
||||||
|
MatPaginatorModule,
|
||||||
|
MatTableModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormsModule,
|
||||||
|
FmtDatePipe,
|
||||||
|
MatInputModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
CommonModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
],
|
||||||
|
templateUrl: './shares.component.html',
|
||||||
|
styleUrl: './shares.component.scss',
|
||||||
|
})
|
||||||
|
export class SharesComponent {
|
||||||
|
sharesResult = new BehaviorSubject(new Array<ShareRecord>(0));
|
||||||
|
selection = new SelectionModel<ShareRecord>(true, []);
|
||||||
|
|
||||||
|
@ViewChild('paginator') paginator!: MatPaginator;
|
||||||
|
count = 0;
|
||||||
|
curLen = 0;
|
||||||
|
page = 0;
|
||||||
|
perPage = 25;
|
||||||
|
pageSizeOptions = [25, 50, 75, 100, 200];
|
||||||
|
columns = ['select', 'type', 'link', 'date', 'owner', 'delete'];
|
||||||
|
curPage = <PageEvent>{ pageIndex: 0, pageSize: 0 };
|
||||||
|
currentSet!: ShareRecord[];
|
||||||
|
currentServerPage = 0; // page is never 0, forces load
|
||||||
|
isLoading = true;
|
||||||
|
subscriptions = new Subscription();
|
||||||
|
pageWindow = 0;
|
||||||
|
fetchIncidents = new BehaviorSubject<ShareListParams>(
|
||||||
|
this.buildParams(this.curPage, this.curPage.pageIndex),
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(private sharesSvc: ShareService) {}
|
||||||
|
|
||||||
|
isAllSelected() {
|
||||||
|
const numSelected = this.selection.selected.length;
|
||||||
|
const numRows = this.curLen;
|
||||||
|
return numSelected === numRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildParams(p: PageEvent, serverPage: number): ShareListParams {
|
||||||
|
const par: ShareListParams = {
|
||||||
|
page: serverPage,
|
||||||
|
perPage: reqPageSize,
|
||||||
|
dir: 'asc',
|
||||||
|
};
|
||||||
|
|
||||||
|
return par;
|
||||||
|
}
|
||||||
|
|
||||||
|
masterToggle() {
|
||||||
|
this.isAllSelected()
|
||||||
|
? this.selection.clear()
|
||||||
|
: this.sharesResult.value.forEach((row) => this.selection.select(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
setPage(p: PageEvent, force?: boolean) {
|
||||||
|
this.selection.clear();
|
||||||
|
this.curPage = p;
|
||||||
|
if (p && p!.pageSize != this.perPage) {
|
||||||
|
this.perPage = p!.pageSize;
|
||||||
|
}
|
||||||
|
this.getShares(p, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.selection.clear();
|
||||||
|
this.getShares(this.curPage, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
getShares(p: PageEvent, force?: boolean) {
|
||||||
|
const pageStart = p.pageIndex * p.pageSize;
|
||||||
|
const serverPage = Math.floor(pageStart / reqPageSize) + 1;
|
||||||
|
this.pageWindow = pageStart % reqPageSize;
|
||||||
|
if (serverPage == this.currentServerPage && !force && this.currentSet) {
|
||||||
|
this.sharesResult.next(
|
||||||
|
this.sharesResult
|
||||||
|
? this.currentSet.slice(this.pageWindow, this.pageWindow + p.pageSize)
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.currentServerPage = serverPage;
|
||||||
|
this.fetchIncidents.next(this.buildParams(p, serverPage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zeroPage(): PageEvent {
|
||||||
|
return <PageEvent>{
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: this.curPage.pageSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.subscriptions.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
let cpp = 25;
|
||||||
|
this.perPage = cpp;
|
||||||
|
|
||||||
|
this.setPage(<PageEvent>{
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: cpp,
|
||||||
|
});
|
||||||
|
this.subscriptions.add(
|
||||||
|
this.fetchIncidents
|
||||||
|
.pipe(
|
||||||
|
switchMap((params) => {
|
||||||
|
return this.sharesSvc.getShares(params);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe((shares) => {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.count = shares.totalCount;
|
||||||
|
this.currentSet = shares.shares;
|
||||||
|
this.sharesResult.next(
|
||||||
|
this.currentSet
|
||||||
|
? this.currentSet.slice(
|
||||||
|
this.pageWindow,
|
||||||
|
this.pageWindow + this.perPage,
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.subscriptions.add(
|
||||||
|
this.sharesResult.subscribe((cr) => {
|
||||||
|
this.curLen = cr.length;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteShare(shareID: string) {
|
||||||
|
if (confirm('Are you sure you want to delete this share?')) {
|
||||||
|
this.sharesSvc.deleteShare(shareID).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.fetchIncidents.next(
|
||||||
|
this.buildParams(this.curPage, this.curPage.pageIndex),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
alert(err);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,43 +58,39 @@ export const iconMapping: IconMap = {
|
||||||
|
|
||||||
export class Talkgroup {
|
export class Talkgroup {
|
||||||
id!: number;
|
id!: number;
|
||||||
system_id!: number;
|
systemId!: number;
|
||||||
tgid!: number;
|
tgid!: number;
|
||||||
name!: string;
|
name!: string;
|
||||||
alpha_tag!: string;
|
alphaTag!: string;
|
||||||
tg_group!: string;
|
tgGroup!: string;
|
||||||
frequency!: number;
|
frequency!: number;
|
||||||
metadata!: Metadata | null;
|
metadata!: Metadata | null;
|
||||||
tags!: string[];
|
tags!: string[];
|
||||||
alert!: boolean;
|
alert!: boolean;
|
||||||
system?: System;
|
system?: System;
|
||||||
alert_rules!: AlertRule[];
|
alertRules!: AlertRule[];
|
||||||
weight!: number;
|
weight!: number;
|
||||||
learned?: boolean;
|
learned?: boolean;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
iconSvg?: string;
|
iconSvg?: string;
|
||||||
constructor(
|
constructor(
|
||||||
id: number,
|
id: number,
|
||||||
system_id: number,
|
systemId: number,
|
||||||
tgid: number,
|
tgid: number,
|
||||||
name: string,
|
name: string,
|
||||||
alpha_tag: string,
|
alphaTag: string,
|
||||||
tg_group: string,
|
tgGroup: string,
|
||||||
frequency: number,
|
frequency: number,
|
||||||
metadata: Metadata | null,
|
metadata: Metadata | null,
|
||||||
tags: string[],
|
tags: string[],
|
||||||
alert: boolean,
|
alert: boolean,
|
||||||
alert_rules: AlertRule[],
|
alertRules: AlertRule[],
|
||||||
weight: number,
|
weight: number,
|
||||||
system?: System,
|
system?: System,
|
||||||
learned?: boolean,
|
learned?: boolean,
|
||||||
icon?: string,
|
icon?: string,
|
||||||
) {
|
) {
|
||||||
this.iconSvg = this.iconMap(this.metadata?.icon!);
|
this.iconSvg = this.iconMap(this.metadata?.icon!);
|
||||||
this.alert_rules = this.alert_rules.map((x) =>
|
|
||||||
Object.assign(new AlertRule(), x),
|
|
||||||
);
|
|
||||||
console.log(this.alert_rules);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
iconMap(icon: string): string {
|
iconMap(icon: string): string {
|
||||||
|
@ -103,7 +99,7 @@ export class Talkgroup {
|
||||||
|
|
||||||
tgTuple(): TGID {
|
tgTuple(): TGID {
|
||||||
return <TGID>{
|
return <TGID>{
|
||||||
sys: this.system_id,
|
sys: this.systemId,
|
||||||
tg: this.tgid,
|
tg: this.tgid,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -115,15 +111,15 @@ export interface TalkgroupUI extends Talkgroup {
|
||||||
|
|
||||||
export interface TalkgroupUpdate {
|
export interface TalkgroupUpdate {
|
||||||
id: number;
|
id: number;
|
||||||
system_id: number;
|
systemId: number;
|
||||||
tgid: number;
|
tgid: number;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
alpha_tag: string | null;
|
alphaTag: string | null;
|
||||||
tg_group: string | null;
|
tgGroup: string | null;
|
||||||
frequency: number | null;
|
frequency: number | null;
|
||||||
metadata: Object | null;
|
metadata: Object | null;
|
||||||
tags: string[] | null;
|
tags: string[] | null;
|
||||||
alert: boolean | null;
|
alert: boolean | null;
|
||||||
alert_rules: AlertRule[] | null;
|
alertRules: AlertRule[] | null;
|
||||||
weight: number | null;
|
weight: number | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,8 +55,8 @@
|
||||||
</td>
|
</td>
|
||||||
<td>{{ tg.system?.name }}</td>
|
<td>{{ tg.system?.name }}</td>
|
||||||
<td>{{ tg.system?.id }}</td>
|
<td>{{ tg.system?.id }}</td>
|
||||||
<td>{{ tg.tg_group }}</td>
|
<td>{{ tg.tgGroup }}</td>
|
||||||
<td>{{ tg.alpha_tag }}</td>
|
<td>{{ tg.alphaTag }}</td>
|
||||||
<td>{{ tg.name }}</td>
|
<td>{{ tg.name }}</td>
|
||||||
<td>{{ tg.tgid }}</td>
|
<td>{{ tg.tgid }}</td>
|
||||||
<td>{{ tg?.metadata?.encrypted ? "E" : "" }}</td>
|
<td>{{ tg?.metadata?.encrypted ? "E" : "" }}</td>
|
||||||
|
|
|
@ -15,9 +15,9 @@
|
||||||
<mat-label>Alpha Tag</mat-label
|
<mat-label>Alpha Tag</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
name="alpha_tag"
|
name="alphaTag"
|
||||||
type="text"
|
type="text"
|
||||||
formControlName="alpha_tag"
|
formControlName="alphaTag"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,9 +26,9 @@
|
||||||
<mat-label>Group</mat-label
|
<mat-label>Group</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
name="tg_group"
|
name="tgTroup"
|
||||||
type="text"
|
type="text"
|
||||||
formControlName="tg_group"
|
formControlName="tgGroup"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
@ -108,7 +108,7 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert">
|
<div class="alert">
|
||||||
<alert-rule-builder [rules]="tg.alert_rules" />
|
<alert-rule-builder [rules]="tg.alertRules" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
} @else {
|
} @else {
|
||||||
|
|
|
@ -84,8 +84,8 @@ export class TalkgroupRecordComponent {
|
||||||
readonly _allTags: Observable<string[]>;
|
readonly _allTags: Observable<string[]>;
|
||||||
form = new FormGroup({
|
form = new FormGroup({
|
||||||
name: new FormControl(''),
|
name: new FormControl(''),
|
||||||
alpha_tag: new FormControl(''),
|
alphaTag: new FormControl(''),
|
||||||
tg_group: new FormControl(''),
|
tgGroup: new FormControl(''),
|
||||||
frequency: new FormControl(0),
|
frequency: new FormControl(0),
|
||||||
alert: new FormControl(false),
|
alert: new FormControl(false),
|
||||||
weight: new FormControl(0.0),
|
weight: new FormControl(0.0),
|
||||||
|
@ -158,9 +158,8 @@ export class TalkgroupRecordComponent {
|
||||||
.getTalkgroup(Number(this.tgid.sys), Number(this.tgid.tg))
|
.getTalkgroup(Number(this.tgid.sys), Number(this.tgid.tg))
|
||||||
.pipe(
|
.pipe(
|
||||||
tap((tg) => {
|
tap((tg) => {
|
||||||
console.log('tap run');
|
tg.alertRules = tg.alertRules
|
||||||
tg.alert_rules = tg.alert_rules
|
? tg.alertRules.map((x) => Object.assign(new AlertRule(), x))
|
||||||
? tg.alert_rules.map((x) => Object.assign(new AlertRule(), x))
|
|
||||||
: [];
|
: [];
|
||||||
this.form.patchValue(tg);
|
this.form.patchValue(tg);
|
||||||
this.form.controls['tagInput'].setValue('');
|
this.form.controls['tagInput'].setValue('');
|
||||||
|
@ -181,17 +180,17 @@ export class TalkgroupRecordComponent {
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
let tgu: TalkgroupUpdate = <TalkgroupUpdate>{
|
let tgu: TalkgroupUpdate = <TalkgroupUpdate>{
|
||||||
system_id: this.tgid.sys,
|
systemId: this.tgid.sys,
|
||||||
tgid: this.tgid.tg,
|
tgid: this.tgid.tg,
|
||||||
};
|
};
|
||||||
if (this.form.controls['name'].dirty) {
|
if (this.form.controls['name'].dirty) {
|
||||||
tgu.name = this.form.controls['name'].value;
|
tgu.name = this.form.controls['name'].value;
|
||||||
}
|
}
|
||||||
if (this.form.controls['alpha_tag'].dirty) {
|
if (this.form.controls['alphaTag'].dirty) {
|
||||||
tgu.alpha_tag = this.form.controls['alpha_tag'].value;
|
tgu.alphaTag = this.form.controls['alphaTag'].value;
|
||||||
}
|
}
|
||||||
if (this.form.controls['tg_group'].dirty) {
|
if (this.form.controls['tgGroup'].dirty) {
|
||||||
tgu.tg_group = this.form.controls['tg_group'].value;
|
tgu.tgGroup = this.form.controls['tgGroup'].value;
|
||||||
}
|
}
|
||||||
if (this.form.controls['frequency'].dirty) {
|
if (this.form.controls['frequency'].dirty) {
|
||||||
tgu.frequency = this.form.controls['frequency'].value;
|
tgu.frequency = this.form.controls['frequency'].value;
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="group">
|
<ng-container matColumnDef="group">
|
||||||
<th mat-header-cell *matHeaderCellDef>Group</th>
|
<th mat-header-cell *matHeaderCellDef>Group</th>
|
||||||
<td mat-cell *matCellDef="let tg">{{ tg.tg_group }}</td>
|
<td mat-cell *matCellDef="let tg">{{ tg.tgGroup }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="name">
|
<ng-container matColumnDef="name">
|
||||||
<th mat-header-cell *matHeaderCellDef>Name</th>
|
<th mat-header-cell *matHeaderCellDef>Name</th>
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="alphaTag">
|
<ng-container matColumnDef="alphaTag">
|
||||||
<th mat-header-cell *matHeaderCellDef>Alpha Tag</th>
|
<th mat-header-cell *matHeaderCellDef>Alpha Tag</th>
|
||||||
<td mat-cell *matCellDef="let tg">{{ tg.alpha_tag }}</td>
|
<td mat-cell *matCellDef="let tg">{{ tg.alphaTag }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="tgid">
|
<ng-container matColumnDef="tgid">
|
||||||
<th mat-header-cell *matHeaderCellDef>TG ID</th>
|
<th mat-header-cell *matHeaderCellDef>TG ID</th>
|
||||||
|
|
|
@ -2,14 +2,17 @@ import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpResponse } from '@angular/common/http';
|
import { HttpClient, HttpResponse } from '@angular/common/http';
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
concatMap,
|
|
||||||
Observable,
|
Observable,
|
||||||
ReplaySubject,
|
ReplaySubject,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
|
Subject,
|
||||||
Subscription,
|
Subscription,
|
||||||
switchMap,
|
switchMap,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { Talkgroup, TalkgroupUpdate, TGID } from '../talkgroup';
|
import { Talkgroup, TalkgroupUpdate, TGID } from '../talkgroup';
|
||||||
|
import { Share } from '../shares';
|
||||||
|
import { ShareService } from '../share/share.service';
|
||||||
|
import { AuthService } from '../login/auth.service';
|
||||||
|
|
||||||
export interface Pagination {
|
export interface Pagination {
|
||||||
page: number;
|
page: number;
|
||||||
|
@ -29,30 +32,57 @@ export class TalkgroupService {
|
||||||
private readonly _getTalkgroup = new Map<string, ReplaySubject<Talkgroup>>();
|
private readonly _getTalkgroup = new Map<string, ReplaySubject<Talkgroup>>();
|
||||||
private tgs$: Observable<Talkgroup[]>;
|
private tgs$: Observable<Talkgroup[]>;
|
||||||
private tags$!: Observable<string[]>;
|
private tags$!: Observable<string[]>;
|
||||||
private fetchAll = new BehaviorSubject<'fetch'>('fetch');
|
private fetchAll = new ReplaySubject<Share | null>();
|
||||||
private subscriptions = new Subscription();
|
private subscriptions = new Subscription();
|
||||||
constructor(private http: HttpClient) {
|
constructor(
|
||||||
this.tgs$ = this.fetchAll.pipe(switchMap(() => this.getTalkgroups()));
|
private http: HttpClient,
|
||||||
|
private shareSvc: ShareService,
|
||||||
|
private authSvc: AuthService,
|
||||||
|
) {
|
||||||
|
this.tgs$ = this.fetchAll.pipe(
|
||||||
|
switchMap((share) => this.getTalkgroups(share)),
|
||||||
|
shareReplay(),
|
||||||
|
);
|
||||||
this.tags$ = this.fetchAll.pipe(
|
this.tags$ = this.fetchAll.pipe(
|
||||||
switchMap(() => this.getAllTags()),
|
switchMap(() => this.getAllTags()),
|
||||||
shareReplay(),
|
shareReplay(),
|
||||||
);
|
);
|
||||||
|
let sh = this.shareSvc.inShare();
|
||||||
|
if (sh) {
|
||||||
|
this.shareSvc.getShare(sh).subscribe(this.fetchAll);
|
||||||
|
} else {
|
||||||
|
if (this.authSvc.isAuth()) {
|
||||||
|
this.fetchAll.next(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
this.fillTgMap();
|
this.fillTgMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setShare(share: Share | null) {
|
||||||
|
if (!this.authSvc.isAuth() && share !== null) {
|
||||||
|
this.fetchAll.next(share);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.subscriptions.unsubscribe();
|
this.subscriptions.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllTags(): Observable<string[]> {
|
getAllTags(): Observable<string[]> {
|
||||||
return this.http.get<string[]>('/api/talkgroup/tags').pipe(shareReplay());
|
return this.http.get<string[]>('/api/talkgroup/tags');
|
||||||
}
|
}
|
||||||
|
|
||||||
getTalkgroups(): Observable<Talkgroup[]> {
|
getTalkgroups(share: Share | null): Observable<Talkgroup[]> {
|
||||||
return this.http.get<Talkgroup[]>('/api/talkgroup/').pipe(shareReplay());
|
return this.http.get<Talkgroup[]>(
|
||||||
|
share ? `/share/${share.id}/talkgroups` : '/api/talkgroup/',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTalkgroup(sys: number, tg: number): Observable<Talkgroup> {
|
getTalkgroup(
|
||||||
|
sys: number,
|
||||||
|
tg: number,
|
||||||
|
share: Share | null = null,
|
||||||
|
): Observable<Talkgroup> {
|
||||||
const key = this.tgKey(sys, tg);
|
const key = this.tgKey(sys, tg);
|
||||||
if (!this._getTalkgroup.get(key)) {
|
if (!this._getTalkgroup.get(key)) {
|
||||||
let rs = new ReplaySubject<Talkgroup>();
|
let rs = new ReplaySubject<Talkgroup>();
|
||||||
|
@ -62,10 +92,10 @@ export class TalkgroupService {
|
||||||
}
|
}
|
||||||
|
|
||||||
putTalkgroup(tu: TalkgroupUpdate): Observable<Talkgroup> {
|
putTalkgroup(tu: TalkgroupUpdate): Observable<Talkgroup> {
|
||||||
let tgid = this.tgKey(tu.system_id, tu.tgid);
|
let tgid = this.tgKey(tu.systemId, tu.tgid);
|
||||||
|
|
||||||
return this.http
|
return this.http
|
||||||
.put<Talkgroup>(`/api/talkgroup/${tu.system_id}/${tu.tgid}`, tu)
|
.put<Talkgroup>(`/api/talkgroup/${tu.systemId}/${tu.tgid}`, tu)
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((tg) => {
|
switchMap((tg) => {
|
||||||
let tObs = this._getTalkgroup.get(tgid);
|
let tObs = this._getTalkgroup.get(tgid);
|
||||||
|
@ -100,7 +130,7 @@ export class TalkgroupService {
|
||||||
this.subscriptions.add(
|
this.subscriptions.add(
|
||||||
this.tgs$.subscribe((tgs) => {
|
this.tgs$.subscribe((tgs) => {
|
||||||
tgs.forEach((tg) => {
|
tgs.forEach((tg) => {
|
||||||
let tgid = this.tgKey(tg.system_id, tg.tgid);
|
let tgid = this.tgKey(tg.systemId, tg.tgid);
|
||||||
const rs = this._getTalkgroup.get(tgid);
|
const rs = this._getTalkgroup.get(tgid);
|
||||||
if (rs) {
|
if (rs) {
|
||||||
(rs as ReplaySubject<Talkgroup>).next(tg);
|
(rs as ReplaySubject<Talkgroup>).next(tg);
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
{
|
{
|
||||||
"/api": {
|
"/api/": {
|
||||||
|
"target": "http://xenon:3050",
|
||||||
|
"secure": false
|
||||||
|
},
|
||||||
|
"/share/": {
|
||||||
"target": "http://xenon:3050",
|
"target": "http://xenon:3050",
|
||||||
"secure": false
|
"secure": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
|
@ -11,10 +13,12 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/version"
|
"dynatron.me/x/stillbox/internal/version"
|
||||||
"dynatron.me/x/stillbox/pkg/pb"
|
"dynatron.me/x/stillbox/pkg/pb"
|
||||||
|
"golang.org/x/term"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
@ -37,6 +41,31 @@ func userAgent(h http.Header) {
|
||||||
h.Set("User-Agent", uaString)
|
h.Set("User-Agent", uaString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCreds() {
|
||||||
|
rdr := bufio.NewReader(os.Stdin)
|
||||||
|
if username == nil || *username == "" {
|
||||||
|
fmt.Print("Username: ")
|
||||||
|
un, err := rdr.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
username = &un
|
||||||
|
}
|
||||||
|
|
||||||
|
if password == nil || *password == "" {
|
||||||
|
fmt.Print("Password: ")
|
||||||
|
bytePass, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pS := string(bytePass)
|
||||||
|
pS = strings.Trim(pS, "\n")
|
||||||
|
password = &pS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
log.SetFlags(0)
|
log.SetFlags(0)
|
||||||
|
@ -53,6 +82,8 @@ func main() {
|
||||||
signal.Notify(interrupt, os.Interrupt)
|
signal.Notify(interrupt, os.Interrupt)
|
||||||
play := NewPlayer()
|
play := NewPlayer()
|
||||||
|
|
||||||
|
getCreds()
|
||||||
|
|
||||||
loginForm := url.Values{}
|
loginForm := url.Values{}
|
||||||
loginForm.Add("username", *username)
|
loginForm.Add("username", *username)
|
||||||
loginForm.Add("password", *password)
|
loginForm.Add("password", *password)
|
||||||
|
|
10
go.mod
10
go.mod
|
@ -5,6 +5,7 @@ go 1.23.2
|
||||||
require (
|
require (
|
||||||
dynatron.me/x/go-minimp3 v0.0.0-20240805171536-7ea857e216d6
|
dynatron.me/x/go-minimp3 v0.0.0-20240805171536-7ea857e216d6
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||||
|
github.com/el-mike/restrict/v2 v2.0.0
|
||||||
github.com/go-audio/wav v1.1.0
|
github.com/go-audio/wav v1.1.0
|
||||||
github.com/go-chi/chi/v5 v5.1.0
|
github.com/go-chi/chi/v5 v5.1.0
|
||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
|
@ -21,6 +22,8 @@ require (
|
||||||
github.com/knadh/koanf/providers/env v1.0.0
|
github.com/knadh/koanf/providers/env v1.0.0
|
||||||
github.com/knadh/koanf/providers/file v1.1.2
|
github.com/knadh/koanf/providers/file v1.1.2
|
||||||
github.com/knadh/koanf/v2 v2.1.2
|
github.com/knadh/koanf/v2 v2.1.2
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.1.3
|
||||||
|
github.com/matoous/go-nanoid v1.5.1
|
||||||
github.com/nikoksr/notify v1.1.0
|
github.com/nikoksr/notify v1.1.0
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
|
@ -28,7 +31,7 @@ require (
|
||||||
github.com/urfave/cli/v2 v2.27.5
|
github.com/urfave/cli/v2 v2.27.5
|
||||||
golang.org/x/crypto v0.29.0
|
golang.org/x/crypto v0.29.0
|
||||||
golang.org/x/sync v0.9.0
|
golang.org/x/sync v0.9.0
|
||||||
golang.org/x/term v0.26.0
|
golang.org/x/term v0.28.0
|
||||||
google.golang.org/protobuf v1.35.2
|
google.golang.org/protobuf v1.35.2
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
@ -39,7 +42,6 @@ require (
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||||
github.com/el-mike/restrict/v2 v2.0.0 // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
github.com/go-audio/audio v1.0.0 // indirect
|
github.com/go-audio/audio v1.0.0 // indirect
|
||||||
github.com/go-audio/riff v1.0.0 // indirect
|
github.com/go-audio/riff v1.0.0 // indirect
|
||||||
|
@ -55,9 +57,7 @@ require (
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
||||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||||
github.com/lestrrat-go/jwx/v2 v2.1.3 // indirect
|
|
||||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||||
github.com/matoous/go-nanoid v1.5.1 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
@ -71,6 +71,6 @@ require (
|
||||||
golang.org/x/exp/shiny v0.0.0-20241108190413-2d47ceb2692f // indirect
|
golang.org/x/exp/shiny v0.0.0-20241108190413-2d47ceb2692f // indirect
|
||||||
golang.org/x/image v0.22.0 // indirect
|
golang.org/x/image v0.22.0 // indirect
|
||||||
golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f // indirect
|
golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f // indirect
|
||||||
golang.org/x/sys v0.27.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/text v0.20.0 // indirect
|
golang.org/x/text v0.20.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
23
go.sum
23
go.sum
|
@ -8,12 +8,6 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
|
||||||
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
|
||||||
github.com/casbin/casbin/v2 v2.103.0 h1:dHElatNXNrr8XcseUov0ZSiWjauwmZZE6YMV3eU1yic=
|
|
||||||
github.com/casbin/casbin/v2 v2.103.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
|
|
||||||
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
|
|
||||||
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
@ -69,7 +63,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
|
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
|
github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
|
||||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
@ -117,8 +110,6 @@ github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCG
|
||||||
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||||
github.com/lestrrat-go/jwx/v2 v2.1.2 h1:6poete4MPsO8+LAEVhpdrNI4Xp2xdiafgl2RD89moBc=
|
|
||||||
github.com/lestrrat-go/jwx/v2 v2.1.2/go.mod h1:pO+Gz9whn7MPdbsqSJzG8TlEpMZCwQDXnFJ+zsUVh8Y=
|
|
||||||
github.com/lestrrat-go/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5/Jo=
|
github.com/lestrrat-go/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5/Jo=
|
||||||
github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo=
|
github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo=
|
||||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||||
|
@ -176,7 +167,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
@ -196,7 +186,6 @@ go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HY
|
||||||
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
@ -208,24 +197,20 @@ golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4
|
||||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f h1:23H/YlmTHfmmvpZ+ajKZL0qLz0+IwFOIqQA0mQbmLeM=
|
golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f h1:23H/YlmTHfmmvpZ+ajKZL0qLz0+IwFOIqQA0mQbmLeM=
|
||||||
golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f/go.mod h1:UbSUP4uu/C9hw9R2CkojhXlAxvayHjBdU9aRvE+c1To=
|
golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f/go.mod h1:UbSUP4uu/C9hw9R2CkojhXlAxvayHjBdU9aRvE+c1To=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
|
@ -38,6 +38,7 @@ type Alerter interface {
|
||||||
|
|
||||||
Enabled() bool
|
Enabled() bool
|
||||||
Go(context.Context)
|
Go(context.Context)
|
||||||
|
HUP(*config.Config)
|
||||||
|
|
||||||
stats
|
stats
|
||||||
}
|
}
|
||||||
|
@ -101,9 +102,7 @@ func New(cfg config.Alerting, tgCache tgstore.Store, opts ...AlertOption) Alerte
|
||||||
tgCache: tgCache,
|
tgCache: tgCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Renotify != nil {
|
as.reload()
|
||||||
as.renotify = cfg.Renotify.Duration()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(as)
|
opt(as)
|
||||||
|
@ -122,6 +121,21 @@ func New(cfg config.Alerting, tgCache tgstore.Store, opts ...AlertOption) Alerte
|
||||||
return as
|
return as
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (as *alerter) reload() {
|
||||||
|
if as.cfg.Renotify != nil {
|
||||||
|
as.renotify = as.cfg.Renotify.Duration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (as *alerter) HUP(cfg *config.Config) {
|
||||||
|
as.Lock()
|
||||||
|
defer as.Unlock()
|
||||||
|
|
||||||
|
log.Debug().Msg("reloading alert config")
|
||||||
|
as.cfg = cfg.Alerting
|
||||||
|
as.reload()
|
||||||
|
}
|
||||||
|
|
||||||
// Go is the alerting loop. It does not start a goroutine.
|
// Go is the alerting loop. It does not start a goroutine.
|
||||||
func (as *alerter) Go(ctx context.Context) {
|
func (as *alerter) Go(ctx context.Context) {
|
||||||
ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "alerter"})
|
ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "alerter"})
|
||||||
|
@ -166,7 +180,12 @@ func (as *alerter) eval(ctx context.Context, now time.Time, testMode bool) ([]al
|
||||||
for _, s := range as.scores {
|
for _, s := range as.scores {
|
||||||
origScore := s.Score
|
origScore := s.Score
|
||||||
tgr, err := as.tgCache.TG(ctx, s.ID)
|
tgr, err := as.tgCache.TG(ctx, s.ID)
|
||||||
if err != nil || !tgr.Talkgroup.Alert {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("alerting eval tg get")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tgr.Talkgroup.Alert {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,3 +395,4 @@ func (*noopAlerter) SinkType() string { return "noopA
|
||||||
func (*noopAlerter) Call(_ context.Context, _ *calls.Call) error { return nil }
|
func (*noopAlerter) Call(_ context.Context, _ *calls.Call) error { return nil }
|
||||||
func (*noopAlerter) Go(_ context.Context) {}
|
func (*noopAlerter) Go(_ context.Context) {}
|
||||||
func (*noopAlerter) Enabled() bool { return false }
|
func (*noopAlerter) Enabled() bool { return false }
|
||||||
|
func (*noopAlerter) HUP(_ *config.Config) {}
|
||||||
|
|
|
@ -34,8 +34,8 @@ type jwtAuth interface {
|
||||||
// InstallVerifyMiddleware installs the JWT verifier middleware to the provided chi Router.
|
// InstallVerifyMiddleware installs the JWT verifier middleware to the provided chi Router.
|
||||||
VerifyMiddleware() func(http.Handler) http.Handler
|
VerifyMiddleware() func(http.Handler) http.Handler
|
||||||
|
|
||||||
// InstallAuthMiddleware installs the JWT authenticator middleware to the provided chi Router.
|
// SubjectMiddleware sets the request context subject from JWT or public.
|
||||||
AuthMiddleware() func(http.Handler) http.Handler
|
SubjectMiddleware(requireAuth bool) func(http.Handler) http.Handler
|
||||||
|
|
||||||
// PublicRoutes installs the auth route to the provided chi Router.
|
// PublicRoutes installs the auth route to the provided chi Router.
|
||||||
PublicRoutes(chi.Router)
|
PublicRoutes(chi.Router)
|
||||||
|
@ -84,22 +84,39 @@ func TokenFromCookie(r *http.Request) string {
|
||||||
return cookie.Value
|
return cookie.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) AuthMiddleware() func(http.Handler) http.Handler {
|
func (a *Auth) PublicSubjectMiddleware() func(http.Handler) http.Handler {
|
||||||
|
return a.SubjectMiddleware(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) AuthorizedSubjectMiddleware() func(http.Handler) http.Handler {
|
||||||
|
return a.SubjectMiddleware(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) SubjectMiddleware(requireToken bool) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
hfn := func(w http.ResponseWriter, r *http.Request) {
|
hfn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
token, _, err := jwtauth.FromContext(r.Context())
|
token, _, err := jwtauth.FromContext(r.Context())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil && requireToken {
|
||||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if token != nil && jwt.Validate(token, a.jwt.ValidateOptions()...) == nil {
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
|
if token != nil {
|
||||||
|
err := jwt.Validate(token, a.jwt.ValidateOptions()...)
|
||||||
|
if err != nil {
|
||||||
|
err = jwtauth.ErrorReason(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
username := token.Subject()
|
username := token.Subject()
|
||||||
|
|
||||||
sub, err := users.FromCtx(ctx).GetUser(ctx, username)
|
sub, err := users.FromCtx(ctx).GetUser(ctx, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Str("username", username).Err(err).Msg("subject middleware get subject")
|
||||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -111,8 +128,9 @@ func (a *Auth) AuthMiddleware() func(http.Handler) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token is authenticated, pass it through
|
// Public subject
|
||||||
next.ServeHTTP(w, r)
|
ctx = entities.CtxWithSubject(ctx, entities.NewPublicSubject(r))
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
}
|
}
|
||||||
return http.HandlerFunc(hfn)
|
return http.HandlerFunc(hfn)
|
||||||
}
|
}
|
||||||
|
@ -211,7 +229,7 @@ func (a *Auth) routeRefresh(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if cookie.Secure {
|
if cookie.Secure {
|
||||||
cookie.Domain = r.Host
|
cookie.Domain = strings.Split(r.Host, ":")[0]
|
||||||
}
|
}
|
||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
|
|
||||||
|
@ -271,7 +289,7 @@ func (a *Auth) routeAuth(w http.ResponseWriter, r *http.Request) {
|
||||||
MaxAge: 60 * 60 * 24 * 30, // one month
|
MaxAge: 60 * 60 * 24 * 30, // one month
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie.Domain = r.Host
|
cookie.Domain = strings.Split(r.Host, ":")[0]
|
||||||
if a.allowInsecureCookie(r) {
|
if a.allowInsecureCookie(r) {
|
||||||
a.setInsecureCookie(cookie)
|
a.setInsecureCookie(cookie)
|
||||||
}
|
}
|
||||||
|
@ -297,7 +315,7 @@ func (a *Auth) routeLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie.Domain = r.Host
|
cookie.Domain = strings.Split(r.Host, ":")[0]
|
||||||
if a.allowInsecureCookie(r) {
|
if a.allowInsecureCookie(r) {
|
||||||
cookie.Secure = true
|
cookie.Secure = true
|
||||||
cookie.SameSite = http.SameSiteNoneMode
|
cookie.SameSite = http.SameSiteNoneMode
|
||||||
|
|
|
@ -3,9 +3,11 @@ package calls
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/audio"
|
"dynatron.me/x/stillbox/internal/audio"
|
||||||
|
"dynatron.me/x/stillbox/internal/common"
|
||||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||||
"dynatron.me/x/stillbox/pkg/pb"
|
"dynatron.me/x/stillbox/pkg/pb"
|
||||||
"dynatron.me/x/stillbox/pkg/rbac/entities"
|
"dynatron.me/x/stillbox/pkg/rbac/entities"
|
||||||
|
@ -22,6 +24,13 @@ func (d CallDuration) Duration() time.Duration {
|
||||||
return time.Duration(d)
|
return time.Duration(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d CallDuration) ColonFormat() string {
|
||||||
|
dur := d.Duration().Round(time.Second)
|
||||||
|
m := dur / time.Minute
|
||||||
|
s := dur / time.Second
|
||||||
|
return fmt.Sprintf("%d:%02d", m, s)
|
||||||
|
}
|
||||||
|
|
||||||
func (d CallDuration) MsInt32Ptr() *int32 {
|
func (d CallDuration) MsInt32Ptr() *int32 {
|
||||||
if time.Duration(d) == 0 {
|
if time.Duration(d) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
@ -59,18 +68,19 @@ type Call struct {
|
||||||
AudioType string `json:"audioType,omitempty" relayOut:"audioType,omitempty"`
|
AudioType string `json:"audioType,omitempty" relayOut:"audioType,omitempty"`
|
||||||
AudioURL *string `json:"audioURL,omitempty" relayOut:"audioURL,omitempty"`
|
AudioURL *string `json:"audioURL,omitempty" relayOut:"audioURL,omitempty"`
|
||||||
Duration CallDuration `json:"duration,omitempty" relayOut:"duration,omitempty"`
|
Duration CallDuration `json:"duration,omitempty" relayOut:"duration,omitempty"`
|
||||||
DateTime time.Time `json:"call_date,omitempty" relayOut:"dateTime,omitempty"`
|
DateTime time.Time `json:"callDate,omitempty" relayOut:"dateTime,omitempty"`
|
||||||
Frequencies []int `json:"frequencies,omitempty" relayOut:"frequencies,omitempty"`
|
Frequencies []int `json:"frequencies,omitempty" relayOut:"frequencies,omitempty"`
|
||||||
Frequency int `json:"frequency,omitempty" relayOut:"frequency,omitempty"`
|
Frequency int `json:"frequency,omitempty" relayOut:"frequency,omitempty"`
|
||||||
Patches []int `json:"patches,omitempty" relayOut:"patches,omitempty"`
|
Patches []int `json:"patches,omitempty" relayOut:"patches,omitempty"`
|
||||||
Source int `json:"source,omitempty" relayOut:"source,omitempty"`
|
Source int `json:"source,omitempty" relayOut:"source,omitempty"`
|
||||||
System int `json:"system_id,omitempty" relayOut:"system,omitempty"`
|
System int `json:"systemId,omitempty" relayOut:"system,omitempty"`
|
||||||
Submitter *users.UserID `json:"submitter,omitempty" relayOut:"submitter,omitempty"`
|
Submitter *users.UserID `json:"submitter,omitempty" relayOut:"submitter,omitempty"`
|
||||||
SystemLabel string `json:"system_name,omitempty" relayOut:"systemLabel,omitempty"`
|
SystemLabel string `json:"systemName,omitempty" relayOut:"systemLabel,omitempty"`
|
||||||
Talkgroup int `json:"tgid,omitempty" relayOut:"talkgroup,omitempty"`
|
Talkgroup int `json:"tgid,omitempty" relayOut:"talkgroup,omitempty"`
|
||||||
TalkgroupGroup *string `json:"talkgroupGroup,omitempty" relayOut:"talkgroupGroup,omitempty"`
|
TalkgroupGroup *string `json:"talkgroupGroup,omitempty" relayOut:"talkgroupGroup,omitempty"`
|
||||||
TalkgroupLabel *string `json:"talkgroupLabel,omitempty" relayOut:"talkgroupLabel,omitempty"`
|
TalkgroupLabel *string `json:"talkgroupLabel,omitempty" relayOut:"talkgroupLabel,omitempty"`
|
||||||
TGAlphaTag *string `json:"tg_name,omitempty" relayOut:"talkgroupTag,omitempty"`
|
TGAlphaTag *string `json:"tgAlphaTag,omitempty" relayOut:"talkgroupTag,omitempty"`
|
||||||
|
Transcript *string `json:"transcript" relayOut:"transcript,omitempty"`
|
||||||
|
|
||||||
shouldStore bool `json:"-"`
|
shouldStore bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
@ -87,6 +97,15 @@ func (c *Call) ShouldStore() bool {
|
||||||
return c.shouldStore
|
return c.shouldStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Call) SetShareURL(baseURL url.URL, shareID string) {
|
||||||
|
if c.AudioURL != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL.Path = fmt.Sprintf("/share/%s/call", shareID)
|
||||||
|
c.AudioURL = common.PtrTo(baseURL.String())
|
||||||
|
}
|
||||||
|
|
||||||
func Make(call *Call, dontStore bool) (*Call, error) {
|
func Make(call *Call, dontStore bool) (*Call, error) {
|
||||||
err := call.computeLength()
|
err := call.computeLength()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -74,6 +74,7 @@ func toAddCallParams(call *calls.Call) database.AddCallParams {
|
||||||
AudioName: common.NilIfZero(call.AudioName),
|
AudioName: common.NilIfZero(call.AudioName),
|
||||||
AudioBlob: call.Audio,
|
AudioBlob: call.Audio,
|
||||||
AudioType: common.NilIfZero(call.AudioType),
|
AudioType: common.NilIfZero(call.AudioType),
|
||||||
|
AudioUrl: call.AudioURL,
|
||||||
Duration: call.Duration.MsInt32Ptr(),
|
Duration: call.Duration.MsInt32Ptr(),
|
||||||
Frequency: call.Frequency,
|
Frequency: call.Frequency,
|
||||||
Frequencies: call.Frequencies,
|
Frequencies: call.Frequencies,
|
||||||
|
@ -145,7 +146,7 @@ func (s *store) CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) Call(ctx context.Context, id uuid.UUID) (*calls.Call, error) {
|
func (s *store) Call(ctx context.Context, id uuid.UUID) (*calls.Call, error) {
|
||||||
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceCall), rbac.WithActions(entities.ActionRead))
|
_, err := rbac.Check(ctx, &calls.Call{ID: id}, rbac.WithActions(entities.ActionRead))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -178,6 +179,7 @@ func (s *store) Call(ctx context.Context, id uuid.UUID) (*calls.Call, error) {
|
||||||
TalkgroupLabel: c.TGLabel,
|
TalkgroupLabel: c.TGLabel,
|
||||||
TalkgroupGroup: c.TGGroup,
|
TalkgroupGroup: c.TGGroup,
|
||||||
TGAlphaTag: c.TGAlphaTag,
|
TGAlphaTag: c.TGAlphaTag,
|
||||||
|
Transcript: c.Transcript,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ type StoreTGVersionBatchResults struct {
|
||||||
|
|
||||||
type StoreTGVersionParams struct {
|
type StoreTGVersionParams struct {
|
||||||
Submitter *int32 `json:"submitter"`
|
Submitter *int32 `json:"submitter"`
|
||||||
SystemID int32 `json:"system_id"`
|
SystemID int32 `json:"systemId"`
|
||||||
TGID int32 `json:"tgid"`
|
TGID int32 `json:"tgid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,16 +135,16 @@ type UpsertTalkgroupBatchResults struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpsertTalkgroupParams struct {
|
type UpsertTalkgroupParams struct {
|
||||||
SystemID int32 `json:"system_id"`
|
SystemID int32 `json:"systemId"`
|
||||||
TGID int32 `json:"tgid"`
|
TGID int32 `json:"tgid"`
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
AlphaTag *string `json:"alpha_tag"`
|
AlphaTag *string `json:"alphaTag"`
|
||||||
TGGroup *string `json:"tg_group"`
|
TGGroup *string `json:"tgGroup"`
|
||||||
Frequency *int32 `json:"frequency"`
|
Frequency *int32 `json:"frequency"`
|
||||||
Metadata jsontypes.Metadata `json:"metadata"`
|
Metadata jsontypes.Metadata `json:"metadata"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Alert interface{} `json:"alert"`
|
Alert interface{} `json:"alert"`
|
||||||
AlertRules rules.AlertRules `json:"alert_rules"`
|
AlertRules rules.AlertRules `json:"alertRules"`
|
||||||
Weight pgtype.Numeric `json:"weight"`
|
Weight pgtype.Numeric `json:"weight"`
|
||||||
Learned *bool `json:"learned"`
|
Learned *bool `json:"learned"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,10 +30,10 @@ VALUES
|
||||||
type AddAlertParams struct {
|
type AddAlertParams struct {
|
||||||
Time pgtype.Timestamptz `json:"time"`
|
Time pgtype.Timestamptz `json:"time"`
|
||||||
TGID int `json:"tgid"`
|
TGID int `json:"tgid"`
|
||||||
SystemID int `json:"system_id"`
|
SystemID int `json:"systemId"`
|
||||||
Weight *float32 `json:"weight"`
|
Weight *float32 `json:"weight"`
|
||||||
Score *float32 `json:"score"`
|
Score *float32 `json:"score"`
|
||||||
OrigScore *float32 `json:"orig_score"`
|
OrigScore *float32 `json:"origScore"`
|
||||||
Notified bool `json:"notified"`
|
Notified bool `json:"notified"`
|
||||||
Metadata []byte `json:"metadata"`
|
Metadata []byte `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
@ -97,18 +97,18 @@ type AddCallParams struct {
|
||||||
Submitter *int32 `json:"submitter"`
|
Submitter *int32 `json:"submitter"`
|
||||||
System int `json:"system"`
|
System int `json:"system"`
|
||||||
Talkgroup int `json:"talkgroup"`
|
Talkgroup int `json:"talkgroup"`
|
||||||
CallDate pgtype.Timestamptz `json:"call_date"`
|
CallDate pgtype.Timestamptz `json:"callDate"`
|
||||||
AudioName *string `json:"audio_name"`
|
AudioName *string `json:"audioName"`
|
||||||
AudioBlob []byte `json:"audio_blob"`
|
AudioBlob []byte `json:"audioBlob"`
|
||||||
AudioType *string `json:"audio_type"`
|
AudioType *string `json:"audioType"`
|
||||||
AudioUrl *string `json:"audio_url"`
|
AudioUrl *string `json:"audioUrl"`
|
||||||
Duration *int32 `json:"duration"`
|
Duration *int32 `json:"duration"`
|
||||||
Frequency int `json:"frequency"`
|
Frequency int `json:"frequency"`
|
||||||
Frequencies []int `json:"frequencies"`
|
Frequencies []int `json:"frequencies"`
|
||||||
Patches []int `json:"patches"`
|
Patches []int `json:"patches"`
|
||||||
TGLabel *string `json:"tg_label"`
|
TGLabel *string `json:"tgLabel"`
|
||||||
TGAlphaTag *string `json:"tg_alpha_tag"`
|
TGAlphaTag *string `json:"tgAlphaTag"`
|
||||||
TGGroup *string `json:"tg_group"`
|
TGGroup *string `json:"tgGroup"`
|
||||||
Source int `json:"source"`
|
Source int `json:"source"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +181,8 @@ SELECT
|
||||||
tg_label,
|
tg_label,
|
||||||
tg_alpha_tag,
|
tg_alpha_tag,
|
||||||
tg_group,
|
tg_group,
|
||||||
source
|
source,
|
||||||
|
transcript
|
||||||
FROM calls
|
FROM calls
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
@ -191,18 +192,19 @@ type GetCallRow struct {
|
||||||
Submitter *int32 `json:"submitter"`
|
Submitter *int32 `json:"submitter"`
|
||||||
System int `json:"system"`
|
System int `json:"system"`
|
||||||
Talkgroup int `json:"talkgroup"`
|
Talkgroup int `json:"talkgroup"`
|
||||||
CallDate pgtype.Timestamptz `json:"call_date"`
|
CallDate pgtype.Timestamptz `json:"callDate"`
|
||||||
AudioName *string `json:"audio_name"`
|
AudioName *string `json:"audioName"`
|
||||||
AudioType *string `json:"audio_type"`
|
AudioType *string `json:"audioType"`
|
||||||
AudioUrl *string `json:"audio_url"`
|
AudioUrl *string `json:"audioUrl"`
|
||||||
Duration *int32 `json:"duration"`
|
Duration *int32 `json:"duration"`
|
||||||
Frequency int `json:"frequency"`
|
Frequency int `json:"frequency"`
|
||||||
Frequencies []int `json:"frequencies"`
|
Frequencies []int `json:"frequencies"`
|
||||||
Patches []int `json:"patches"`
|
Patches []int `json:"patches"`
|
||||||
TGLabel *string `json:"tg_label"`
|
TGLabel *string `json:"tgLabel"`
|
||||||
TGAlphaTag *string `json:"tg_alpha_tag"`
|
TGAlphaTag *string `json:"tgAlphaTag"`
|
||||||
TGGroup *string `json:"tg_group"`
|
TGGroup *string `json:"tgGroup"`
|
||||||
Source int `json:"source"`
|
Source int `json:"source"`
|
||||||
|
Transcript *string `json:"transcript"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetCall(ctx context.Context, id uuid.UUID) (GetCallRow, error) {
|
func (q *Queries) GetCall(ctx context.Context, id uuid.UUID) (GetCallRow, error) {
|
||||||
|
@ -225,6 +227,7 @@ func (q *Queries) GetCall(ctx context.Context, id uuid.UUID) (GetCallRow, error)
|
||||||
&i.TGAlphaTag,
|
&i.TGAlphaTag,
|
||||||
&i.TGGroup,
|
&i.TGGroup,
|
||||||
&i.Source,
|
&i.Source,
|
||||||
|
&i.Transcript,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
@ -248,10 +251,10 @@ WHERE sc.id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetCallAudioByIDRow struct {
|
type GetCallAudioByIDRow struct {
|
||||||
CallDate pgtype.Timestamptz `json:"call_date"`
|
CallDate pgtype.Timestamptz `json:"callDate"`
|
||||||
AudioName *string `json:"audio_name"`
|
AudioName *string `json:"audioName"`
|
||||||
AudioType *string `json:"audio_type"`
|
AudioType *string `json:"audioType"`
|
||||||
AudioBlob []byte `json:"audio_blob"`
|
AudioBlob []byte `json:"audioBlob"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetCallAudioByID(ctx context.Context, id uuid.UUID) (GetCallAudioByIDRow, error) {
|
func (q *Queries) GetCallAudioByID(ctx context.Context, id uuid.UUID) (GetCallAudioByIDRow, error) {
|
||||||
|
@ -315,10 +318,10 @@ CASE WHEN $4::TEXT[] IS NOT NULL THEN
|
||||||
type ListCallsCountParams struct {
|
type ListCallsCountParams struct {
|
||||||
Start pgtype.Timestamptz `json:"start"`
|
Start pgtype.Timestamptz `json:"start"`
|
||||||
End pgtype.Timestamptz `json:"end"`
|
End pgtype.Timestamptz `json:"end"`
|
||||||
TagsAny []string `json:"tags_any"`
|
TagsAny []string `json:"tagsAny"`
|
||||||
TagsNot []string `json:"tags_not"`
|
TagsNot []string `json:"tagsNot"`
|
||||||
TGFilter *string `json:"tg_filter"`
|
TGFilter *string `json:"tgFilter"`
|
||||||
LongerThan pgtype.Numeric `json:"longer_than"`
|
LongerThan pgtype.Numeric `json:"longerThan"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListCallsCount(ctx context.Context, arg ListCallsCountParams) (int64, error) {
|
func (q *Queries) ListCallsCount(ctx context.Context, arg ListCallsCountParams) (int64, error) {
|
||||||
|
@ -375,20 +378,20 @@ FETCH NEXT $9 ROWS ONLY
|
||||||
type ListCallsPParams struct {
|
type ListCallsPParams struct {
|
||||||
Start pgtype.Timestamptz `json:"start"`
|
Start pgtype.Timestamptz `json:"start"`
|
||||||
End pgtype.Timestamptz `json:"end"`
|
End pgtype.Timestamptz `json:"end"`
|
||||||
TagsAny []string `json:"tags_any"`
|
TagsAny []string `json:"tagsAny"`
|
||||||
TagsNot []string `json:"tags_not"`
|
TagsNot []string `json:"tagsNot"`
|
||||||
TGFilter *string `json:"tg_filter"`
|
TGFilter *string `json:"tgFilter"`
|
||||||
LongerThan pgtype.Numeric `json:"longer_than"`
|
LongerThan pgtype.Numeric `json:"longerThan"`
|
||||||
Direction string `json:"direction"`
|
Direction string `json:"direction"`
|
||||||
Offset int32 `json:"offset"`
|
Offset int32 `json:"offset"`
|
||||||
PerPage int32 `json:"per_page"`
|
PerPage int32 `json:"perPage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListCallsPRow struct {
|
type ListCallsPRow struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
CallDate pgtype.Timestamptz `json:"call_date"`
|
CallDate pgtype.Timestamptz `json:"callDate"`
|
||||||
Duration *int32 `json:"duration"`
|
Duration *int32 `json:"duration"`
|
||||||
SystemID int `json:"system_id"`
|
SystemID int `json:"systemId"`
|
||||||
TGID int `json:"tgid"`
|
TGID int `json:"tgid"`
|
||||||
Incidents int64 `json:"incidents"`
|
Incidents int64 `json:"incidents"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ INSERT INTO incidents (
|
||||||
name,
|
name,
|
||||||
owner,
|
owner,
|
||||||
description,
|
description,
|
||||||
|
created_at,
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
location,
|
location,
|
||||||
|
@ -70,12 +71,13 @@ INSERT INTO incidents (
|
||||||
$2,
|
$2,
|
||||||
$3,
|
$3,
|
||||||
$4,
|
$4,
|
||||||
|
NOW(),
|
||||||
$5,
|
$5,
|
||||||
$6,
|
$6,
|
||||||
$7,
|
$7,
|
||||||
$8
|
$8
|
||||||
)
|
)
|
||||||
RETURNING id, name, owner, description, start_time, end_time, location, metadata
|
RETURNING id, name, owner, description, created_at, start_time, end_time, location, metadata
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateIncidentParams struct {
|
type CreateIncidentParams struct {
|
||||||
|
@ -83,8 +85,8 @@ type CreateIncidentParams struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Owner int `json:"owner"`
|
Owner int `json:"owner"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
StartTime pgtype.Timestamptz `json:"start_time"`
|
StartTime pgtype.Timestamptz `json:"startTime"`
|
||||||
EndTime pgtype.Timestamptz `json:"end_time"`
|
EndTime pgtype.Timestamptz `json:"endTime"`
|
||||||
Location []byte `json:"location"`
|
Location []byte `json:"location"`
|
||||||
Metadata jsontypes.Metadata `json:"metadata"`
|
Metadata jsontypes.Metadata `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
@ -106,6 +108,7 @@ func (q *Queries) CreateIncident(ctx context.Context, arg CreateIncidentParams)
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Owner,
|
&i.Owner,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
|
&i.CreatedAt,
|
||||||
&i.StartTime,
|
&i.StartTime,
|
||||||
&i.EndTime,
|
&i.EndTime,
|
||||||
&i.Location,
|
&i.Location,
|
||||||
|
@ -129,6 +132,7 @@ SELECT
|
||||||
i.name,
|
i.name,
|
||||||
i.owner,
|
i.owner,
|
||||||
i.description,
|
i.description,
|
||||||
|
i.created_at,
|
||||||
i.start_time,
|
i.start_time,
|
||||||
i.end_time,
|
i.end_time,
|
||||||
i.location,
|
i.location,
|
||||||
|
@ -145,6 +149,7 @@ func (q *Queries) GetIncident(ctx context.Context, id uuid.UUID) (Incident, erro
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Owner,
|
&i.Owner,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
|
&i.CreatedAt,
|
||||||
&i.StartTime,
|
&i.StartTime,
|
||||||
&i.EndTime,
|
&i.EndTime,
|
||||||
&i.Location,
|
&i.Location,
|
||||||
|
@ -206,16 +211,16 @@ ORDER BY ic.call_date ASC
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetIncidentCallsRow struct {
|
type GetIncidentCallsRow struct {
|
||||||
CallID uuid.UUID `json:"call_id"`
|
CallID uuid.UUID `json:"callId"`
|
||||||
CallDate pgtype.Timestamptz `json:"call_date"`
|
CallDate pgtype.Timestamptz `json:"callDate"`
|
||||||
Duration *int32 `json:"duration"`
|
Duration *int32 `json:"duration"`
|
||||||
SystemID int `json:"system_id"`
|
SystemID int `json:"systemId"`
|
||||||
TGID int `json:"tgid"`
|
TGID int `json:"tgid"`
|
||||||
Notes []byte `json:"notes"`
|
Notes []byte `json:"notes"`
|
||||||
Submitter *int32 `json:"submitter"`
|
Submitter *int32 `json:"submitter"`
|
||||||
AudioName *string `json:"audio_name"`
|
AudioName *string `json:"audioName"`
|
||||||
AudioType *string `json:"audio_type"`
|
AudioType *string `json:"audioType"`
|
||||||
AudioUrl *string `json:"audio_url"`
|
AudioUrl *string `json:"audioUrl"`
|
||||||
Frequency int `json:"frequency"`
|
Frequency int `json:"frequency"`
|
||||||
Frequencies []int `json:"frequencies"`
|
Frequencies []int `json:"frequencies"`
|
||||||
Patches []int `json:"patches"`
|
Patches []int `json:"patches"`
|
||||||
|
@ -297,6 +302,7 @@ SELECT
|
||||||
i.name,
|
i.name,
|
||||||
i.owner,
|
i.owner,
|
||||||
i.description,
|
i.description,
|
||||||
|
i.created_at,
|
||||||
i.start_time,
|
i.start_time,
|
||||||
i.end_time,
|
i.end_time,
|
||||||
i.location,
|
i.location,
|
||||||
|
@ -327,7 +333,7 @@ type ListIncidentsPParams struct {
|
||||||
Filter *string `json:"filter"`
|
Filter *string `json:"filter"`
|
||||||
Direction string `json:"direction"`
|
Direction string `json:"direction"`
|
||||||
Offset int32 `json:"offset"`
|
Offset int32 `json:"offset"`
|
||||||
PerPage int32 `json:"per_page"`
|
PerPage int32 `json:"perPage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListIncidentsPRow struct {
|
type ListIncidentsPRow struct {
|
||||||
|
@ -335,11 +341,12 @@ type ListIncidentsPRow struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Owner int `json:"owner"`
|
Owner int `json:"owner"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
StartTime pgtype.Timestamptz `json:"start_time"`
|
CreatedAt pgtype.Timestamptz `json:"createdAt"`
|
||||||
EndTime pgtype.Timestamptz `json:"end_time"`
|
StartTime pgtype.Timestamptz `json:"startTime"`
|
||||||
|
EndTime pgtype.Timestamptz `json:"endTime"`
|
||||||
Location []byte `json:"location"`
|
Location []byte `json:"location"`
|
||||||
Metadata jsontypes.Metadata `json:"metadata"`
|
Metadata jsontypes.Metadata `json:"metadata"`
|
||||||
CallsCount int64 `json:"calls_count"`
|
CallsCount int64 `json:"callsCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListIncidentsP(ctx context.Context, arg ListIncidentsPParams) ([]ListIncidentsPRow, error) {
|
func (q *Queries) ListIncidentsP(ctx context.Context, arg ListIncidentsPParams) ([]ListIncidentsPRow, error) {
|
||||||
|
@ -363,6 +370,7 @@ func (q *Queries) ListIncidentsP(ctx context.Context, arg ListIncidentsPParams)
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Owner,
|
&i.Owner,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
|
&i.CreatedAt,
|
||||||
&i.StartTime,
|
&i.StartTime,
|
||||||
&i.EndTime,
|
&i.EndTime,
|
||||||
&i.Location,
|
&i.Location,
|
||||||
|
@ -411,14 +419,14 @@ SET
|
||||||
metadata = COALESCE($6, metadata)
|
metadata = COALESCE($6, metadata)
|
||||||
WHERE
|
WHERE
|
||||||
id = $7
|
id = $7
|
||||||
RETURNING id, name, owner, description, start_time, end_time, location, metadata
|
RETURNING id, name, owner, description, created_at, start_time, end_time, location, metadata
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateIncidentParams struct {
|
type UpdateIncidentParams struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
StartTime pgtype.Timestamptz `json:"start_time"`
|
StartTime pgtype.Timestamptz `json:"startTime"`
|
||||||
EndTime pgtype.Timestamptz `json:"end_time"`
|
EndTime pgtype.Timestamptz `json:"endTime"`
|
||||||
Location []byte `json:"location"`
|
Location []byte `json:"location"`
|
||||||
Metadata jsontypes.Metadata `json:"metadata"`
|
Metadata jsontypes.Metadata `json:"metadata"`
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
|
@ -440,6 +448,7 @@ func (q *Queries) UpdateIncident(ctx context.Context, arg UpdateIncidentParams)
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Owner,
|
&i.Owner,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
|
&i.CreatedAt,
|
||||||
&i.StartTime,
|
&i.StartTime,
|
||||||
&i.EndTime,
|
&i.EndTime,
|
||||||
&i.Location,
|
&i.Location,
|
||||||
|
|
|
@ -1924,6 +1924,122 @@ func (_c *Store_GetShare_Call) RunAndReturn(run func(context.Context, string) (d
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSharesP provides a mock function with given fields: ctx, arg
|
||||||
|
func (_m *Store) GetSharesP(ctx context.Context, arg database.GetSharesPParams) ([]database.GetSharesPRow, error) {
|
||||||
|
ret := _m.Called(ctx, arg)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetSharesP")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 []database.GetSharesPRow
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, database.GetSharesPParams) ([]database.GetSharesPRow, error)); ok {
|
||||||
|
return rf(ctx, arg)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, database.GetSharesPParams) []database.GetSharesPRow); ok {
|
||||||
|
r0 = rf(ctx, arg)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]database.GetSharesPRow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, database.GetSharesPParams) error); ok {
|
||||||
|
r1 = rf(ctx, arg)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store_GetSharesP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSharesP'
|
||||||
|
type Store_GetSharesP_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSharesP is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - arg database.GetSharesPParams
|
||||||
|
func (_e *Store_Expecter) GetSharesP(ctx interface{}, arg interface{}) *Store_GetSharesP_Call {
|
||||||
|
return &Store_GetSharesP_Call{Call: _e.mock.On("GetSharesP", ctx, arg)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetSharesP_Call) Run(run func(ctx context.Context, arg database.GetSharesPParams)) *Store_GetSharesP_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(database.GetSharesPParams))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetSharesP_Call) Return(_a0 []database.GetSharesPRow, _a1 error) *Store_GetSharesP_Call {
|
||||||
|
_c.Call.Return(_a0, _a1)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetSharesP_Call) RunAndReturn(run func(context.Context, database.GetSharesPParams) ([]database.GetSharesPRow, error)) *Store_GetSharesP_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSharesPCount provides a mock function with given fields: ctx, owner
|
||||||
|
func (_m *Store) GetSharesPCount(ctx context.Context, owner *int32) (int64, error) {
|
||||||
|
ret := _m.Called(ctx, owner)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetSharesPCount")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 int64
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *int32) (int64, error)); ok {
|
||||||
|
return rf(ctx, owner)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *int32) int64); ok {
|
||||||
|
r0 = rf(ctx, owner)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *int32) error); ok {
|
||||||
|
r1 = rf(ctx, owner)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store_GetSharesPCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSharesPCount'
|
||||||
|
type Store_GetSharesPCount_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSharesPCount is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - owner *int32
|
||||||
|
func (_e *Store_Expecter) GetSharesPCount(ctx interface{}, owner interface{}) *Store_GetSharesPCount_Call {
|
||||||
|
return &Store_GetSharesPCount_Call{Call: _e.mock.On("GetSharesPCount", ctx, owner)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetSharesPCount_Call) Run(run func(ctx context.Context, owner *int32)) *Store_GetSharesPCount_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(*int32))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetSharesPCount_Call) Return(_a0 int64, _a1 error) *Store_GetSharesPCount_Call {
|
||||||
|
_c.Call.Return(_a0, _a1)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetSharesPCount_Call) RunAndReturn(run func(context.Context, *int32) (int64, error)) *Store_GetSharesPCount_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// GetSystemName provides a mock function with given fields: ctx, systemID
|
// GetSystemName provides a mock function with given fields: ctx, systemID
|
||||||
func (_m *Store) GetSystemName(ctx context.Context, systemID int) (string, error) {
|
func (_m *Store) GetSystemName(ctx context.Context, systemID int) (string, error) {
|
||||||
ret := _m.Called(ctx, systemID)
|
ret := _m.Called(ctx, systemID)
|
||||||
|
|
|
@ -17,10 +17,10 @@ type Alert struct {
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
Time pgtype.Timestamptz `json:"time,omitempty"`
|
Time pgtype.Timestamptz `json:"time,omitempty"`
|
||||||
TGID int `json:"tgid,omitempty"`
|
TGID int `json:"tgid,omitempty"`
|
||||||
SystemID int `json:"system_id,omitempty"`
|
SystemID int `json:"systemId,omitempty"`
|
||||||
Weight *float32 `json:"weight,omitempty"`
|
Weight *float32 `json:"weight,omitempty"`
|
||||||
Score *float32 `json:"score,omitempty"`
|
Score *float32 `json:"score,omitempty"`
|
||||||
OrigScore *float32 `json:"orig_score,omitempty"`
|
OrigScore *float32 `json:"origScore,omitempty"`
|
||||||
Notified bool `json:"notified,omitempty"`
|
Notified bool `json:"notified,omitempty"`
|
||||||
Metadata []byte `json:"metadata,omitempty"`
|
Metadata []byte `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -28,10 +28,10 @@ type Alert struct {
|
||||||
type ApiKey struct {
|
type ApiKey struct {
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
Owner int `json:"owner,omitempty"`
|
Owner int `json:"owner,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||||
Expires pgtype.Timestamp `json:"expires,omitempty"`
|
Expires pgtype.Timestamp `json:"expires,omitempty"`
|
||||||
Disabled *bool `json:"disabled,omitempty"`
|
Disabled *bool `json:"disabled,omitempty"`
|
||||||
ApiKey string `json:"api_key,omitempty"`
|
ApiKey string `json:"apiKey,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Call struct {
|
type Call struct {
|
||||||
|
@ -39,18 +39,18 @@ type Call struct {
|
||||||
Submitter *int32 `json:"submitter,omitempty"`
|
Submitter *int32 `json:"submitter,omitempty"`
|
||||||
System int `json:"system,omitempty"`
|
System int `json:"system,omitempty"`
|
||||||
Talkgroup int `json:"talkgroup,omitempty"`
|
Talkgroup int `json:"talkgroup,omitempty"`
|
||||||
CallDate pgtype.Timestamptz `json:"call_date,omitempty"`
|
CallDate pgtype.Timestamptz `json:"callDate,omitempty"`
|
||||||
AudioName *string `json:"audio_name,omitempty"`
|
AudioName *string `json:"audioName,omitempty"`
|
||||||
AudioBlob []byte `json:"audio_blob,omitempty"`
|
AudioBlob []byte `json:"audioBlob,omitempty"`
|
||||||
Duration *int32 `json:"duration,omitempty"`
|
Duration *int32 `json:"duration,omitempty"`
|
||||||
AudioType *string `json:"audio_type,omitempty"`
|
AudioType *string `json:"audioType,omitempty"`
|
||||||
AudioUrl *string `json:"audio_url,omitempty"`
|
AudioUrl *string `json:"audioUrl,omitempty"`
|
||||||
Frequency int `json:"frequency,omitempty"`
|
Frequency int `json:"frequency,omitempty"`
|
||||||
Frequencies []int `json:"frequencies,omitempty"`
|
Frequencies []int `json:"frequencies,omitempty"`
|
||||||
Patches []int `json:"patches,omitempty"`
|
Patches []int `json:"patches,omitempty"`
|
||||||
TGLabel *string `json:"tg_label,omitempty"`
|
TGLabel *string `json:"tgLabel,omitempty"`
|
||||||
TGAlphaTag *string `json:"tg_alpha_tag,omitempty"`
|
TGAlphaTag *string `json:"tgAlphaTag,omitempty"`
|
||||||
TGGroup *string `json:"tg_group,omitempty"`
|
TGGroup *string `json:"tgGroup,omitempty"`
|
||||||
Source int `json:"source,omitempty"`
|
Source int `json:"source,omitempty"`
|
||||||
Transcript *string `json:"transcript,omitempty"`
|
Transcript *string `json:"transcript,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -60,32 +60,33 @@ type Incident struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Owner int `json:"owner,omitempty"`
|
Owner int `json:"owner,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
StartTime pgtype.Timestamptz `json:"start_time,omitempty"`
|
CreatedAt pgtype.Timestamptz `json:"createdAt,omitempty"`
|
||||||
EndTime pgtype.Timestamptz `json:"end_time,omitempty"`
|
StartTime pgtype.Timestamptz `json:"startTime,omitempty"`
|
||||||
|
EndTime pgtype.Timestamptz `json:"endTime,omitempty"`
|
||||||
Location []byte `json:"location,omitempty"`
|
Location []byte `json:"location,omitempty"`
|
||||||
Metadata jsontypes.Metadata `json:"metadata,omitempty"`
|
Metadata jsontypes.Metadata `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IncidentsCall struct {
|
type IncidentsCall struct {
|
||||||
IncidentID uuid.UUID `json:"incident_id,omitempty"`
|
IncidentID uuid.UUID `json:"incidentId,omitempty"`
|
||||||
CallID uuid.UUID `json:"call_id,omitempty"`
|
CallID uuid.UUID `json:"callId,omitempty"`
|
||||||
CallsTblID pgtype.UUID `json:"calls_tbl_id,omitempty"`
|
CallsTblID pgtype.UUID `json:"callsTblId,omitempty"`
|
||||||
SweptCallID pgtype.UUID `json:"swept_call_id,omitempty"`
|
SweptCallID pgtype.UUID `json:"sweptCallId,omitempty"`
|
||||||
CallDate pgtype.Timestamptz `json:"call_date,omitempty"`
|
CallDate pgtype.Timestamptz `json:"callDate,omitempty"`
|
||||||
Notes []byte `json:"notes,omitempty"`
|
Notes []byte `json:"notes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Setting struct {
|
type Setting struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
UpdatedBy *int32 `json:"updated_by,omitempty"`
|
UpdatedBy *int32 `json:"updatedBy,omitempty"`
|
||||||
Value []byte `json:"value,omitempty"`
|
Value []byte `json:"value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Share struct {
|
type Share struct {
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
EntityType string `json:"entity_type,omitempty"`
|
EntityType string `json:"entityType,omitempty"`
|
||||||
EntityID uuid.UUID `json:"entity_id,omitempty"`
|
EntityID uuid.UUID `json:"entityId,omitempty"`
|
||||||
EntityDate pgtype.Timestamptz `json:"entity_date,omitempty"`
|
EntityDate pgtype.Timestamptz `json:"entityDate,omitempty"`
|
||||||
Owner int `json:"owner,omitempty"`
|
Owner int `json:"owner,omitempty"`
|
||||||
Expiration pgtype.Timestamptz `json:"expiration,omitempty"`
|
Expiration pgtype.Timestamptz `json:"expiration,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -95,18 +96,18 @@ type SweptCall struct {
|
||||||
Submitter *int32 `json:"submitter,omitempty"`
|
Submitter *int32 `json:"submitter,omitempty"`
|
||||||
System int `json:"system,omitempty"`
|
System int `json:"system,omitempty"`
|
||||||
Talkgroup int `json:"talkgroup,omitempty"`
|
Talkgroup int `json:"talkgroup,omitempty"`
|
||||||
CallDate pgtype.Timestamptz `json:"call_date,omitempty"`
|
CallDate pgtype.Timestamptz `json:"callDate,omitempty"`
|
||||||
AudioName *string `json:"audio_name,omitempty"`
|
AudioName *string `json:"audioName,omitempty"`
|
||||||
AudioBlob []byte `json:"audio_blob,omitempty"`
|
AudioBlob []byte `json:"audioBlob,omitempty"`
|
||||||
Duration *int32 `json:"duration,omitempty"`
|
Duration *int32 `json:"duration,omitempty"`
|
||||||
AudioType *string `json:"audio_type,omitempty"`
|
AudioType *string `json:"audioType,omitempty"`
|
||||||
AudioUrl *string `json:"audio_url,omitempty"`
|
AudioUrl *string `json:"audioUrl,omitempty"`
|
||||||
Frequency int `json:"frequency,omitempty"`
|
Frequency int `json:"frequency,omitempty"`
|
||||||
Frequencies []int `json:"frequencies,omitempty"`
|
Frequencies []int `json:"frequencies,omitempty"`
|
||||||
Patches []int `json:"patches,omitempty"`
|
Patches []int `json:"patches,omitempty"`
|
||||||
TGLabel *string `json:"tg_label,omitempty"`
|
TGLabel *string `json:"tgLabel,omitempty"`
|
||||||
TGAlphaTag *string `json:"tg_alpha_tag,omitempty"`
|
TGAlphaTag *string `json:"tgAlphaTag,omitempty"`
|
||||||
TGGroup *string `json:"tg_group,omitempty"`
|
TGGroup *string `json:"tgGroup,omitempty"`
|
||||||
Source int `json:"source,omitempty"`
|
Source int `json:"source,omitempty"`
|
||||||
Transcript *string `json:"transcript,omitempty"`
|
Transcript *string `json:"transcript,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -118,16 +119,16 @@ type System struct {
|
||||||
|
|
||||||
type Talkgroup struct {
|
type Talkgroup struct {
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
SystemID int32 `json:"system_id,omitempty"`
|
SystemID int32 `json:"systemId,omitempty"`
|
||||||
TGID int32 `json:"tgid,omitempty"`
|
TGID int32 `json:"tgid,omitempty"`
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
AlphaTag *string `json:"alpha_tag,omitempty"`
|
AlphaTag *string `json:"alphaTag,omitempty"`
|
||||||
TGGroup *string `json:"tg_group,omitempty"`
|
TGGroup *string `json:"tgGroup,omitempty"`
|
||||||
Frequency *int32 `json:"frequency,omitempty"`
|
Frequency *int32 `json:"frequency,omitempty"`
|
||||||
Metadata jsontypes.Metadata `json:"metadata,omitempty"`
|
Metadata jsontypes.Metadata `json:"metadata,omitempty"`
|
||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
Alert bool `json:"alert,omitempty"`
|
Alert bool `json:"alert,omitempty"`
|
||||||
AlertRules rules.AlertRules `json:"alert_rules,omitempty"`
|
AlertRules rules.AlertRules `json:"alertRules,omitempty"`
|
||||||
Weight float32 `json:"weight,omitempty"`
|
Weight float32 `json:"weight,omitempty"`
|
||||||
Learned bool `json:"learned,omitempty"`
|
Learned bool `json:"learned,omitempty"`
|
||||||
Ignored bool `json:"ignored,omitempty"`
|
Ignored bool `json:"ignored,omitempty"`
|
||||||
|
@ -136,18 +137,18 @@ type Talkgroup struct {
|
||||||
type TalkgroupVersion struct {
|
type TalkgroupVersion struct {
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
Time pgtype.Timestamptz `json:"time,omitempty"`
|
Time pgtype.Timestamptz `json:"time,omitempty"`
|
||||||
CreatedBy *int32 `json:"created_by,omitempty"`
|
CreatedBy *int32 `json:"createdBy,omitempty"`
|
||||||
Deleted *bool `json:"deleted,omitempty"`
|
Deleted *bool `json:"deleted,omitempty"`
|
||||||
SystemID *int32 `json:"system_id,omitempty"`
|
SystemID *int32 `json:"systemId,omitempty"`
|
||||||
TGID *int32 `json:"tgid,omitempty"`
|
TGID *int32 `json:"tgid,omitempty"`
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
AlphaTag *string `json:"alpha_tag,omitempty"`
|
AlphaTag *string `json:"alphaTag,omitempty"`
|
||||||
TGGroup *string `json:"tg_group,omitempty"`
|
TGGroup *string `json:"tgGroup,omitempty"`
|
||||||
Frequency *int32 `json:"frequency,omitempty"`
|
Frequency *int32 `json:"frequency,omitempty"`
|
||||||
Metadata []byte `json:"metadata,omitempty"`
|
Metadata []byte `json:"metadata,omitempty"`
|
||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
Alert *bool `json:"alert,omitempty"`
|
Alert *bool `json:"alert,omitempty"`
|
||||||
AlertRules []byte `json:"alert_rules,omitempty"`
|
AlertRules []byte `json:"alertRules,omitempty"`
|
||||||
Weight *float32 `json:"weight,omitempty"`
|
Weight *float32 `json:"weight,omitempty"`
|
||||||
Learned *bool `json:"learned,omitempty"`
|
Learned *bool `json:"learned,omitempty"`
|
||||||
Ignored *bool `json:"ignored,omitempty"`
|
Ignored *bool `json:"ignored,omitempty"`
|
||||||
|
@ -158,6 +159,6 @@ type User struct {
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
IsAdmin bool `json:"is_admin,omitempty"`
|
IsAdmin bool `json:"isAdmin,omitempty"`
|
||||||
Prefs []byte `json:"prefs,omitempty"`
|
Prefs []byte `json:"prefs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,8 @@ type Querier interface {
|
||||||
GetIncidentOwner(ctx context.Context, id uuid.UUID) (int, error)
|
GetIncidentOwner(ctx context.Context, id uuid.UUID) (int, error)
|
||||||
GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]GetIncidentTalkgroupsRow, error)
|
GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]GetIncidentTalkgroupsRow, error)
|
||||||
GetShare(ctx context.Context, id string) (Share, error)
|
GetShare(ctx context.Context, id string) (Share, error)
|
||||||
|
GetSharesP(ctx context.Context, arg GetSharesPParams) ([]GetSharesPRow, error)
|
||||||
|
GetSharesPCount(ctx context.Context, owner *int32) (int64, error)
|
||||||
GetSystemName(ctx context.Context, systemID int) (string, error)
|
GetSystemName(ctx context.Context, systemID int) (string, error)
|
||||||
GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error)
|
GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error)
|
||||||
GetTalkgroupIDsByTags(ctx context.Context, anyTags []string, allTags []string, notTags []string) ([]GetTalkgroupIDsByTagsRow, error)
|
GetTalkgroupIDsByTags(ctx context.Context, anyTags []string, allTags []string, notTags []string) ([]GetTalkgroupIDsByTagsRow, error)
|
||||||
|
|
|
@ -25,9 +25,9 @@ INSERT INTO shares (
|
||||||
|
|
||||||
type CreateShareParams struct {
|
type CreateShareParams struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
EntityType string `json:"entity_type"`
|
EntityType string `json:"entityType"`
|
||||||
EntityID uuid.UUID `json:"entity_id"`
|
EntityID uuid.UUID `json:"entityId"`
|
||||||
EntityDate pgtype.Timestamptz `json:"entity_date"`
|
EntityDate pgtype.Timestamptz `json:"entityDate"`
|
||||||
Owner int `json:"owner"`
|
Owner int `json:"owner"`
|
||||||
Expiration pgtype.Timestamptz `json:"expiration"`
|
Expiration pgtype.Timestamptz `json:"expiration"`
|
||||||
}
|
}
|
||||||
|
@ -55,14 +55,14 @@ func (q *Queries) DeleteShare(ctx context.Context, id string) error {
|
||||||
|
|
||||||
const getShare = `-- name: GetShare :one
|
const getShare = `-- name: GetShare :one
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
s.id,
|
||||||
entity_type,
|
s.entity_type,
|
||||||
entity_id,
|
s.entity_id,
|
||||||
entity_date,
|
s.entity_date,
|
||||||
owner,
|
s.owner,
|
||||||
expiration
|
s.expiration
|
||||||
FROM shares
|
FROM shares s
|
||||||
WHERE id = $1
|
WHERE s.id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetShare(ctx context.Context, id string) (Share, error) {
|
func (q *Queries) GetShare(ctx context.Context, id string) (Share, error) {
|
||||||
|
@ -79,6 +79,82 @@ func (q *Queries) GetShare(ctx context.Context, id string) (Share, error) {
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSharesP = `-- name: GetSharesP :many
|
||||||
|
SELECT
|
||||||
|
s.id, s.entity_type, s.entity_id, s.entity_date, s.owner, s.expiration,
|
||||||
|
u.username
|
||||||
|
FROM shares s
|
||||||
|
JOIN users u ON (s.owner = u.id)
|
||||||
|
WHERE
|
||||||
|
CASE WHEN $1::INTEGER IS NOT NULL THEN
|
||||||
|
s.owner = $1 ELSE TRUE END
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN $2::TEXT = 'asc' THEN s.entity_date END ASC,
|
||||||
|
CASE WHEN $2::TEXT = 'desc' THEN s.entity_date END DESC
|
||||||
|
OFFSET $3 ROWS
|
||||||
|
FETCH NEXT $4 ROWS ONLY
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetSharesPParams struct {
|
||||||
|
Owner *int32 `json:"owner"`
|
||||||
|
Direction string `json:"direction"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
PerPage int32 `json:"perPage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetSharesPRow struct {
|
||||||
|
Share Share `json:"share"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetSharesP(ctx context.Context, arg GetSharesPParams) ([]GetSharesPRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getSharesP,
|
||||||
|
arg.Owner,
|
||||||
|
arg.Direction,
|
||||||
|
arg.Offset,
|
||||||
|
arg.PerPage,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetSharesPRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetSharesPRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.Share.ID,
|
||||||
|
&i.Share.EntityType,
|
||||||
|
&i.Share.EntityID,
|
||||||
|
&i.Share.EntityDate,
|
||||||
|
&i.Share.Owner,
|
||||||
|
&i.Share.Expiration,
|
||||||
|
&i.Username,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSharesPCount = `-- name: GetSharesPCount :one
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM shares s
|
||||||
|
WHERE
|
||||||
|
CASE WHEN $1::INTEGER IS NOT NULL THEN
|
||||||
|
s.owner = $1 ELSE TRUE END
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetSharesPCount(ctx context.Context, owner *int32) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getSharesPCount, owner)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
const pruneShares = `-- name: PruneShares :exec
|
const pruneShares = `-- name: PruneShares :exec
|
||||||
DELETE FROM shares WHERE expiration < NOW()
|
DELETE FROM shares WHERE expiration < NOW()
|
||||||
`
|
`
|
||||||
|
|
|
@ -32,11 +32,11 @@ INSERT INTO talkgroups(
|
||||||
`
|
`
|
||||||
|
|
||||||
type AddLearnedTalkgroupParams struct {
|
type AddLearnedTalkgroupParams struct {
|
||||||
SystemID int32 `json:"system_id"`
|
SystemID int32 `json:"systemId"`
|
||||||
TGID int32 `json:"tgid"`
|
TGID int32 `json:"tgid"`
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
AlphaTag *string `json:"alpha_tag"`
|
AlphaTag *string `json:"alphaTag"`
|
||||||
TGGroup *string `json:"tg_group"`
|
TGGroup *string `json:"tgGroup"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AddLearnedTalkgroup(ctx context.Context, arg AddLearnedTalkgroupParams) (Talkgroup, error) {
|
func (q *Queries) AddLearnedTalkgroup(ctx context.Context, arg AddLearnedTalkgroupParams) (Talkgroup, error) {
|
||||||
|
@ -202,7 +202,7 @@ AND NOT (tags @> ARRAY[$3])
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetTalkgroupIDsByTagsRow struct {
|
type GetTalkgroupIDsByTagsRow struct {
|
||||||
SystemID int32 `json:"system_id"`
|
SystemID int32 `json:"systemId"`
|
||||||
TGID int32 `json:"tgid"`
|
TGID int32 `json:"tgid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -511,9 +511,9 @@ FETCH NEXT $5 ROWS ONLY
|
||||||
type GetTalkgroupsWithLearnedBySystemPParams struct {
|
type GetTalkgroupsWithLearnedBySystemPParams struct {
|
||||||
System int32 `json:"system"`
|
System int32 `json:"system"`
|
||||||
Filter *string `json:"filter"`
|
Filter *string `json:"filter"`
|
||||||
OrderBy string `json:"order_by"`
|
OrderBy string `json:"orderBy"`
|
||||||
Offset int32 `json:"offset"`
|
Offset int32 `json:"offset"`
|
||||||
PerPage int32 `json:"per_page"`
|
PerPage int32 `json:"perPage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetTalkgroupsWithLearnedBySystemPRow struct {
|
type GetTalkgroupsWithLearnedBySystemPRow struct {
|
||||||
|
@ -611,9 +611,9 @@ FETCH NEXT $4 ROWS ONLY
|
||||||
|
|
||||||
type GetTalkgroupsWithLearnedPParams struct {
|
type GetTalkgroupsWithLearnedPParams struct {
|
||||||
Filter *string `json:"filter"`
|
Filter *string `json:"filter"`
|
||||||
OrderBy string `json:"order_by"`
|
OrderBy string `json:"orderBy"`
|
||||||
Offset int32 `json:"offset"`
|
Offset int32 `json:"offset"`
|
||||||
PerPage int32 `json:"per_page"`
|
PerPage int32 `json:"perPage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetTalkgroupsWithLearnedPRow struct {
|
type GetTalkgroupsWithLearnedPRow struct {
|
||||||
|
@ -774,17 +774,17 @@ RETURNING id, system_id, tgid, name, alpha_tag, tg_group, frequency, metadata, t
|
||||||
|
|
||||||
type UpdateTalkgroupParams struct {
|
type UpdateTalkgroupParams struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
AlphaTag *string `json:"alpha_tag"`
|
AlphaTag *string `json:"alphaTag"`
|
||||||
TGGroup *string `json:"tg_group"`
|
TGGroup *string `json:"tgGroup"`
|
||||||
Frequency *int32 `json:"frequency"`
|
Frequency *int32 `json:"frequency"`
|
||||||
Metadata jsontypes.Metadata `json:"metadata"`
|
Metadata jsontypes.Metadata `json:"metadata"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Alert *bool `json:"alert"`
|
Alert *bool `json:"alert"`
|
||||||
AlertRules rules.AlertRules `json:"alert_rules"`
|
AlertRules rules.AlertRules `json:"alertRules"`
|
||||||
Weight *float32 `json:"weight"`
|
Weight *float32 `json:"weight"`
|
||||||
Learned *bool `json:"learned"`
|
Learned *bool `json:"learned"`
|
||||||
ID *int32 `json:"id"`
|
ID *int32 `json:"id"`
|
||||||
SystemID *int32 `json:"system_id"`
|
SystemID *int32 `json:"systemId"`
|
||||||
TGID *int32 `json:"tgid"`
|
TGID *int32 `json:"tgid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ type CreateUserParams struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
||||||
|
@ -108,10 +108,10 @@ WHERE api_key = $1
|
||||||
type GetAPIKeyRow struct {
|
type GetAPIKeyRow struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Owner int `json:"owner"`
|
Owner int `json:"owner"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
Expires pgtype.Timestamp `json:"expires"`
|
Expires pgtype.Timestamp `json:"expires"`
|
||||||
Disabled *bool `json:"disabled"`
|
Disabled *bool `json:"disabled"`
|
||||||
ApiKey string `json:"api_key"`
|
ApiKey string `json:"apiKey"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,11 @@ package incidents
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/common"
|
||||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||||
"dynatron.me/x/stillbox/pkg/calls"
|
"dynatron.me/x/stillbox/pkg/calls"
|
||||||
"dynatron.me/x/stillbox/pkg/rbac/entities"
|
"dynatron.me/x/stillbox/pkg/rbac/entities"
|
||||||
|
@ -14,19 +18,35 @@ type Incident struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Owner users.UserID `json:"owner"`
|
Owner users.UserID `json:"owner"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description,omitempty"`
|
||||||
StartTime *jsontypes.Time `json:"startTime"`
|
CreatedAt jsontypes.Time `json:"createdAt"`
|
||||||
EndTime *jsontypes.Time `json:"endTime"`
|
StartTime *jsontypes.Time `json:"startTime,omitempty"`
|
||||||
Location jsontypes.Location `json:"location"`
|
EndTime *jsontypes.Time `json:"endTime,omitempty"`
|
||||||
Metadata jsontypes.Metadata `json:"metadata"`
|
Location jsontypes.Location `json:"location,omitempty"`
|
||||||
|
Metadata jsontypes.Metadata `json:"metadata,omitempty"`
|
||||||
Calls []IncidentCall `json:"calls"`
|
Calls []IncidentCall `json:"calls"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (inc *Incident) SetShareURL(bu url.URL, shareID string) {
|
||||||
|
bu.Path = fmt.Sprintf("/share/%s/call/", shareID)
|
||||||
|
for i := range inc.Calls {
|
||||||
|
if inc.Calls[i].AudioURL != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inc.Calls[i].AudioURL = common.PtrTo(bu.String() + inc.Calls[i].ID.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (inc *Incident) GetResourceName() string {
|
func (inc *Incident) GetResourceName() string {
|
||||||
return entities.ResourceIncident
|
return entities.ResourceIncident
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (inc *Incident) PlaylistFilename() string {
|
||||||
|
rep := strings.NewReplacer(" ", "_", "/", "_", ":", "_")
|
||||||
|
return rep.Replace(strings.ToLower(inc.Name))
|
||||||
|
}
|
||||||
|
|
||||||
type IncidentCall struct {
|
type IncidentCall struct {
|
||||||
calls.Call
|
calls.Call
|
||||||
Notes json.RawMessage `json:"notes"`
|
Notes json.RawMessage `json:"notes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ type Store interface {
|
||||||
// CallIn returns whether an incident is in an call
|
// CallIn returns whether an incident is in an call
|
||||||
CallIn(ctx context.Context, inc uuid.UUID, call uuid.UUID) (bool, error)
|
CallIn(ctx context.Context, inc uuid.UUID, call uuid.UUID) (bool, error)
|
||||||
|
|
||||||
// TGsIn returns the talkgroups referenced by an incident as a map, primary for rbac use.
|
// TGsIn returns the talkgroups referenced by an incident as a map, primarily for rbac use.
|
||||||
TGsIn(ctx context.Context, inc uuid.UUID) (talkgroups.PresenceMap, error)
|
TGsIn(ctx context.Context, inc uuid.UUID) (talkgroups.PresenceMap, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,6 +231,7 @@ func fromDBIncident(id uuid.UUID, d database.Incident) incidents.Incident {
|
||||||
Owner: users.UserID(d.Owner),
|
Owner: users.UserID(d.Owner),
|
||||||
Name: d.Name,
|
Name: d.Name,
|
||||||
Description: d.Description,
|
Description: d.Description,
|
||||||
|
CreatedAt: jsontypes.Time(d.CreatedAt.Time),
|
||||||
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
||||||
EndTime: jsontypes.TimePtrFromTSTZ(d.EndTime),
|
EndTime: jsontypes.TimePtrFromTSTZ(d.EndTime),
|
||||||
Metadata: d.Metadata,
|
Metadata: d.Metadata,
|
||||||
|
@ -250,6 +251,7 @@ func fromDBListInPRow(id uuid.UUID, d database.ListIncidentsPRow) Incident {
|
||||||
Owner: users.UserID(d.Owner),
|
Owner: users.UserID(d.Owner),
|
||||||
Name: d.Name,
|
Name: d.Name,
|
||||||
Description: d.Description,
|
Description: d.Description,
|
||||||
|
CreatedAt: jsontypes.Time(d.CreatedAt.Time),
|
||||||
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
||||||
EndTime: jsontypes.TimePtrFromTSTZ(d.EndTime),
|
EndTime: jsontypes.TimePtrFromTSTZ(d.EndTime),
|
||||||
Metadata: d.Metadata,
|
Metadata: d.Metadata,
|
||||||
|
@ -268,6 +270,7 @@ func fromDBCalls(d []database.GetIncidentCallsRow) []incidents.IncidentCall {
|
||||||
ID: v.CallID,
|
ID: v.CallID,
|
||||||
AudioName: common.ZeroIfNil(v.AudioName),
|
AudioName: common.ZeroIfNil(v.AudioName),
|
||||||
AudioType: common.ZeroIfNil(v.AudioType),
|
AudioType: common.ZeroIfNil(v.AudioType),
|
||||||
|
AudioURL: v.AudioUrl,
|
||||||
Duration: dur,
|
Duration: dur,
|
||||||
DateTime: v.CallDate.Time,
|
DateTime: v.CallDate.Time,
|
||||||
Frequencies: v.Frequencies,
|
Frequencies: v.Frequencies,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.33.0
|
// protoc-gen-go v1.33.0
|
||||||
// protoc v5.28.3
|
// protoc v5.29.3
|
||||||
// source: stillbox.proto
|
// source: stillbox.proto
|
||||||
|
|
||||||
package pb
|
package pb
|
||||||
|
|
|
@ -2,6 +2,8 @@ package entities
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/el-mike/restrict/v2"
|
"github.com/el-mike/restrict/v2"
|
||||||
)
|
)
|
||||||
|
@ -30,14 +32,15 @@ const (
|
||||||
|
|
||||||
func SubjectFrom(ctx context.Context) Subject {
|
func SubjectFrom(ctx context.Context) Subject {
|
||||||
sub, ok := ctx.Value(SubjectCtxKey).(Subject)
|
sub, ok := ctx.Value(SubjectCtxKey).(Subject)
|
||||||
if ok {
|
if !ok {
|
||||||
|
panic("no subject in context")
|
||||||
|
}
|
||||||
|
|
||||||
return sub
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
return new(PublicSubject)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Subject interface {
|
type Subject interface {
|
||||||
|
fmt.Stringer
|
||||||
restrict.Subject
|
restrict.Subject
|
||||||
GetName() string
|
GetName() string
|
||||||
}
|
}
|
||||||
|
@ -62,10 +65,18 @@ func (s *PublicSubject) GetName() string {
|
||||||
return "PUBLIC:" + s.RemoteAddr
|
return "PUBLIC:" + s.RemoteAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PublicSubject) String() string {
|
||||||
|
return s.GetName()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *PublicSubject) GetRoles() []string {
|
func (s *PublicSubject) GetRoles() []string {
|
||||||
return []string{RolePublic}
|
return []string{RolePublic}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewPublicSubject(r *http.Request) *PublicSubject {
|
||||||
|
return &PublicSubject{RemoteAddr: r.RemoteAddr}
|
||||||
|
}
|
||||||
|
|
||||||
type SystemServiceSubject struct {
|
type SystemServiceSubject struct {
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
@ -74,6 +85,10 @@ func (s *SystemServiceSubject) GetName() string {
|
||||||
return "SYSTEM:" + s.Name
|
return "SYSTEM:" + s.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SystemServiceSubject) String() string {
|
||||||
|
return s.GetName()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SystemServiceSubject) GetRoles() []string {
|
func (s *SystemServiceSubject) GetRoles() []string {
|
||||||
return []string{RoleSystem}
|
return []string{RoleSystem}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,19 +78,29 @@ var Policy = &restrict.PolicyDefinition{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entities.RoleAdmin: {
|
entities.RoleAdmin: {
|
||||||
|
Description: "A superuser",
|
||||||
Parents: []string{entities.RoleUser},
|
Parents: []string{entities.RoleUser},
|
||||||
Grants: restrict.GrantsMap{
|
Grants: restrict.GrantsMap{
|
||||||
entities.ResourceIncident: {
|
entities.ResourceIncident: {
|
||||||
|
&restrict.Permission{Action: entities.ActionRead},
|
||||||
&restrict.Permission{Action: entities.ActionUpdate},
|
&restrict.Permission{Action: entities.ActionUpdate},
|
||||||
&restrict.Permission{Action: entities.ActionDelete},
|
&restrict.Permission{Action: entities.ActionDelete},
|
||||||
&restrict.Permission{Action: entities.ActionShare},
|
&restrict.Permission{Action: entities.ActionShare},
|
||||||
},
|
},
|
||||||
entities.ResourceCall: {
|
entities.ResourceCall: {
|
||||||
|
&restrict.Permission{Action: entities.ActionRead},
|
||||||
&restrict.Permission{Action: entities.ActionUpdate},
|
&restrict.Permission{Action: entities.ActionUpdate},
|
||||||
&restrict.Permission{Action: entities.ActionDelete},
|
&restrict.Permission{Action: entities.ActionDelete},
|
||||||
&restrict.Permission{Action: entities.ActionShare},
|
&restrict.Permission{Action: entities.ActionShare},
|
||||||
},
|
},
|
||||||
entities.ResourceTalkgroup: {
|
entities.ResourceTalkgroup: {
|
||||||
|
&restrict.Permission{Action: entities.ActionRead},
|
||||||
|
&restrict.Permission{Action: entities.ActionUpdate},
|
||||||
|
&restrict.Permission{Action: entities.ActionCreate},
|
||||||
|
&restrict.Permission{Action: entities.ActionDelete},
|
||||||
|
},
|
||||||
|
entities.ResourceShare: {
|
||||||
|
&restrict.Permission{Action: entities.ActionRead},
|
||||||
&restrict.Permission{Action: entities.ActionUpdate},
|
&restrict.Permission{Action: entities.ActionUpdate},
|
||||||
&restrict.Permission{Action: entities.ActionCreate},
|
&restrict.Permission{Action: entities.ActionCreate},
|
||||||
&restrict.Permission{Action: entities.ActionDelete},
|
&restrict.Permission{Action: entities.ActionDelete},
|
||||||
|
@ -98,16 +108,15 @@ var Policy = &restrict.PolicyDefinition{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entities.RoleSystem: {
|
entities.RoleSystem: {
|
||||||
Parents: []string{entities.RoleSystem},
|
Description: "A system service",
|
||||||
|
Parents: []string{entities.RoleAdmin},
|
||||||
},
|
},
|
||||||
entities.RolePublic: {
|
entities.RolePublic: {
|
||||||
/*
|
|
||||||
Grants: restrict.GrantsMap{
|
Grants: restrict.GrantsMap{
|
||||||
entities.ResourceShare: {
|
entities.ResourceShare: {
|
||||||
&restrict.Permission{Action: entities.ActionRead},
|
&restrict.Permission{Action: entities.ActionRead},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
*/
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
PermissionPresets: restrict.PermissionPresets{
|
PermissionPresets: restrict.PermissionPresets{
|
||||||
|
|
|
@ -8,17 +8,23 @@ import (
|
||||||
|
|
||||||
"github.com/el-mike/restrict/v2"
|
"github.com/el-mike/restrict/v2"
|
||||||
"github.com/el-mike/restrict/v2/adapters"
|
"github.com/el-mike/restrict/v2/adapters"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrBadSubject = errors.New("bad subject in token")
|
ErrBadSubject = errors.New("bad subject in token")
|
||||||
|
ErrAccessDenied = errors.New("access denied")
|
||||||
)
|
)
|
||||||
|
|
||||||
func ErrAccessDenied(err error) *restrict.AccessDeniedError {
|
func IsErrAccessDenied(err error) error {
|
||||||
if accessErr, ok := err.(*restrict.AccessDeniedError); ok {
|
if accessErr, ok := err.(*restrict.AccessDeniedError); ok {
|
||||||
return accessErr
|
return accessErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err == ErrAccessDenied {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,5 +121,19 @@ func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOp
|
||||||
Context: o.context,
|
Context: o.context,
|
||||||
}
|
}
|
||||||
|
|
||||||
return sub, r.access.Authorize(req)
|
authRes := r.access.Authorize(req)
|
||||||
|
if IsErrAccessDenied(authRes) != nil {
|
||||||
|
subS := ""
|
||||||
|
resS := ""
|
||||||
|
if sub != nil {
|
||||||
|
subS = sub.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if res != nil {
|
||||||
|
resS = res.GetResourceName()
|
||||||
|
}
|
||||||
|
log.Error().Str("resource", resS).Strs("actions", req.Actions).Str("subject", subS).Msg("access denied")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sub, authRes
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,16 +48,7 @@ func New(baseURL url.URL) *api {
|
||||||
incidents: newIncidentsAPI(&baseURL),
|
incidents: newIncidentsAPI(&baseURL),
|
||||||
users: new(usersAPI),
|
users: new(usersAPI),
|
||||||
}
|
}
|
||||||
s.shares = newShareAPI(&baseURL,
|
s.shares = newShareAPI(&baseURL, s.shareHandlers())
|
||||||
ShareHandlers{
|
|
||||||
ShareRequestCall: s.calls.shareCallRoute,
|
|
||||||
ShareRequestCallDL: s.calls.shareCallDLRoute,
|
|
||||||
ShareRequestIncident: s.incidents.getIncident,
|
|
||||||
ShareRequestIncidentM3U: s.incidents.getCallsM3U,
|
|
||||||
ShareRequestTalkgroups: s.tgs.getTGsShareRoute,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +179,7 @@ func autoError(err error) render.Renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if rbac.ErrAccessDenied(err) != nil {
|
if rbac.IsErrAccessDenied(err) != nil {
|
||||||
return forbiddenErrText(err)
|
return forbiddenErrText(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package rest
|
package rest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
|
@ -11,7 +12,6 @@ import (
|
||||||
"dynatron.me/x/stillbox/internal/forms"
|
"dynatron.me/x/stillbox/internal/forms"
|
||||||
"dynatron.me/x/stillbox/pkg/calls/callstore"
|
"dynatron.me/x/stillbox/pkg/calls/callstore"
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
"dynatron.me/x/stillbox/pkg/shares"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -102,7 +102,12 @@ func (ca *callsAPI) getAudio(p getAudioParams, w http.ResponseWriter, r *http.Re
|
||||||
_, _ = w.Write(call.AudioBlob)
|
_, _ = w.Write(call.AudioBlob)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ca *callsAPI) shareCallRoute(id ID, _ *shares.Share, w http.ResponseWriter, r *http.Request) {
|
func (ca *callsAPI) getCallInfo(ctx context.Context, id ID) (SharedItem, error) {
|
||||||
|
cs := callstore.FromCtx(ctx)
|
||||||
|
return cs.Call(ctx, id.(uuid.UUID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ca *callsAPI) shareCallRoute(id ID, w http.ResponseWriter, r *http.Request) {
|
||||||
p := getAudioParams{
|
p := getAudioParams{
|
||||||
CallID: common.PtrTo(id.(uuid.UUID)),
|
CallID: common.PtrTo(id.(uuid.UUID)),
|
||||||
}
|
}
|
||||||
|
@ -110,7 +115,7 @@ func (ca *callsAPI) shareCallRoute(id ID, _ *shares.Share, w http.ResponseWriter
|
||||||
ca.getAudio(p, w, r)
|
ca.getAudio(p, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ca *callsAPI) shareCallDLRoute(id ID, _ *shares.Share, w http.ResponseWriter, r *http.Request) {
|
func (ca *callsAPI) shareCallDLRoute(id ID, w http.ResponseWriter, r *http.Request) {
|
||||||
p := getAudioParams{
|
p := getAudioParams{
|
||||||
CallID: common.PtrTo(id.(uuid.UUID)),
|
CallID: common.PtrTo(id.(uuid.UUID)),
|
||||||
Download: common.PtrTo("download"),
|
Download: common.PtrTo("download"),
|
||||||
|
|
|
@ -2,6 +2,7 @@ package rest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -12,7 +13,6 @@ import (
|
||||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||||
"dynatron.me/x/stillbox/pkg/incidents"
|
"dynatron.me/x/stillbox/pkg/incidents"
|
||||||
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
||||||
"dynatron.me/x/stillbox/pkg/shares"
|
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
@ -91,22 +91,22 @@ func (ia *incidentsAPI) createIncident(w http.ResponseWriter, r *http.Request) {
|
||||||
func (ia *incidentsAPI) getIncidentRoute(w http.ResponseWriter, r *http.Request) {
|
func (ia *incidentsAPI) getIncidentRoute(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := idOnlyParam(w, r)
|
id, err := idOnlyParam(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ia.getIncident(id, nil, w, r)
|
e, err := ia.getIncident(r.Context(), id)
|
||||||
}
|
|
||||||
|
|
||||||
func (ia *incidentsAPI) getIncident(id ID, share *shares.Share, w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := r.Context()
|
|
||||||
incs := incstore.FromCtx(ctx)
|
|
||||||
inc, err := incs.Incident(ctx, id.(uuid.UUID))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wErr(w, r, autoError(err))
|
wErr(w, r, autoError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
respond(w, r, inc)
|
respond(w, r, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *incidentsAPI) getIncident(ctx context.Context, id ID) (SharedItem, error) {
|
||||||
|
incs := incstore.FromCtx(ctx)
|
||||||
|
return incs.Incident(ctx, id.(uuid.UUID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ia *incidentsAPI) updateIncident(w http.ResponseWriter, r *http.Request) {
|
func (ia *incidentsAPI) updateIncident(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -195,10 +195,10 @@ func (ia *incidentsAPI) getCallsM3URoute(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ia.getCallsM3U(id, nil, w, r)
|
ia.getCallsM3U(id, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ia *incidentsAPI) getCallsM3U(id ID, share *shares.Share, w http.ResponseWriter, r *http.Request) {
|
func (ia *incidentsAPI) getCallsM3U(id ID, w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
incs := incstore.FromCtx(ctx)
|
incs := incstore.FromCtx(ctx)
|
||||||
tgst := tgstore.FromCtx(ctx)
|
tgst := tgstore.FromCtx(ctx)
|
||||||
|
@ -213,9 +213,13 @@ func (ia *incidentsAPI) getCallsM3U(id ID, share *shares.Share, w http.ResponseW
|
||||||
|
|
||||||
callUrl := common.PtrTo(*ia.baseURL)
|
callUrl := common.PtrTo(*ia.baseURL)
|
||||||
urlRoot := "/api/call"
|
urlRoot := "/api/call"
|
||||||
|
filename := inc.PlaylistFilename()
|
||||||
|
share := ShareFrom(ctx)
|
||||||
if share != nil {
|
if share != nil {
|
||||||
urlRoot = fmt.Sprintf("/share/%s/call/", share.ID)
|
urlRoot = fmt.Sprintf("/share/%s/call/", share.ID)
|
||||||
|
filename += "_" + share.ID
|
||||||
}
|
}
|
||||||
|
filename += ".m3u"
|
||||||
|
|
||||||
b.WriteString("#EXTM3U\n\n")
|
b.WriteString("#EXTM3U\n\n")
|
||||||
for _, c := range inc.Calls {
|
for _, c := range inc.Calls {
|
||||||
|
@ -231,11 +235,12 @@ func (ia *incidentsAPI) getCallsM3U(id ID, share *shares.Share, w http.ResponseW
|
||||||
|
|
||||||
callUrl.Path = urlRoot + c.ID.String()
|
callUrl.Path = urlRoot + c.ID.String()
|
||||||
|
|
||||||
fmt.Fprintf(b, "#EXTINF:%d,%s%s (%s)\n%s\n\n",
|
fmt.Fprintf(b, "#EXTINF:%d,%s%s (%s @ %s)\n%s\n\n",
|
||||||
c.Duration.Seconds(),
|
c.Duration.Seconds(),
|
||||||
tg.StringTag(true),
|
tg.StringTag(true),
|
||||||
from,
|
from,
|
||||||
c.DateTime.Format("15:04 01/02"),
|
c.Duration.ColonFormat(),
|
||||||
|
c.DateTime.Format("15:04:05 01/02"),
|
||||||
callUrl,
|
callUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -243,6 +248,7 @@ func (ia *incidentsAPI) getCallsM3U(id ID, share *shares.Share, w http.ResponseW
|
||||||
// Not a lot of agreement on which MIME type to use for non-HLS m3u,
|
// Not a lot of agreement on which MIME type to use for non-HLS m3u,
|
||||||
// let's hope this is good enough
|
// let's hope this is good enough
|
||||||
w.Header().Set("Content-Type", "audio/x-mpegurl")
|
w.Header().Set("Content-Type", "audio/x-mpegurl")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, _ = b.WriteTo(w)
|
_, _ = b.WriteTo(w)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package rest
|
package rest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -23,15 +24,28 @@ type ShareRequestType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ShareRequestCall ShareRequestType = "call"
|
ShareRequestCall ShareRequestType = "call"
|
||||||
|
ShareRequestCallInfo ShareRequestType = "callinfo"
|
||||||
ShareRequestCallDL ShareRequestType = "callDL"
|
ShareRequestCallDL ShareRequestType = "callDL"
|
||||||
ShareRequestIncident ShareRequestType = "incident"
|
ShareRequestIncident ShareRequestType = "incident"
|
||||||
ShareRequestIncidentM3U ShareRequestType = "m3u"
|
ShareRequestIncidentM3U ShareRequestType = "m3u"
|
||||||
ShareRequestTalkgroups ShareRequestType = "talkgroups"
|
ShareRequestTalkgroups ShareRequestType = "talkgroups"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// shareHandlers returns a ShareHandlers map from the api.
|
||||||
|
func (s *api) shareHandlers() ShareHandlers {
|
||||||
|
return ShareHandlers{
|
||||||
|
ShareRequestCall: s.calls.shareCallRoute,
|
||||||
|
ShareRequestCallInfo: s.respondShareHandler(s.calls.getCallInfo),
|
||||||
|
ShareRequestCallDL: s.calls.shareCallDLRoute,
|
||||||
|
ShareRequestIncident: s.respondShareHandler(s.incidents.getIncident),
|
||||||
|
ShareRequestIncidentM3U: s.incidents.getCallsM3U,
|
||||||
|
ShareRequestTalkgroups: s.tgs.getTGsShareRoute,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (rt ShareRequestType) IsValid() bool {
|
func (rt ShareRequestType) IsValid() bool {
|
||||||
switch rt {
|
switch rt {
|
||||||
case ShareRequestCall, ShareRequestCallDL, ShareRequestIncident,
|
case ShareRequestCall, ShareRequestCallInfo, ShareRequestCallDL, ShareRequestIncident,
|
||||||
ShareRequestIncidentM3U, ShareRequestTalkgroups:
|
ShareRequestIncidentM3U, ShareRequestTalkgroups:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -39,25 +53,62 @@ func (rt ShareRequestType) IsValid() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rt ShareRequestType) IsValidSubtype() bool {
|
|
||||||
switch rt {
|
|
||||||
case ShareRequestCall, ShareRequestCallDL, ShareRequestTalkgroups:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type ID interface {
|
type ID interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type HandlerFunc func(id ID, share *shares.Share, w http.ResponseWriter, r *http.Request)
|
type ShareHandlerFunc func(id ID, w http.ResponseWriter, r *http.Request)
|
||||||
type ShareHandlers map[ShareRequestType]HandlerFunc
|
type ShareHandlers map[ShareRequestType]ShareHandlerFunc
|
||||||
type shareAPI struct {
|
type shareAPI struct {
|
||||||
baseURL *url.URL
|
baseURL *url.URL
|
||||||
shnd ShareHandlers
|
shnd ShareHandlers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EntityFunc func(ctx context.Context, id ID) (SharedItem, error)
|
||||||
|
type SharedItem interface {
|
||||||
|
SetShareURL(baseURL url.URL, shareID string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type shareResponse struct {
|
||||||
|
ID ID `json:"id"`
|
||||||
|
Type shares.EntityType `json:"type"`
|
||||||
|
SharedItem SharedItem `json:"sharedItem,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShareFrom(ctx context.Context) *shares.Share {
|
||||||
|
if share, hasShare := entities.SubjectFrom(ctx).(*shares.Share); hasShare {
|
||||||
|
return share
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *api) respondShareHandler(ie EntityFunc) ShareHandlerFunc {
|
||||||
|
return func(id ID, w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
share := ShareFrom(ctx)
|
||||||
|
if share == nil {
|
||||||
|
wErr(w, r, autoError(ErrBadShare))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ie(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sRes := shareResponse{
|
||||||
|
ID: share.ID,
|
||||||
|
Type: share.Type,
|
||||||
|
SharedItem: res,
|
||||||
|
}
|
||||||
|
|
||||||
|
sRes.SharedItem.SetShareURL(*s.baseURL, share.ID)
|
||||||
|
|
||||||
|
respond(w, r, sRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newShareAPI(baseURL *url.URL, shnd ShareHandlers) *shareAPI {
|
func newShareAPI(baseURL *url.URL, shnd ShareHandlers) *shareAPI {
|
||||||
return &shareAPI{
|
return &shareAPI{
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
|
@ -70,6 +121,7 @@ func (sa *shareAPI) Subrouter() http.Handler {
|
||||||
|
|
||||||
r.Post(`/create`, sa.createShare)
|
r.Post(`/create`, sa.createShare)
|
||||||
r.Delete(`/{id:[A-Za-z0-9_-]{20,}}`, sa.deleteShare)
|
r.Delete(`/{id:[A-Za-z0-9_-]{20,}}`, sa.deleteShare)
|
||||||
|
r.Post(`/`, sa.listShares)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
@ -79,6 +131,7 @@ func (sa *shareAPI) RootRouter() http.Handler {
|
||||||
|
|
||||||
r.Get("/{shareId:[A-Za-z0-9_-]{20,}}", sa.routeShare)
|
r.Get("/{shareId:[A-Za-z0-9_-]{20,}}", sa.routeShare)
|
||||||
r.Get("/{shareId:[A-Za-z0-9_-]{20,}}/{type}", sa.routeShare)
|
r.Get("/{shareId:[A-Za-z0-9_-]{20,}}/{type}", sa.routeShare)
|
||||||
|
r.Get("/{shareId:[A-Za-z0-9_-]{20,}}.{type}", sa.routeShare)
|
||||||
r.Get("/{shareId:[A-Za-z0-9_-]{20,}}/{type}/{subID}", sa.routeShare)
|
r.Get("/{shareId:[A-Za-z0-9_-]{20,}}/{type}/{subID}", sa.routeShare)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
@ -104,6 +157,34 @@ func (sa *shareAPI) createShare(w http.ResponseWriter, r *http.Request) {
|
||||||
respond(w, r, sh)
|
respond(w, r, sh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sa *shareAPI) listShares(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
shs := shares.FromCtx(ctx)
|
||||||
|
|
||||||
|
p := shares.SharesParams{}
|
||||||
|
err := forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, badRequest(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shRes, count, err := shs.Shares(ctx, p)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := struct {
|
||||||
|
Shares []*shares.Share `json:"shares"`
|
||||||
|
TotalCount int `json:"totalCount"`
|
||||||
|
}{
|
||||||
|
Shares: shRes,
|
||||||
|
TotalCount: count,
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(w, r, &response)
|
||||||
|
}
|
||||||
|
|
||||||
func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) {
|
func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
shs := shares.FromCtx(ctx)
|
shs := shares.FromCtx(ctx)
|
||||||
|
@ -134,12 +215,11 @@ func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) {
|
||||||
} else {
|
} else {
|
||||||
switch sh.Type {
|
switch sh.Type {
|
||||||
case shares.EntityCall:
|
case shares.EntityCall:
|
||||||
rType = ShareRequestCall
|
rType = ShareRequestCallInfo
|
||||||
params.SubID = common.PtrTo(sh.EntityID.String())
|
params.SubID = common.PtrTo(sh.EntityID.String())
|
||||||
case shares.EntityIncident:
|
case shares.EntityIncident:
|
||||||
rType = ShareRequestIncident
|
rType = ShareRequestIncident
|
||||||
}
|
}
|
||||||
w.Header().Set("X-Share-Type", string(rType))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !rType.IsValid() {
|
if !rType.IsValid() {
|
||||||
|
@ -157,23 +237,44 @@ func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
switch rType {
|
switch rType {
|
||||||
case ShareRequestTalkgroups:
|
case ShareRequestTalkgroups:
|
||||||
sa.shnd[rType](nil, sh, w, r)
|
sa.shnd[rType](nil, w, r)
|
||||||
case ShareRequestCall, ShareRequestCallDL:
|
case ShareRequestCall, ShareRequestCallInfo, ShareRequestCallDL:
|
||||||
if params.SubID == nil {
|
var subIDU uuid.UUID
|
||||||
wErr(w, r, autoError(ErrBadShare))
|
if params.SubID != nil {
|
||||||
return
|
subIDU, err = uuid.Parse(*params.SubID)
|
||||||
}
|
|
||||||
|
|
||||||
subIDU, err := uuid.Parse(*params.SubID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wErr(w, r, badRequest(err))
|
wErr(w, r, badRequest(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sa.shnd[rType](subIDU, sh, w, r)
|
} else {
|
||||||
|
subIDU = sh.EntityID
|
||||||
|
}
|
||||||
|
|
||||||
|
sa.shnd[rType](subIDU, w, r)
|
||||||
case ShareRequestIncident, ShareRequestIncidentM3U:
|
case ShareRequestIncident, ShareRequestIncidentM3U:
|
||||||
sa.shnd[rType](sh.EntityID, sh, w, r)
|
sa.shnd[rType](sh.EntityID, w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sa *shareAPI) deleteShare(w http.ResponseWriter, r *http.Request) {
|
func (sa *shareAPI) deleteShare(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
shs := shares.FromCtx(ctx)
|
||||||
|
|
||||||
|
p := struct {
|
||||||
|
ID string `param:"id"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err := decodeParams(&p, r)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = shs.Delete(ctx, p.ID)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,11 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/forms"
|
"dynatron.me/x/stillbox/internal/forms"
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
||||||
"dynatron.me/x/stillbox/pkg/shares"
|
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups/xport"
|
"dynatron.me/x/stillbox/pkg/talkgroups/xport"
|
||||||
|
@ -161,10 +161,16 @@ func (tga *talkgroupAPI) postPaginated(w http.ResponseWriter, r *http.Request) {
|
||||||
respond(w, r, res)
|
respond(w, r, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tga *talkgroupAPI) getTGsShareRoute(_ ID, share *shares.Share, w http.ResponseWriter, r *http.Request) {
|
func (tga *talkgroupAPI) getTGsShareRoute(_ ID, w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
tgs := tgstore.FromCtx(ctx)
|
tgs := tgstore.FromCtx(ctx)
|
||||||
|
|
||||||
|
share := ShareFrom(ctx)
|
||||||
|
if share == nil {
|
||||||
|
wErr(w, r, autoError(ErrBadShare))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
tgIDs, err := incstore.FromCtx(ctx).TGsIn(ctx, share.EntityID)
|
tgIDs, err := incstore.FromCtx(ctx).TGsIn(ctx, share.EntityID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wErr(w, r, autoError(err))
|
wErr(w, r, autoError(err))
|
||||||
|
@ -176,6 +182,14 @@ func (tga *talkgroupAPI) getTGsShareRoute(_ ID, share *shares.Share, w http.Resp
|
||||||
idSl = append(idSl, id)
|
idSl = append(idSl, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(idSl, func(a, b talkgroups.ID) int {
|
||||||
|
if d := int(a.System) - int(b.System); d != 0 {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(a.Talkgroup) - int(b.Talkgroup)
|
||||||
|
})
|
||||||
|
|
||||||
tgRes, err := tgs.TGs(ctx, idSl)
|
tgRes, err := tgs.TGs(ctx, idSl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wErr(w, r, autoError(err))
|
wErr(w, r, autoError(err))
|
||||||
|
|
|
@ -29,10 +29,11 @@ func (s *Server) setupRoutes() {
|
||||||
r.Use(s.WithCtxStores())
|
r.Use(s.WithCtxStores())
|
||||||
|
|
||||||
s.installPprof()
|
s.installPprof()
|
||||||
|
r.Use(s.auth.VerifyMiddleware())
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(s.auth.AuthorizedSubjectMiddleware())
|
||||||
// authenticated routes
|
// authenticated routes
|
||||||
r.Use(s.auth.VerifyMiddleware(), s.auth.AuthMiddleware())
|
|
||||||
s.nex.PrivateRoutes(r)
|
s.nex.PrivateRoutes(r)
|
||||||
s.auth.PrivateRoutes(r)
|
s.auth.PrivateRoutes(r)
|
||||||
s.alerter.PrivateRoutes(r)
|
s.alerter.PrivateRoutes(r)
|
||||||
|
@ -41,6 +42,7 @@ func (s *Server) setupRoutes() {
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
s.rateLimit(r)
|
s.rateLimit(r)
|
||||||
|
r.Use(s.auth.PublicSubjectMiddleware())
|
||||||
r.Use(render.SetContentType(render.ContentTypeJSON))
|
r.Use(render.SetContentType(render.ContentTypeJSON))
|
||||||
// public routes
|
// public routes
|
||||||
s.sources.PublicRoutes(r)
|
s.sources.PublicRoutes(r)
|
||||||
|
@ -49,6 +51,7 @@ func (s *Server) setupRoutes() {
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
// auth/share routes get rate-limited heavily, but not using middleware
|
// auth/share routes get rate-limited heavily, but not using middleware
|
||||||
s.rateLimit(r)
|
s.rateLimit(r)
|
||||||
|
r.Use(s.auth.PublicSubjectMiddleware())
|
||||||
r.Use(render.SetContentType(render.ContentTypeJSON))
|
r.Use(render.SetContentType(render.ContentTypeJSON))
|
||||||
s.auth.PublicRoutes(r)
|
s.auth.PublicRoutes(r)
|
||||||
r.Mount("/share", s.rest.ShareRouter())
|
r.Mount("/share", s.rest.ShareRouter())
|
||||||
|
@ -56,9 +59,8 @@ func (s *Server) setupRoutes() {
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
s.rateLimit(r)
|
s.rateLimit(r)
|
||||||
r.Use(s.auth.VerifyMiddleware())
|
|
||||||
|
|
||||||
// optional auth routes
|
// optional auth routes
|
||||||
|
r.Use(s.auth.PublicSubjectMiddleware())
|
||||||
|
|
||||||
s.clientRoute(r, clientRoot)
|
s.clientRoute(r, clientRoot)
|
||||||
})
|
})
|
||||||
|
|
|
@ -18,6 +18,7 @@ func (s *Server) huppers() []hupper {
|
||||||
s.logger,
|
s.logger,
|
||||||
s.auth,
|
s.auth,
|
||||||
s.tgs,
|
s.tgs,
|
||||||
|
s.alerter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,8 +47,9 @@ func (et EntityType) IsValid() bool {
|
||||||
type Share struct {
|
type Share struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type EntityType `json:"entityType"`
|
Type EntityType `json:"entityType"`
|
||||||
Date *jsontypes.Time `json:"-"` // we handle this for the user
|
Date *jsontypes.Time `json:"entityDate,omitempty"` // we handle this for the user
|
||||||
Owner users.UserID `json:"owner"`
|
Owner users.UserID `json:"-"`
|
||||||
|
OwnerUser *string `json:"owner,omitempty"`
|
||||||
EntityID uuid.UUID `json:"entityID"`
|
EntityID uuid.UUID `json:"entityID"`
|
||||||
Expiration *jsontypes.Time `json:"expiration"`
|
Expiration *jsontypes.Time `json:"expiration"`
|
||||||
}
|
}
|
||||||
|
@ -57,6 +58,10 @@ func (s *Share) GetName() string {
|
||||||
return "SHARE:" + s.ID
|
return "SHARE:" + s.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Share) String() string {
|
||||||
|
return s.GetName()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Share) GetRoles() []string {
|
func (s *Share) GetRoles() []string {
|
||||||
return []string{entities.RoleShareGuest}
|
return []string{entities.RoleShareGuest}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,9 @@ package shares
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/common"
|
||||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
"dynatron.me/x/stillbox/pkg/rbac"
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
@ -12,13 +14,21 @@ import (
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SharesParams struct {
|
||||||
|
common.Pagination
|
||||||
|
Direction *common.SortDirection `json:"dir"`
|
||||||
|
}
|
||||||
|
|
||||||
type Shares interface {
|
type Shares interface {
|
||||||
// NewShare creates a new share.
|
// NewShare creates a new share.
|
||||||
NewShare(ctx context.Context, sh CreateShareParams) (*Share, error)
|
NewShare(ctx context.Context, sh CreateShareParams) (*Share, error)
|
||||||
|
|
||||||
// Share retreives a share record.
|
// Share retrieves a share record.
|
||||||
GetShare(ctx context.Context, id string) (*Share, error)
|
GetShare(ctx context.Context, id string) (*Share, error)
|
||||||
|
|
||||||
|
// Shares retrieves shares visible by the context Subject.
|
||||||
|
Shares(ctx context.Context, p SharesParams) (shares []*Share, totalCount int, err error)
|
||||||
|
|
||||||
// Create stores a new share record.
|
// Create stores a new share record.
|
||||||
Create(ctx context.Context, share *Share) error
|
Create(ctx context.Context, share *Share) error
|
||||||
|
|
||||||
|
@ -48,6 +58,11 @@ func recToShare(share database.Share) *Share {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *postgresStore) GetShare(ctx context.Context, id string) (*Share, error) {
|
func (s *postgresStore) GetShare(ctx context.Context, id string) (*Share, error) {
|
||||||
|
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceShare), rbac.WithActions(entities.ActionRead))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
db := database.FromCtx(ctx)
|
db := database.FromCtx(ctx)
|
||||||
rec, err := db.GetShare(ctx, id)
|
rec, err := db.GetShare(ctx, id)
|
||||||
switch err {
|
switch err {
|
||||||
|
@ -80,7 +95,12 @@ func (s *postgresStore) Create(ctx context.Context, share *Share) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *postgresStore) Delete(ctx context.Context, id string) error {
|
func (s *postgresStore) Delete(ctx context.Context, id string) error {
|
||||||
_, err := rbac.Check(ctx, new(Share), rbac.WithActions(entities.ActionDelete))
|
sh, err := s.GetShare(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rbac.Check(ctx, sh, rbac.WithActions(entities.ActionDelete))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -88,6 +108,54 @@ func (s *postgresStore) Delete(ctx context.Context, id string) error {
|
||||||
return database.FromCtx(ctx).DeleteShare(ctx, id)
|
return database.FromCtx(ctx).DeleteShare(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) Shares(ctx context.Context, p SharesParams) (shares []*Share, totalCount int, err error) {
|
||||||
|
sub := entities.SubjectFrom(ctx)
|
||||||
|
|
||||||
|
// ersatz RBAC
|
||||||
|
owner := common.PtrTo(int32(-1)) // invalid UID
|
||||||
|
switch s := sub.(type) {
|
||||||
|
case *users.User:
|
||||||
|
if !s.IsAdmin {
|
||||||
|
owner = s.ID.Int32Ptr()
|
||||||
|
} else {
|
||||||
|
owner = nil
|
||||||
|
}
|
||||||
|
case *entities.SystemServiceSubject:
|
||||||
|
owner = nil
|
||||||
|
default:
|
||||||
|
return nil, 0, rbac.ErrAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
db := database.FromCtx(ctx)
|
||||||
|
|
||||||
|
count, err := db.GetSharesPCount(ctx, owner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("shares count: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
offset, perPage := p.Pagination.OffsetPerPage(100)
|
||||||
|
dbParam := database.GetSharesPParams{
|
||||||
|
Owner: owner,
|
||||||
|
Direction: p.Direction.DirString(common.DirAsc),
|
||||||
|
Offset: offset,
|
||||||
|
PerPage: perPage,
|
||||||
|
}
|
||||||
|
|
||||||
|
shs, err := db.GetSharesP(ctx, dbParam)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
shares = make([]*Share, 0, len(shs))
|
||||||
|
for _, v := range shs {
|
||||||
|
s := recToShare(v.Share)
|
||||||
|
s.OwnerUser = &v.Username
|
||||||
|
shares = append(shares, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return shares, int(count), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *postgresStore) Prune(ctx context.Context) error {
|
func (s *postgresStore) Prune(ctx context.Context) error {
|
||||||
return database.FromCtx(ctx).PruneShares(ctx)
|
return database.FromCtx(ctx).PruneShares(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,7 +134,7 @@ func (h *RdioHTTP) routeCallUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
err = h.ing.Ingest(entities.CtxWithSubject(ctx, submitterSub), call)
|
err = h.ing.Ingest(entities.CtxWithSubject(ctx, submitterSub), call)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if rbac.ErrAccessDenied(err) != nil {
|
if rbac.IsErrAccessDenied(err) != nil {
|
||||||
log.Error().Err(err).Msg("ingest failed")
|
log.Error().Err(err).Msg("ingest failed")
|
||||||
http.Error(w, "Call ingest failed.", http.StatusForbidden)
|
http.Error(w, "Call ingest failed.", http.StatusForbidden)
|
||||||
}
|
}
|
||||||
|
|
|
@ -588,6 +588,7 @@ func (t *cache) UpdateTG(ctx context.Context, input database.UpdateTalkgroupPara
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) DeleteSystem(ctx context.Context, id int) error {
|
func (t *cache) DeleteSystem(ctx context.Context, id int) error {
|
||||||
|
// talkgroups don't have owners, so we can use a generic Resource
|
||||||
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionDelete))
|
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionDelete))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -2,11 +2,16 @@ package users
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/cache"
|
"dynatron.me/x/stillbox/internal/cache"
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoSuchUser = errors.New("no such user")
|
||||||
|
)
|
||||||
|
|
||||||
type Store interface {
|
type Store interface {
|
||||||
// GetUser gets a user by UID.
|
// GetUser gets a user by UID.
|
||||||
GetUser(ctx context.Context, username string) (*User, error)
|
GetUser(ctx context.Context, username string) (*User, error)
|
||||||
|
@ -84,6 +89,10 @@ func (s *postgresStore) GetUser(ctx context.Context, username string) (*User, er
|
||||||
|
|
||||||
dbu, err := s.db.GetUserByUsername(ctx, username)
|
dbu, err := s.db.GetUserByUsername(ctx, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if database.IsNoRows(err) {
|
||||||
|
return nil, ErrNoSuchUser
|
||||||
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,10 @@ func (u *User) GetName() string {
|
||||||
return u.Username
|
return u.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) String() string {
|
||||||
|
return "USER:"+u.GetName()
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) GetRoles() []string {
|
func (u *User) GetRoles() []string {
|
||||||
r := make([]string, 1, 2)
|
r := make([]string, 1, 2)
|
||||||
|
|
||||||
|
|
|
@ -143,6 +143,7 @@ CREATE TABLE IF NOT EXISTS incidents(
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
owner INTEGER NOT NULL,
|
owner INTEGER NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
start_time TIMESTAMPTZ,
|
start_time TIMESTAMPTZ,
|
||||||
end_time TIMESTAMPTZ,
|
end_time TIMESTAMPTZ,
|
||||||
location JSONB,
|
location JSONB,
|
||||||
|
|
|
@ -180,6 +180,7 @@ SELECT
|
||||||
tg_label,
|
tg_label,
|
||||||
tg_alpha_tag,
|
tg_alpha_tag,
|
||||||
tg_group,
|
tg_group,
|
||||||
source
|
source,
|
||||||
|
transcript
|
||||||
FROM calls
|
FROM calls
|
||||||
WHERE id = @id;
|
WHERE id = @id;
|
||||||
|
|
|
@ -42,6 +42,7 @@ INSERT INTO incidents (
|
||||||
name,
|
name,
|
||||||
owner,
|
owner,
|
||||||
description,
|
description,
|
||||||
|
created_at,
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
location,
|
location,
|
||||||
|
@ -51,6 +52,7 @@ INSERT INTO incidents (
|
||||||
@name,
|
@name,
|
||||||
@owner,
|
@owner,
|
||||||
sqlc.narg('description'),
|
sqlc.narg('description'),
|
||||||
|
NOW(),
|
||||||
sqlc.narg('start_time'),
|
sqlc.narg('start_time'),
|
||||||
sqlc.narg('end_time'),
|
sqlc.narg('end_time'),
|
||||||
sqlc.narg('location'),
|
sqlc.narg('location'),
|
||||||
|
@ -65,6 +67,7 @@ SELECT
|
||||||
i.name,
|
i.name,
|
||||||
i.owner,
|
i.owner,
|
||||||
i.description,
|
i.description,
|
||||||
|
i.created_at,
|
||||||
i.start_time,
|
i.start_time,
|
||||||
i.end_time,
|
i.end_time,
|
||||||
i.location,
|
i.location,
|
||||||
|
@ -160,6 +163,7 @@ SELECT
|
||||||
i.name,
|
i.name,
|
||||||
i.owner,
|
i.owner,
|
||||||
i.description,
|
i.description,
|
||||||
|
i.created_at,
|
||||||
i.start_time,
|
i.start_time,
|
||||||
i.end_time,
|
i.end_time,
|
||||||
i.location,
|
i.location,
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
-- name: GetShare :one
|
-- name: GetShare :one
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
s.id,
|
||||||
entity_type,
|
s.entity_type,
|
||||||
entity_id,
|
s.entity_id,
|
||||||
entity_date,
|
s.entity_date,
|
||||||
owner,
|
s.owner,
|
||||||
expiration
|
s.expiration
|
||||||
FROM shares
|
FROM shares s
|
||||||
WHERE id = @id;
|
WHERE s.id = @id;
|
||||||
|
|
||||||
-- name: CreateShare :exec
|
-- name: CreateShare :exec
|
||||||
INSERT INTO shares (
|
INSERT INTO shares (
|
||||||
|
@ -24,3 +24,27 @@ DELETE FROM shares WHERE id = @id;
|
||||||
|
|
||||||
-- name: PruneShares :exec
|
-- name: PruneShares :exec
|
||||||
DELETE FROM shares WHERE expiration < NOW();
|
DELETE FROM shares WHERE expiration < NOW();
|
||||||
|
|
||||||
|
-- name: GetSharesP :many
|
||||||
|
SELECT
|
||||||
|
sqlc.embed(s),
|
||||||
|
u.username
|
||||||
|
FROM shares s
|
||||||
|
JOIN users u ON (s.owner = u.id)
|
||||||
|
WHERE
|
||||||
|
CASE WHEN sqlc.narg('owner')::INTEGER IS NOT NULL THEN
|
||||||
|
s.owner = @owner ELSE TRUE END
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN @direction::TEXT = 'asc' THEN s.entity_date END ASC,
|
||||||
|
CASE WHEN @direction::TEXT = 'desc' THEN s.entity_date END DESC
|
||||||
|
OFFSET sqlc.arg('offset') ROWS
|
||||||
|
FETCH NEXT sqlc.arg('per_page') ROWS ONLY
|
||||||
|
;
|
||||||
|
|
||||||
|
-- name: GetSharesPCount :one
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM shares s
|
||||||
|
WHERE
|
||||||
|
CASE WHEN sqlc.narg('owner')::INTEGER IS NOT NULL THEN
|
||||||
|
s.owner = @owner ELSE TRUE END
|
||||||
|
;
|
||||||
|
|
|
@ -11,6 +11,7 @@ sql:
|
||||||
query_parameter_limit: 3
|
query_parameter_limit: 3
|
||||||
emit_json_tags: true
|
emit_json_tags: true
|
||||||
emit_interface: true
|
emit_interface: true
|
||||||
|
json_tags_case_style: camel
|
||||||
initialisms:
|
initialisms:
|
||||||
- id
|
- id
|
||||||
- tgid
|
- tgid
|
||||||
|
|
Loading…
Add table
Reference in a new issue