Merge pull request 'System wide settings' (#125) from settings124 into trunk
Reviewed-on: #125
This commit is contained in:
commit
b32e437011
15 changed files with 656 additions and 106 deletions
|
@ -260,6 +260,13 @@ export class CallsComponent {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
this.subscriptions.add(
|
||||||
|
this.prefsSvc.get('calls.view.showSourceAlias').subscribe((v) => {
|
||||||
|
if (v != true) {
|
||||||
|
this.columns = this.columns.filter((e) => e != 'talker');
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
this.subscriptions.add(
|
this.subscriptions.add(
|
||||||
this.fetchCalls
|
this.fetchCalls
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
map,
|
||||||
concatMap,
|
merge,
|
||||||
Observable,
|
Observable,
|
||||||
ReplaySubject,
|
ReplaySubject,
|
||||||
share,
|
|
||||||
shareReplay,
|
shareReplay,
|
||||||
Subscription,
|
Subscription,
|
||||||
switchMap,
|
switchMap,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
|
export interface UserSysPreferences {
|
||||||
|
userPrefs: Preferences;
|
||||||
|
sysPrefs: Preferences;
|
||||||
|
}
|
||||||
export interface Preferences {
|
export interface Preferences {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
@ -28,8 +31,8 @@ function mapToObj(map: Map<any, any>): { [key: string]: any } {
|
||||||
})
|
})
|
||||||
export class PrefsService {
|
export class PrefsService {
|
||||||
private readonly _getPref = new Map<string, ReplaySubject<any>>();
|
private readonly _getPref = new Map<string, ReplaySubject<any>>();
|
||||||
prefs$: Observable<Preferences>;
|
prefs$: Observable<UserSysPreferences>;
|
||||||
last!: Preferences;
|
last!: UserSysPreferences;
|
||||||
subscriptions = new Subscription();
|
subscriptions = new Subscription();
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
|
@ -57,20 +60,22 @@ export class PrefsService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.last = {};
|
this.last = <UserSysPreferences>{};
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(): Observable<Preferences> {
|
fetch(): Observable<UserSysPreferences> {
|
||||||
return this.http.get<Preferences>('/api/user/prefs/stillbox');
|
return this.http.get<UserSysPreferences>('/api/prefs/stillbox');
|
||||||
}
|
}
|
||||||
|
|
||||||
get(k: string): Observable<any> {
|
get(k: string): Observable<any> {
|
||||||
if (!this._getPref.get(k)) {
|
if (!this._getPref.get(k)) {
|
||||||
return this.prefs$.pipe(
|
return this.prefs$.pipe(
|
||||||
switchMap((pref) => {
|
switchMap((res) => {
|
||||||
|
let rv = <Preferences>res.sysPrefs;
|
||||||
|
let pref = <Preferences>mergeDeep(rv, res.userPrefs);
|
||||||
let ns = new ReplaySubject<any>(1);
|
let ns = new ReplaySubject<any>(1);
|
||||||
ns.next(pref ? pref[k] : null);
|
ns.next(pref ? pref[k] : null);
|
||||||
return ns;
|
return ns;
|
||||||
|
@ -82,14 +87,54 @@ export class PrefsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
set(pref: string, value: any) {
|
set(pref: string, value: any) {
|
||||||
this.last[pref] = value;
|
if (this.last == null) {
|
||||||
|
this.last = <UserSysPreferences>{};
|
||||||
|
}
|
||||||
|
if (this.last.userPrefs == null) {
|
||||||
|
this.last.userPrefs = <Preferences>{};
|
||||||
|
}
|
||||||
|
this.last.userPrefs[pref] = value;
|
||||||
let ex = this._getPref.get(pref);
|
let ex = this._getPref.get(pref);
|
||||||
if (!ex) {
|
if (ex == null) {
|
||||||
ex = new ReplaySubject<any>(1);
|
ex = new ReplaySubject<any>(1);
|
||||||
this._getPref.set(pref, ex);
|
this._getPref.set(pref, ex);
|
||||||
|
ex.next(value);
|
||||||
}
|
}
|
||||||
|
console.log('gonna up');
|
||||||
this.http
|
this.http
|
||||||
.put<Preferences>('/api/user/prefs/stillbox', this.last)
|
.put<Preferences>('/api/prefs/stillbox', this.last.userPrefs)
|
||||||
.subscribe((ev) => {});
|
.subscribe((ev) => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple object check.
|
||||||
|
* @param item
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isObject(item: any): boolean {
|
||||||
|
return item && typeof item === 'object' && !Array.isArray(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep merge two objects.
|
||||||
|
* @param target
|
||||||
|
* @param ...sources
|
||||||
|
*/
|
||||||
|
export function mergeDeep(target: any, ...sources: any): any {
|
||||||
|
if (!sources.length) return target;
|
||||||
|
const source = sources.shift();
|
||||||
|
|
||||||
|
if (isObject(target) && isObject(source)) {
|
||||||
|
for (const key in source) {
|
||||||
|
if (isObject(source[key])) {
|
||||||
|
if (!target[key]) Object.assign(target, { [key]: {} });
|
||||||
|
mergeDeep(target[key], source[key]);
|
||||||
|
} else {
|
||||||
|
Object.assign(target, { [key]: source[key] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergeDeep(target, ...sources);
|
||||||
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<mat-label>Group</mat-label
|
<mat-label>Group</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
name="tgTroup"
|
name="tgGroup"
|
||||||
type="text"
|
type="text"
|
||||||
formControlName="tgGroup"
|
formControlName="tgGroup"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -947,6 +947,53 @@ func (_c *Store_DeleteIncident_Call) RunAndReturn(run func(context.Context, uuid
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteSetting provides a mock function with given fields: ctx, name
|
||||||
|
func (_m *Store) DeleteSetting(ctx context.Context, name string) error {
|
||||||
|
ret := _m.Called(ctx, name)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for DeleteSetting")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||||
|
r0 = rf(ctx, name)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store_DeleteSetting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteSetting'
|
||||||
|
type Store_DeleteSetting_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSetting is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - name string
|
||||||
|
func (_e *Store_Expecter) DeleteSetting(ctx interface{}, name interface{}) *Store_DeleteSetting_Call {
|
||||||
|
return &Store_DeleteSetting_Call{Call: _e.mock.On("DeleteSetting", ctx, name)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_DeleteSetting_Call) Run(run func(ctx context.Context, name string)) *Store_DeleteSetting_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(string))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_DeleteSetting_Call) Return(_a0 error) *Store_DeleteSetting_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_DeleteSetting_Call) RunAndReturn(run func(context.Context, string) error) *Store_DeleteSetting_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteShare provides a mock function with given fields: ctx, id
|
// DeleteShare provides a mock function with given fields: ctx, id
|
||||||
func (_m *Store) DeleteShare(ctx context.Context, id string) error {
|
func (_m *Store) DeleteShare(ctx context.Context, id string) error {
|
||||||
ret := _m.Called(ctx, id)
|
ret := _m.Called(ctx, id)
|
||||||
|
@ -1989,6 +2036,65 @@ func (_c *Store_GetIncidentTalkgroups_Call) RunAndReturn(run func(context.Contex
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSetting provides a mock function with given fields: ctx, name
|
||||||
|
func (_m *Store) GetSetting(ctx context.Context, name string) ([]byte, error) {
|
||||||
|
ret := _m.Called(ctx, name)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetSetting")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 []byte
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) ([]byte, error)); ok {
|
||||||
|
return rf(ctx, name)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) []byte); ok {
|
||||||
|
r0 = rf(ctx, name)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]byte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||||
|
r1 = rf(ctx, name)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store_GetSetting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSetting'
|
||||||
|
type Store_GetSetting_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSetting is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - name string
|
||||||
|
func (_e *Store_Expecter) GetSetting(ctx interface{}, name interface{}) *Store_GetSetting_Call {
|
||||||
|
return &Store_GetSetting_Call{Call: _e.mock.On("GetSetting", ctx, name)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetSetting_Call) Run(run func(ctx context.Context, name string)) *Store_GetSetting_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(string))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetSetting_Call) Return(_a0 []byte, _a1 error) *Store_GetSetting_Call {
|
||||||
|
_c.Call.Return(_a0, _a1)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetSetting_Call) RunAndReturn(run func(context.Context, string) ([]byte, error)) *Store_GetSetting_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// GetShare provides a mock function with given fields: ctx, id
|
// GetShare provides a mock function with given fields: ctx, id
|
||||||
func (_m *Store) GetShare(ctx context.Context, id string) (database.Share, error) {
|
func (_m *Store) GetShare(ctx context.Context, id string) (database.Share, error) {
|
||||||
ret := _m.Called(ctx, id)
|
ret := _m.Called(ctx, id)
|
||||||
|
@ -3804,6 +3910,55 @@ func (_c *Store_SetCallTranscript_Call) RunAndReturn(run func(context.Context, u
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSetting provides a mock function with given fields: ctx, name, updatedBy, value
|
||||||
|
func (_m *Store) SetSetting(ctx context.Context, name string, updatedBy *int32, value []byte) error {
|
||||||
|
ret := _m.Called(ctx, name, updatedBy, value)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for SetSetting")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, *int32, []byte) error); ok {
|
||||||
|
r0 = rf(ctx, name, updatedBy, value)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store_SetSetting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetSetting'
|
||||||
|
type Store_SetSetting_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSetting is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - name string
|
||||||
|
// - updatedBy *int32
|
||||||
|
// - value []byte
|
||||||
|
func (_e *Store_Expecter) SetSetting(ctx interface{}, name interface{}, updatedBy interface{}, value interface{}) *Store_SetSetting_Call {
|
||||||
|
return &Store_SetSetting_Call{Call: _e.mock.On("SetSetting", ctx, name, updatedBy, value)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_SetSetting_Call) Run(run func(ctx context.Context, name string, updatedBy *int32, value []byte)) *Store_SetSetting_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(string), args[2].(*int32), args[3].([]byte))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_SetSetting_Call) Return(_a0 error) *Store_SetSetting_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_SetSetting_Call) RunAndReturn(run func(context.Context, string, *int32, []byte) error) *Store_SetSetting_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// SetTalkgroupTags provides a mock function with given fields: ctx, tags, systemID, tGID
|
// SetTalkgroupTags provides a mock function with given fields: ctx, tags, systemID, tGID
|
||||||
func (_m *Store) SetTalkgroupTags(ctx context.Context, tags []string, systemID int32, tGID int32) error {
|
func (_m *Store) SetTalkgroupTags(ctx context.Context, tags []string, systemID int32, tGID int32) error {
|
||||||
ret := _m.Called(ctx, tags, systemID, tGID)
|
ret := _m.Called(ctx, tags, systemID, tGID)
|
||||||
|
|
|
@ -26,6 +26,7 @@ type Querier interface {
|
||||||
DeleteAPIKey(ctx context.Context, apiKey string) error
|
DeleteAPIKey(ctx context.Context, apiKey string) error
|
||||||
DeleteCall(ctx context.Context, id uuid.UUID) error
|
DeleteCall(ctx context.Context, id uuid.UUID) error
|
||||||
DeleteIncident(ctx context.Context, id uuid.UUID) error
|
DeleteIncident(ctx context.Context, id uuid.UUID) error
|
||||||
|
DeleteSetting(ctx context.Context, name string) error
|
||||||
DeleteShare(ctx context.Context, id string) error
|
DeleteShare(ctx context.Context, id string) error
|
||||||
DeleteSystem(ctx context.Context, id int) error
|
DeleteSystem(ctx context.Context, id int) error
|
||||||
DeleteTalkgroup(ctx context.Context, systemID int32, tGID int32) error
|
DeleteTalkgroup(ctx context.Context, systemID int32, tGID int32) error
|
||||||
|
@ -43,6 +44,7 @@ type Querier interface {
|
||||||
GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error)
|
GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error)
|
||||||
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)
|
||||||
|
GetSetting(ctx context.Context, name string) ([]byte, 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)
|
GetSharesP(ctx context.Context, arg GetSharesPParams) ([]GetSharesPRow, error)
|
||||||
GetSharesPCount(ctx context.Context, owner *int32) (int64, error)
|
GetSharesPCount(ctx context.Context, owner *int32) (int64, error)
|
||||||
|
@ -71,6 +73,7 @@ type Querier interface {
|
||||||
RestoreTalkgroupVersion(ctx context.Context, versionIds int) (Talkgroup, error)
|
RestoreTalkgroupVersion(ctx context.Context, versionIds int) (Talkgroup, error)
|
||||||
SetAppPrefs(ctx context.Context, appName string, prefs []byte, uid int) error
|
SetAppPrefs(ctx context.Context, appName string, prefs []byte, uid int) error
|
||||||
SetCallTranscript(ctx context.Context, iD uuid.UUID, transcript *string) error
|
SetCallTranscript(ctx context.Context, iD uuid.UUID, transcript *string) error
|
||||||
|
SetSetting(ctx context.Context, name string, updatedBy *int32, value []byte) error
|
||||||
SetTalkgroupTags(ctx context.Context, tags []string, systemID int32, tGID int32) error
|
SetTalkgroupTags(ctx context.Context, tags []string, systemID int32, tGID int32) error
|
||||||
StoreDeletedTGVersion(ctx context.Context, systemID *int32, tGID *int32, submitter *int32) error
|
StoreDeletedTGVersion(ctx context.Context, systemID *int32, tGID *int32, submitter *int32) error
|
||||||
StoreTGVersion(ctx context.Context, arg []StoreTGVersionParams) *StoreTGVersionBatchResults
|
StoreTGVersion(ctx context.Context, arg []StoreTGVersionParams) *StoreTGVersionBatchResults
|
||||||
|
|
42
pkg/database/settings.sql.go
Normal file
42
pkg/database/settings.sql.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.27.0
|
||||||
|
// source: settings.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteSetting = `-- name: DeleteSetting :exec
|
||||||
|
DELETE FROM settings WHERE name = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteSetting(ctx context.Context, name string) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteSetting, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSetting = `-- name: GetSetting :one
|
||||||
|
SELECT value FROM settings WHERE name = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetSetting(ctx context.Context, name string) ([]byte, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getSetting, name)
|
||||||
|
var value []byte
|
||||||
|
err := row.Scan(&value)
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSetting = `-- name: SetSetting :exec
|
||||||
|
INSERT INTO settings (name, updated_by, value) VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (name) DO UPDATE SET
|
||||||
|
value = $3,
|
||||||
|
updated_by = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) SetSetting(ctx context.Context, name string, updatedBy *int32, value []byte) error {
|
||||||
|
_, err := q.db.Exec(ctx, setSetting, name, updatedBy, value)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ const (
|
||||||
ResourceShare = "Share"
|
ResourceShare = "Share"
|
||||||
ResourceAPIKey = "APIKey"
|
ResourceAPIKey = "APIKey"
|
||||||
ResourceCallStats = "CallStats"
|
ResourceCallStats = "CallStats"
|
||||||
|
ResourceSetting = "Setting"
|
||||||
|
|
||||||
ActionRead = "read"
|
ActionRead = "read"
|
||||||
ActionCreate = "create"
|
ActionCreate = "create"
|
||||||
|
@ -97,3 +98,9 @@ func (s *SystemServiceSubject) String() string {
|
||||||
func (s *SystemServiceSubject) GetRoles() []string {
|
func (s *SystemServiceSubject) GetRoles() []string {
|
||||||
return []string{RoleSystem}
|
return []string{RoleSystem}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsSystemService(sub Subject) bool {
|
||||||
|
_, is := sub.(*SystemServiceSubject)
|
||||||
|
|
||||||
|
return is
|
||||||
|
}
|
||||||
|
|
|
@ -50,6 +50,9 @@ var Policy = &restrict.PolicyDefinition{
|
||||||
entities.ResourceCallStats: {
|
entities.ResourceCallStats: {
|
||||||
&restrict.Permission{Action: entities.ActionRead},
|
&restrict.Permission{Action: entities.ActionRead},
|
||||||
},
|
},
|
||||||
|
entities.ResourceSetting: {
|
||||||
|
&restrict.Permission{Action: entities.ActionRead},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entities.RoleSubmitter: {
|
entities.RoleSubmitter: {
|
||||||
|
@ -108,6 +111,12 @@ var Policy = &restrict.PolicyDefinition{
|
||||||
&restrict.Permission{Action: entities.ActionCreate},
|
&restrict.Permission{Action: entities.ActionCreate},
|
||||||
&restrict.Permission{Action: entities.ActionDelete},
|
&restrict.Permission{Action: entities.ActionDelete},
|
||||||
},
|
},
|
||||||
|
entities.ResourceSetting: {
|
||||||
|
&restrict.Permission{Action: entities.ActionRead},
|
||||||
|
&restrict.Permission{Action: entities.ActionCreate},
|
||||||
|
&restrict.Permission{Action: entities.ActionUpdate},
|
||||||
|
&restrict.Permission{Action: entities.ActionDelete},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entities.RoleSystem: {
|
entities.RoleSystem: {
|
||||||
|
|
|
@ -35,6 +35,7 @@ type api struct {
|
||||||
calls *callsAPI
|
calls *callsAPI
|
||||||
users *usersAPI
|
users *usersAPI
|
||||||
incidents *incidentsAPI
|
incidents *incidentsAPI
|
||||||
|
prefs *prefsAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *api) ShareRouter() http.Handler {
|
func (a *api) ShareRouter() http.Handler {
|
||||||
|
@ -48,6 +49,7 @@ func New(baseURL url.URL) *api {
|
||||||
calls: new(callsAPI),
|
calls: new(callsAPI),
|
||||||
incidents: newIncidentsAPI(&baseURL),
|
incidents: newIncidentsAPI(&baseURL),
|
||||||
users: new(usersAPI),
|
users: new(usersAPI),
|
||||||
|
prefs: new(prefsAPI),
|
||||||
}
|
}
|
||||||
s.shares = newShareAPI(&baseURL, s.shareHandlers())
|
s.shares = newShareAPI(&baseURL, s.shareHandlers())
|
||||||
return s
|
return s
|
||||||
|
@ -61,6 +63,7 @@ func (a *api) Subrouter() http.Handler {
|
||||||
r.Mount("/call", a.calls.Subrouter())
|
r.Mount("/call", a.calls.Subrouter())
|
||||||
r.Mount("/incident", a.incidents.Subrouter())
|
r.Mount("/incident", a.incidents.Subrouter())
|
||||||
r.Mount("/share", a.shares.Subrouter())
|
r.Mount("/share", a.shares.Subrouter())
|
||||||
|
r.Mount("/prefs", a.prefs.Subrouter())
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
128
pkg/rest/prefs.go
Normal file
128
pkg/rest/prefs.go
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/pkg/auth"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
"dynatron.me/x/stillbox/pkg/settings"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBadAppName = errors.New("bad app name")
|
||||||
|
)
|
||||||
|
|
||||||
|
type prefsAPI struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pa *prefsAPI) Subrouter() http.Handler {
|
||||||
|
r := chi.NewMux()
|
||||||
|
|
||||||
|
r.Get(`/{appName}`, pa.getPrefs)
|
||||||
|
r.Put(`/{appName}`, pa.putPrefs)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pa *prefsAPI) getPrefs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
username := auth.UsernameFrom(ctx)
|
||||||
|
|
||||||
|
if username == nil {
|
||||||
|
wErr(w, r, autoError(rbac.ErrBadSubject))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := struct {
|
||||||
|
AppName *string `param:"appName"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err := decodeParams(&p, r)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, badRequest(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.AppName == nil {
|
||||||
|
wErr(w, r, autoError(ErrBadAppName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
us := users.FromCtx(ctx)
|
||||||
|
prefs, err := us.UserPrefs(ctx, *username, *p.AppName)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sysPrefs, err := settings.FromCtx(ctx).GetPrefs(ctx, *p.AppName)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
po := struct {
|
||||||
|
User json.RawMessage `json:"userPrefs"`
|
||||||
|
System json.RawMessage `json:"sysPrefs"`
|
||||||
|
}{
|
||||||
|
User: prefs,
|
||||||
|
System: sysPrefs,
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(w, r, po)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pa *prefsAPI) putPrefs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
username := auth.UsernameFrom(ctx)
|
||||||
|
|
||||||
|
if username == nil {
|
||||||
|
wErr(w, r, autoError(rbac.ErrBadSubject))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := strings.Split(r.Header.Get("Content-Type"), ";")[0]
|
||||||
|
if contentType != "application/json" {
|
||||||
|
wErr(w, r, badRequest(errors.New("only json accepted")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := struct {
|
||||||
|
AppName *string `param:"appName"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err := decodeParams(&p, r)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, badRequest(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.AppName == nil {
|
||||||
|
wErr(w, r, autoError(ErrBadAppName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
us := users.FromCtx(ctx)
|
||||||
|
err = us.SetUserPrefs(ctx, *username, *p.AppName, prefs)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = w.Write(prefs)
|
||||||
|
}
|
|
@ -1,112 +1,21 @@
|
||||||
package rest
|
package rest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/pkg/auth"
|
|
||||||
"dynatron.me/x/stillbox/pkg/rbac"
|
|
||||||
"dynatron.me/x/stillbox/pkg/users"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ErrBadAppName = errors.New("bad app name")
|
|
||||||
)
|
|
||||||
|
|
||||||
type usersAPI struct {
|
type usersAPI struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ua *usersAPI) Subrouter() http.Handler {
|
func (ua *usersAPI) Subrouter() http.Handler {
|
||||||
r := chi.NewMux()
|
r := chi.NewMux()
|
||||||
|
|
||||||
r.Get(`/prefs/{appName}`, ua.getPrefs)
|
r.Get("/{user}", ua.getUser)
|
||||||
r.Put(`/prefs/{appName}`, ua.putPrefs)
|
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ua *usersAPI) getPrefs(w http.ResponseWriter, r *http.Request) {
|
func (ua *usersAPI) getUser(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
username := auth.UsernameFrom(ctx)
|
|
||||||
|
|
||||||
if username == nil {
|
|
||||||
wErr(w, r, autoError(rbac.ErrBadSubject))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p := struct {
|
|
||||||
AppName *string `param:"appName"`
|
|
||||||
}{}
|
|
||||||
|
|
||||||
err := decodeParams(&p, r)
|
|
||||||
if err != nil {
|
|
||||||
wErr(w, r, badRequest(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.AppName == nil {
|
|
||||||
wErr(w, r, autoError(ErrBadAppName))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
us := users.FromCtx(ctx)
|
|
||||||
prefs, err := us.UserPrefs(ctx, *username, *p.AppName)
|
|
||||||
if err != nil {
|
|
||||||
wErr(w, r, autoError(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = w.Write(prefs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ua *usersAPI) putPrefs(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
username := auth.UsernameFrom(ctx)
|
|
||||||
|
|
||||||
if username == nil {
|
|
||||||
wErr(w, r, autoError(rbac.ErrBadSubject))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := strings.Split(r.Header.Get("Content-Type"), ";")[0]
|
|
||||||
if contentType != "application/json" {
|
|
||||||
wErr(w, r, badRequest(errors.New("only json accepted")))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p := struct {
|
|
||||||
AppName *string `param:"appName"`
|
|
||||||
}{}
|
|
||||||
|
|
||||||
err := decodeParams(&p, r)
|
|
||||||
if err != nil {
|
|
||||||
wErr(w, r, badRequest(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.AppName == nil {
|
|
||||||
wErr(w, r, autoError(ErrBadAppName))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
wErr(w, r, autoError(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
us := users.FromCtx(ctx)
|
|
||||||
err = us.SetUserPrefs(ctx, *username, *p.AppName, prefs)
|
|
||||||
if err != nil {
|
|
||||||
wErr(w, r, autoError(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = w.Write(prefs)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"dynatron.me/x/stillbox/pkg/rbac/policy"
|
"dynatron.me/x/stillbox/pkg/rbac/policy"
|
||||||
"dynatron.me/x/stillbox/pkg/rest"
|
"dynatron.me/x/stillbox/pkg/rest"
|
||||||
"dynatron.me/x/stillbox/pkg/services"
|
"dynatron.me/x/stillbox/pkg/services"
|
||||||
|
"dynatron.me/x/stillbox/pkg/settings"
|
||||||
"dynatron.me/x/stillbox/pkg/shares"
|
"dynatron.me/x/stillbox/pkg/shares"
|
||||||
"dynatron.me/x/stillbox/pkg/sinks"
|
"dynatron.me/x/stillbox/pkg/sinks"
|
||||||
"dynatron.me/x/stillbox/pkg/sources"
|
"dynatron.me/x/stillbox/pkg/sources"
|
||||||
|
@ -57,6 +58,7 @@ type Server struct {
|
||||||
share shares.Service
|
share shares.Service
|
||||||
rbac rbac.RBAC
|
rbac rbac.RBAC
|
||||||
stats stats.Stats
|
stats stats.Stats
|
||||||
|
settings settings.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
||||||
|
@ -110,6 +112,7 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
||||||
incidents: incstore.NewStore(),
|
incidents: incstore.NewStore(),
|
||||||
rbac: rbacSvc,
|
rbac: rbacSvc,
|
||||||
stats: statsSvc,
|
stats: statsSvc,
|
||||||
|
settings: settings.New(settings.ConfigDefaults),
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.DB.Partition.Enabled {
|
if cfg.DB.Partition.Enabled {
|
||||||
|
@ -176,6 +179,7 @@ func (s *Server) fillCtx(ctx context.Context) context.Context {
|
||||||
ctx = shares.CtxWithStore(ctx, s.share)
|
ctx = shares.CtxWithStore(ctx, s.share)
|
||||||
ctx = rbac.CtxWithRBAC(ctx, s.rbac)
|
ctx = rbac.CtxWithRBAC(ctx, s.rbac)
|
||||||
ctx = stats.CtxWithStats(ctx, s.stats)
|
ctx = stats.CtxWithStats(ctx, s.stats)
|
||||||
|
ctx = settings.CtxWithStore(ctx, s.settings)
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
22
pkg/settings/defaults.go
Normal file
22
pkg/settings/defaults.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package settings
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type Defaults map[string]Setting
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func MustMarshal(s interface{}) json.RawMessage {
|
||||||
|
b, err := json.Marshal(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
var ConfigDefaults = Defaults{
|
||||||
|
prefsName("stillbox"): MustMarshal(Defaults{
|
||||||
|
"calls.view.showSourceAlias": false,
|
||||||
|
}),
|
||||||
|
}
|
205
pkg/settings/settings.go
Normal file
205
pkg/settings/settings.go
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/cache"
|
||||||
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac/entities"
|
||||||
|
"dynatron.me/x/stillbox/pkg/services"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoSetting = errors.New("no such setting")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store interface {
|
||||||
|
// Get gets a setting and unmarshals it into dst.
|
||||||
|
Get(ctx context.Context, name string) (Setting, error)
|
||||||
|
|
||||||
|
// GetPrefs gets system prefs for an app name.
|
||||||
|
GetPrefs(ctx context.Context, appName string) (json.RawMessage, error)
|
||||||
|
|
||||||
|
// SetPrefs gets system prefs for an app name.
|
||||||
|
SetPrefs(ctx context.Context, appName string, val Setting) error
|
||||||
|
|
||||||
|
// Set sets a setting.
|
||||||
|
Set(ctx context.Context, name string, val Setting) error
|
||||||
|
|
||||||
|
// Delete removes a setting.
|
||||||
|
Delete(ctx context.Context, name string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Setting interface {
|
||||||
|
}
|
||||||
|
|
||||||
|
type postgresStore struct {
|
||||||
|
c cache.Cache[string, Setting]
|
||||||
|
defaults Defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
type storeCtxKey string
|
||||||
|
|
||||||
|
const StoreCtxKey storeCtxKey = "store"
|
||||||
|
|
||||||
|
func CtxWithStore(ctx context.Context, s Store) context.Context {
|
||||||
|
return services.WithValue(ctx, StoreCtxKey, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromCtx(ctx context.Context) Store {
|
||||||
|
s, ok := services.Value(ctx, StoreCtxKey).(Store)
|
||||||
|
if !ok {
|
||||||
|
panic("no settings store in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(defaults Defaults) *postgresStore {
|
||||||
|
s := &postgresStore{
|
||||||
|
c: cache.New[string, Setting](),
|
||||||
|
defaults: defaults,
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) defaultPrefs(appName string) json.RawMessage {
|
||||||
|
d, has := s.defaults[prefsName(appName)].(json.RawMessage)
|
||||||
|
if has {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) getJSONB(ctx context.Context, name string) (json.RawMessage, error) {
|
||||||
|
db := database.FromCtx(ctx)
|
||||||
|
|
||||||
|
sRes, err := db.GetSetting(ctx, name)
|
||||||
|
if err != nil && database.IsNoRows(err) {
|
||||||
|
return nil, ErrNoSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
return sRes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefsName(appName string) string {
|
||||||
|
return "prefs." + appName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) Get(ctx context.Context, name string) (Setting, error) {
|
||||||
|
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceSetting), rbac.WithActions(entities.ActionRead))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ci, has := s.c.Get(name)
|
||||||
|
if has {
|
||||||
|
return ci, nil
|
||||||
|
}
|
||||||
|
sRes, err := s.getJSONB(ctx, name)
|
||||||
|
if err != nil && errors.Is(err, ErrNoSetting) {
|
||||||
|
def, hasDefault := s.defaults[name]
|
||||||
|
if hasDefault {
|
||||||
|
return def, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(sRes, &ci)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.c.Set(name, ci)
|
||||||
|
|
||||||
|
return ci, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) GetPrefs(ctx context.Context, appName string) (json.RawMessage, error) {
|
||||||
|
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceSetting), rbac.WithActions(entities.ActionRead))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prName := prefsName(appName)
|
||||||
|
ci, has := s.c.Get(prName)
|
||||||
|
if has {
|
||||||
|
rm, isRM := ci.(json.RawMessage)
|
||||||
|
if isRM {
|
||||||
|
return rm, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := s.getJSONB(ctx, prName)
|
||||||
|
if errors.Is(err, ErrNoSetting) {
|
||||||
|
def, hasDefault := s.defaults[prName].(json.RawMessage)
|
||||||
|
if hasDefault {
|
||||||
|
return def, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.c.Set(prName, p)
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) SetPrefs(ctx context.Context, appName string, val Setting) error {
|
||||||
|
return s.Set(ctx, prefsName(appName), val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) Set(ctx context.Context, name string, val Setting) error {
|
||||||
|
subj, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceSetting), rbac.WithActions(entities.ActionCreate, entities.ActionUpdate))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(val)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var uid *int32
|
||||||
|
switch u := subj.(type) {
|
||||||
|
case *entities.SystemServiceSubject:
|
||||||
|
// uid remains null
|
||||||
|
case *users.User:
|
||||||
|
u, err := users.FromSubject(subj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
uid = u.ID.Int32Ptr()
|
||||||
|
default:
|
||||||
|
return rbac.ErrBadSubject
|
||||||
|
}
|
||||||
|
|
||||||
|
db := database.FromCtx(ctx)
|
||||||
|
|
||||||
|
err = db.SetSetting(ctx, name, uid, b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.c.Set(name, val)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) Delete(ctx context.Context, name string) error {
|
||||||
|
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceSetting), rbac.WithActions(entities.ActionDelete))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.c.Delete(name)
|
||||||
|
return database.FromCtx(ctx).DeleteSetting(ctx, name)
|
||||||
|
}
|
11
sql/postgres/queries/settings.sql
Normal file
11
sql/postgres/queries/settings.sql
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
-- name: GetSetting :one
|
||||||
|
SELECT value FROM settings WHERE name = @name;
|
||||||
|
|
||||||
|
-- name: SetSetting :exec
|
||||||
|
INSERT INTO settings (name, updated_by, value) VALUES (@name, @updated_by, @value)
|
||||||
|
ON CONFLICT (name) DO UPDATE SET
|
||||||
|
value = @value,
|
||||||
|
updated_by = @updated_by;
|
||||||
|
|
||||||
|
-- name: DeleteSetting :exec
|
||||||
|
DELETE FROM settings WHERE name = @name;
|
Loading…
Add table
Reference in a new issue