Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
55dc937385
|
|||
|
e98a804efc
|
|||
|
1f9abb8574
|
|||
|
9352ff5c2c
|
|||
|
1dafab8f5c
|
@@ -93,13 +93,13 @@ func (h APIHandler) GetChannelPreview(pctx ginext.PreContext) ginext.HTTPRespons
|
|||||||
|
|
||||||
userid := *ctx.GetPermissionUserID()
|
userid := *ctx.GetPermissionUserID()
|
||||||
|
|
||||||
channel, err := h.database.GetChannelByID(ctx, u.ChannelID)
|
channel, err := h.database.GetChannelByIDOpt(ctx, u.ChannelID)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
|
||||||
}
|
}
|
||||||
|
if channel == nil {
|
||||||
|
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
|
||||||
|
}
|
||||||
|
|
||||||
sub, err := h.database.GetSubscriptionBySubscriber(ctx, userid, channel.ChannelID)
|
sub, err := h.database.GetSubscriptionBySubscriber(ctx, userid, channel.ChannelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -162,13 +162,13 @@ func (h APIHandler) GetUserKeyPreview(pctx ginext.PreContext) ginext.HTTPRespons
|
|||||||
|
|
||||||
// Query by token.token
|
// Query by token.token
|
||||||
|
|
||||||
keytoken, err := h.database.GetKeyTokenByToken(ctx, u.KeyID)
|
keytoken, err := h.database.GetKeyTokenByTokenOpt(ctx, u.KeyID)
|
||||||
if keytoken == nil {
|
|
||||||
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
|
||||||
}
|
}
|
||||||
|
if keytoken == nil {
|
||||||
|
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
||||||
|
}
|
||||||
|
|
||||||
return finishSuccess(ginext.JSON(http.StatusOK, keytoken.Preview()))
|
return finishSuccess(ginext.JSON(http.StatusOK, keytoken.Preview()))
|
||||||
|
|
||||||
@@ -215,13 +215,13 @@ func (h APIHandler) GetClientPreview(pctx ginext.PreContext) ginext.HTTPResponse
|
|||||||
return *permResp
|
return *permResp
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := h.database.GetClientByID(ctx, u.ClientID)
|
client, err := h.database.GetClientByIDOpt(ctx, u.ClientID)
|
||||||
if client == nil {
|
|
||||||
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
|
||||||
}
|
}
|
||||||
|
if client == nil {
|
||||||
|
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
|
||||||
|
}
|
||||||
|
|
||||||
user, err := h.database.GetUser(ctx, client.UserID)
|
user, err := h.database.GetUser(ctx, client.UserID)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ func (h APIHandler) CreateSubscription(pctx ginext.PreContext) ginext.HTTPRespon
|
|||||||
|
|
||||||
} else if b.ChannelOwnerUserID == nil && b.ChannelInternalName == nil && b.ChannelID != nil {
|
} else if b.ChannelOwnerUserID == nil && b.ChannelInternalName == nil && b.ChannelID != nil {
|
||||||
|
|
||||||
outchannel, err := h.database.GetChannelByID(ctx, *b.ChannelID)
|
outchannel, err := h.database.GetChannelByIDOpt(ctx, *b.ChannelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ func (h CompatHandler) Info(pctx ginext.PreContext) ginext.HTTPResponse {
|
|||||||
return ginresp.CompatAPIError(0, "Failed to query user")
|
return ginresp.CompatAPIError(0, "Failed to query user")
|
||||||
}
|
}
|
||||||
|
|
||||||
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
|
keytok, err := h.database.GetKeyTokenByTokenOpt(ctx, *data.UserKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.CompatAPIError(0, "Failed to query token")
|
return ginresp.CompatAPIError(0, "Failed to query token")
|
||||||
}
|
}
|
||||||
@@ -417,7 +417,7 @@ func (h CompatHandler) Ack(pctx ginext.PreContext) ginext.HTTPResponse {
|
|||||||
return ginresp.CompatAPIError(0, "Failed to query user")
|
return ginresp.CompatAPIError(0, "Failed to query user")
|
||||||
}
|
}
|
||||||
|
|
||||||
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
|
keytok, err := h.database.GetKeyTokenByTokenOpt(ctx, *data.UserKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.CompatAPIError(0, "Failed to query token")
|
return ginresp.CompatAPIError(0, "Failed to query token")
|
||||||
}
|
}
|
||||||
@@ -523,7 +523,7 @@ func (h CompatHandler) Requery(pctx ginext.PreContext) ginext.HTTPResponse {
|
|||||||
return ginresp.CompatAPIError(0, "Failed to query user")
|
return ginresp.CompatAPIError(0, "Failed to query user")
|
||||||
}
|
}
|
||||||
|
|
||||||
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
|
keytok, err := h.database.GetKeyTokenByTokenOpt(ctx, *data.UserKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.CompatAPIError(0, "Failed to query token")
|
return ginresp.CompatAPIError(0, "Failed to query token")
|
||||||
}
|
}
|
||||||
@@ -644,7 +644,7 @@ func (h CompatHandler) Update(pctx ginext.PreContext) ginext.HTTPResponse {
|
|||||||
return ginresp.CompatAPIError(0, "Failed to query user")
|
return ginresp.CompatAPIError(0, "Failed to query user")
|
||||||
}
|
}
|
||||||
|
|
||||||
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
|
keytok, err := h.database.GetKeyTokenByTokenOpt(ctx, *data.UserKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.CompatAPIError(0, "Failed to query token")
|
return ginresp.CompatAPIError(0, "Failed to query token")
|
||||||
}
|
}
|
||||||
@@ -778,7 +778,7 @@ func (h CompatHandler) Expand(pctx ginext.PreContext) ginext.HTTPResponse {
|
|||||||
return ginresp.CompatAPIError(0, "Failed to query user")
|
return ginresp.CompatAPIError(0, "Failed to query user")
|
||||||
}
|
}
|
||||||
|
|
||||||
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
|
keytok, err := h.database.GetKeyTokenByTokenOpt(ctx, *data.UserKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.CompatAPIError(0, "Failed to query token")
|
return ginresp.CompatAPIError(0, "Failed to query token")
|
||||||
}
|
}
|
||||||
@@ -901,7 +901,7 @@ func (h CompatHandler) Upgrade(pctx ginext.PreContext) ginext.HTTPResponse {
|
|||||||
return ginresp.CompatAPIError(0, "Failed to query user")
|
return ginresp.CompatAPIError(0, "Failed to query user")
|
||||||
}
|
}
|
||||||
|
|
||||||
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
|
keytok, err := h.database.GetKeyTokenByTokenOpt(ctx, *data.UserKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.CompatAPIError(0, "Failed to query token")
|
return ginresp.CompatAPIError(0, "Failed to query token")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package primary
|
package primary
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"blackforestbytes.com/simplecloudnotifier/db"
|
"blackforestbytes.com/simplecloudnotifier/db"
|
||||||
"blackforestbytes.com/simplecloudnotifier/models"
|
"blackforestbytes.com/simplecloudnotifier/models"
|
||||||
"git.blackforestbytes.com/BlackForestBytes/goext/sq"
|
"git.blackforestbytes.com/BlackForestBytes/goext/sq"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (db *Database) GetChannelByName(ctx db.TxContext, userid models.UserID, chanName string) (*models.Channel, error) {
|
func (db *Database) GetChannelByName(ctx db.TxContext, userid models.UserID, chanName string) (*models.Channel, error) {
|
||||||
@@ -16,7 +17,7 @@ func (db *Database) GetChannelByName(ctx db.TxContext, userid models.UserID, cha
|
|||||||
return sq.QuerySingleOpt[models.Channel](ctx, tx, "SELECT * FROM channels WHERE owner_user_id = :uid AND internal_name = :nam AND deleted=0 LIMIT 1", sq.PP{"uid": userid, "nam": chanName}, sq.SModeExtended, sq.Safe)
|
return sq.QuerySingleOpt[models.Channel](ctx, tx, "SELECT * FROM channels WHERE owner_user_id = :uid AND internal_name = :nam AND deleted=0 LIMIT 1", sq.PP{"uid": userid, "nam": chanName}, sq.SModeExtended, sq.Safe)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) GetChannelByID(ctx db.TxContext, chanid models.ChannelID) (*models.Channel, error) {
|
func (db *Database) GetChannelByIDOpt(ctx db.TxContext, chanid models.ChannelID) (*models.Channel, error) {
|
||||||
tx, err := ctx.GetOrCreateTransaction(db)
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (db *Database) GetClient(ctx db.TxContext, userid models.UserID, clientid m
|
|||||||
}, sq.SModeExtended, sq.Safe)
|
}, sq.SModeExtended, sq.Safe)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) GetClientByID(ctx db.TxContext, clientid models.ClientID) (*models.Client, error) {
|
func (db *Database) GetClientByIDOpt(ctx db.TxContext, clientid models.ClientID) (*models.Client, error) {
|
||||||
tx, err := ctx.GetOrCreateTransaction(db)
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package primary
|
package primary
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"blackforestbytes.com/simplecloudnotifier/db"
|
"blackforestbytes.com/simplecloudnotifier/db"
|
||||||
"blackforestbytes.com/simplecloudnotifier/models"
|
"blackforestbytes.com/simplecloudnotifier/models"
|
||||||
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
||||||
"git.blackforestbytes.com/BlackForestBytes/goext/sq"
|
"git.blackforestbytes.com/BlackForestBytes/goext/sq"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (db *Database) CreateKeyToken(ctx db.TxContext, name string, owner models.UserID, allChannels bool, channels []models.ChannelID, permissions models.TokenPermissionList, token string) (models.KeyToken, error) {
|
func (db *Database) CreateKeyToken(ctx db.TxContext, name string, owner models.UserID, allChannels bool, channels []models.ChannelID, permissions models.TokenPermissionList, token string) (models.KeyToken, error) {
|
||||||
@@ -67,7 +68,7 @@ func (db *Database) GetKeyTokenByID(ctx db.TxContext, keyTokenid models.KeyToken
|
|||||||
return sq.QuerySingle[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE keytoken_id = :cid AND deleted=0 LIMIT 1", sq.PP{"cid": keyTokenid}, sq.SModeExtended, sq.Safe)
|
return sq.QuerySingle[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE keytoken_id = :cid AND deleted=0 LIMIT 1", sq.PP{"cid": keyTokenid}, sq.SModeExtended, sq.Safe)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) GetKeyTokenByToken(ctx db.TxContext, key string) (*models.KeyToken, error) {
|
func (db *Database) GetKeyTokenByTokenOpt(ctx db.TxContext, key string) (*models.KeyToken, error) {
|
||||||
tx, err := ctx.GetOrCreateTransaction(db)
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package jobs
|
package jobs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
|
"blackforestbytes.com/simplecloudnotifier/db/simplectx"
|
||||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||||
"blackforestbytes.com/simplecloudnotifier/models"
|
"blackforestbytes.com/simplecloudnotifier/models"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"git.blackforestbytes.com/BlackForestBytes/goext/syncext"
|
"git.blackforestbytes.com/BlackForestBytes/goext/syncext"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DeliveryRetryJob struct {
|
type DeliveryRetryJob struct {
|
||||||
@@ -208,7 +209,7 @@ func (j *DeliveryRetryJob) redeliver(ctx *simplectx.SimpleContext, delivery mode
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
channel, err := j.app.Database.Primary.GetChannelByID(ctx, msg.ChannelID)
|
channel, err := j.app.Database.Primary.GetChannelByIDOpt(ctx, msg.ChannelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Str("ChannelID", msg.ChannelID.String()).Msg("Failed to get channel")
|
log.Err(err).Str("ChannelID", msg.ChannelID.String()).Msg("Failed to get channel")
|
||||||
ctx.RollbackTransaction()
|
ctx.RollbackTransaction()
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ func (app *Application) getPermissions(ctx db.TxContext, hdr string) (models.Per
|
|||||||
|
|
||||||
key := strings.TrimSpace(hdr[4:])
|
key := strings.TrimSpace(hdr[4:])
|
||||||
|
|
||||||
tok, err := app.Database.Primary.GetKeyTokenByToken(ctx, key)
|
tok, err := app.Database.Primary.GetKeyTokenByTokenOpt(ctx, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return models.PermissionSet{}, err
|
return models.PermissionSet{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func (ac *AppContext) CheckPermissionUserAdmin(userid models.UserID) *ginext.HTT
|
|||||||
|
|
||||||
func (ac *AppContext) CheckPermissionSend(channel models.Channel, key string) (*models.KeyToken, *ginext.HTTPResponse) {
|
func (ac *AppContext) CheckPermissionSend(channel models.Channel, key string) (*models.KeyToken, *ginext.HTTPResponse) {
|
||||||
|
|
||||||
keytok, err := ac.app.Database.Primary.GetKeyTokenByToken(ac, key)
|
keytok, err := ac.app.Database.Primary.GetKeyTokenByTokenOpt(ac, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, langext.Ptr(ginresp.APIError(ac.ginContext, 500, apierr.DATABASE_ERROR, "Failed to query token", err))
|
return nil, langext.Ptr(ginresp.APIError(ac.ginContext, 500, apierr.DATABASE_ERROR, "Failed to query token", err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1159,6 +1159,8 @@ func TestSendToTooLongChannel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestQuotaExceededNoPro(t *testing.T) {
|
func TestQuotaExceededNoPro(t *testing.T) {
|
||||||
|
t.Skip("takes too long on server")
|
||||||
|
|
||||||
_, baseUrl, stop := tt.StartSimpleWebserver(t)
|
_, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
@@ -1236,6 +1238,8 @@ func TestQuotaExceededNoPro(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestQuotaExceededPro(t *testing.T) {
|
func TestQuotaExceededPro(t *testing.T) {
|
||||||
|
t.Skip("takes too long on server")
|
||||||
|
|
||||||
_, baseUrl, stop := tt.StartSimpleWebserver(t)
|
_, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export interface ChannelPreview {
|
|||||||
owner_user_id: string;
|
owner_user_id: string;
|
||||||
internal_name: string;
|
internal_name: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
|
description_name: string | null;
|
||||||
|
messages_sent: number;
|
||||||
|
subscription: Subscription | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChannelSelector = 'owned' | 'subscribed' | 'all' | 'subscribed_any' | 'all_any';
|
export type ChannelSelector = 'owned' | 'subscribed' | 'all' | 'subscribed_any' | 'all_any';
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ export interface ClientListResponse {
|
|||||||
|
|
||||||
export interface ClientPreview {
|
export interface ClientPreview {
|
||||||
client_id: string;
|
client_id: string;
|
||||||
|
user_id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
type: ClientType;
|
type: ClientType;
|
||||||
|
timestamp_created: string;
|
||||||
agent_model: string;
|
agent_model: string;
|
||||||
agent_version: string;
|
agent_version: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ export interface KeyToken {
|
|||||||
export interface KeyTokenPreview {
|
export interface KeyTokenPreview {
|
||||||
keytoken_id: string;
|
keytoken_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
owner_user_id: string;
|
||||||
|
all_channels: boolean;
|
||||||
|
channels: string[];
|
||||||
|
permissions: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TokenPermission = 'A' | 'CR' | 'CS' | 'UR';
|
export type TokenPermission = 'A' | 'CR' | 'CS' | 'UR';
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
User,
|
User,
|
||||||
UserWithExtra,
|
UserWithExtra,
|
||||||
UserPreview,
|
UserPreview,
|
||||||
|
ChannelPreview,
|
||||||
|
KeyTokenPreview,
|
||||||
Message,
|
Message,
|
||||||
MessageListParams,
|
MessageListParams,
|
||||||
MessageListResponse,
|
MessageListResponse,
|
||||||
@@ -98,6 +100,14 @@ export class ApiService {
|
|||||||
return this.http.get<ClientPreviewResponse>(`${this.baseUrl}/preview/clients/${clientId}`);
|
return this.http.get<ClientPreviewResponse>(`${this.baseUrl}/preview/clients/${clientId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getChannelPreview(channelId: string): Observable<ChannelPreview> {
|
||||||
|
return this.http.get<ChannelPreview>(`${this.baseUrl}/preview/channels/${channelId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getKeyPreview(keyId: string): Observable<KeyTokenPreview> {
|
||||||
|
return this.http.get<KeyTokenPreview>(`${this.baseUrl}/preview/keys/${keyId}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Channel endpoints
|
// Channel endpoints
|
||||||
getChannels(userId: string, selector?: ChannelSelector): Observable<ChannelListResponse> {
|
getChannels(userId: string, selector?: ChannelSelector): Observable<ChannelListResponse> {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { Observable, of, catchError, map, shareReplay } from 'rxjs';
|
import { Observable, of, catchError, map, shareReplay } from 'rxjs';
|
||||||
import { ApiService } from './api.service';
|
import { ApiService } from './api.service';
|
||||||
|
import { ClientPreview, UserPreview } from '../models';
|
||||||
|
|
||||||
export interface ResolvedClient {
|
export interface ResolvedClient {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
@@ -17,18 +18,14 @@ export interface ResolvedClient {
|
|||||||
export class ClientCacheService {
|
export class ClientCacheService {
|
||||||
private apiService = inject(ApiService);
|
private apiService = inject(ApiService);
|
||||||
|
|
||||||
private cache = new Map<string, Observable<ResolvedClient | null>>();
|
private cache = new Map<string, Observable<{client: ClientPreview, user: UserPreview} | null>>();
|
||||||
|
|
||||||
resolveClient(clientId: string): Observable<ResolvedClient | null> {
|
resolveClient(clientId: string): Observable<{client: ClientPreview, user: UserPreview} | null> {
|
||||||
if (!this.cache.has(clientId)) {
|
if (!this.cache.has(clientId)) {
|
||||||
const request$ = this.apiService.getClientPreview(clientId).pipe(
|
const request$ = this.apiService.getClientPreview(clientId).pipe(
|
||||||
map(response => ({
|
map(response => ({
|
||||||
clientId: response.client.client_id,
|
client: response.client,
|
||||||
clientName: response.client.name,
|
user: response.user
|
||||||
userId: response.user.user_id,
|
|
||||||
userName: response.user.username,
|
|
||||||
agentModel: response.client.agent_model,
|
|
||||||
agentVersion: response.client.agent_version,
|
|
||||||
})),
|
})),
|
||||||
catchError(() => of(null)),
|
catchError(() => of(null)),
|
||||||
shareReplay(1)
|
shareReplay(1)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="loading-container">
|
<div class="loading-container">
|
||||||
<nz-spin nzSimple nzSize="large"></nz-spin>
|
<nz-spin nzSimple nzSize="large"></nz-spin>
|
||||||
</div>
|
</div>
|
||||||
} @else if (channel()) {
|
} @else if (channelData()) {
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<button nz-button (click)="goBack()">
|
<button nz-button (click)="goBack()">
|
||||||
<span nz-icon nzType="arrow-left" nzTheme="outline"></span>
|
<span nz-icon nzType="arrow-left" nzTheme="outline"></span>
|
||||||
@@ -32,13 +32,13 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nz-card [nzTitle]="channel()!.display_name">
|
<nz-card [nzTitle]="channelData()!.display_name">
|
||||||
<scn-metadata-grid>
|
<scn-metadata-grid>
|
||||||
<scn-metadata-value label="Channel ID">
|
<scn-metadata-value label="Channel ID">
|
||||||
<span class="mono">{{ channel()!.channel_id }}</span>
|
<span class="mono">{{ channelData()!.channel_id }}</span>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Internal Name">
|
<scn-metadata-value label="Internal Name">
|
||||||
<span class="mono">{{ channel()!.internal_name }}</span>
|
<span class="mono">{{ channelData()!.internal_name }}</span>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Status">
|
<scn-metadata-value label="Status">
|
||||||
<nz-tag [nzColor]="getSubscriptionStatus().color">
|
<nz-tag [nzColor]="getSubscriptionStatus().color">
|
||||||
@@ -46,16 +46,22 @@
|
|||||||
</nz-tag>
|
</nz-tag>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Owner">
|
<scn-metadata-value label="Owner">
|
||||||
<span class="mono">{{ channel()!.owner_user_id }}</span>
|
@if (resolvedOwner()) {
|
||||||
|
<div class="owner-name">{{ resolvedOwner()!.displayName }}</div>
|
||||||
|
<div class="owner-id mono">{{ channelData()!.owner_user_id }}</div>
|
||||||
|
} @else {
|
||||||
|
<span class="mono">{{ channelData()!.owner_user_id }}</span>
|
||||||
|
}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
@if (channel()!.description_name) {
|
@if (channelData()!.description_name) {
|
||||||
<scn-metadata-value label="Description">
|
<scn-metadata-value label="Description">
|
||||||
{{ channel()!.description_name }}
|
{{ channelData()!.description_name }}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
}
|
}
|
||||||
<scn-metadata-value label="Messages Sent">
|
<scn-metadata-value label="Messages Sent">
|
||||||
{{ channel()!.messages_sent }}
|
{{ channelData()!.messages_sent }}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
|
@if (channel()) {
|
||||||
<scn-metadata-value label="Last Sent">
|
<scn-metadata-value label="Last Sent">
|
||||||
@if (channel()!.timestamp_lastsent) {
|
@if (channel()!.timestamp_lastsent) {
|
||||||
<div class="timestamp-absolute">{{ channel()!.timestamp_lastsent | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
<div class="timestamp-absolute">{{ channel()!.timestamp_lastsent | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
||||||
@@ -68,7 +74,8 @@
|
|||||||
<div class="timestamp-absolute">{{ channel()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
<div class="timestamp-absolute">{{ channel()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
||||||
<div class="timestamp-relative">{{ channel()!.timestamp_created | relativeTime }}</div>
|
<div class="timestamp-relative">{{ channel()!.timestamp_created | relativeTime }}</div>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
@if (isOwner() && channel()!.subscribe_key) {
|
}
|
||||||
|
@if (isOwner() && channel()?.subscribe_key) {
|
||||||
<scn-metadata-value label="Subscribe Key">
|
<scn-metadata-value label="Subscribe Key">
|
||||||
<div class="key-field">
|
<div class="key-field">
|
||||||
<nz-input-group [nzSuffix]="subscribeKeySuffix">
|
<nz-input-group [nzSuffix]="subscribeKeySuffix">
|
||||||
|
|||||||
@@ -109,6 +109,17 @@
|
|||||||
overflow-y: clip;
|
overflow-y: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.owner-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.owner-id {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.text-muted {
|
.text-muted {
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { AuthService } from '../../../core/services/auth.service';
|
|||||||
import { NotificationService } from '../../../core/services/notification.service';
|
import { NotificationService } from '../../../core/services/notification.service';
|
||||||
import { SettingsService } from '../../../core/services/settings.service';
|
import { SettingsService } from '../../../core/services/settings.service';
|
||||||
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
||||||
import { ChannelWithSubscription, Subscription, Message } from '../../../core/models';
|
import { ChannelWithSubscription, ChannelPreview, Subscription, Message } from '../../../core/models';
|
||||||
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
||||||
import { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive';
|
import { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive';
|
||||||
import { QrCodeDisplayComponent } from '../../../shared/components/qr-code-display/qr-code-display.component';
|
import { QrCodeDisplayComponent } from '../../../shared/components/qr-code-display/qr-code-display.component';
|
||||||
@@ -68,9 +68,11 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
private userCacheService = inject(UserCacheService);
|
private userCacheService = inject(UserCacheService);
|
||||||
|
|
||||||
channel = signal<ChannelWithSubscription | null>(null);
|
channel = signal<ChannelWithSubscription | null>(null);
|
||||||
|
channelPreview = signal<ChannelPreview | null>(null);
|
||||||
subscriptions = signal<Subscription[]>([]);
|
subscriptions = signal<Subscription[]>([]);
|
||||||
messages = signal<Message[]>([]);
|
messages = signal<Message[]>([]);
|
||||||
userNames = signal<Map<string, ResolvedUser>>(new Map());
|
userNames = signal<Map<string, ResolvedUser>>(new Map());
|
||||||
|
resolvedOwner = signal<ResolvedUser | null>(null);
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
loadingSubscriptions = signal(false);
|
loadingSubscriptions = signal(false);
|
||||||
loadingMessages = signal(false);
|
loadingMessages = signal(false);
|
||||||
@@ -115,19 +117,31 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
|
this.apiService.getChannelPreview(channelId).subscribe({
|
||||||
|
next: (preview) => {
|
||||||
|
this.channelPreview.set(preview);
|
||||||
|
this.resolveOwner(preview.owner_user_id);
|
||||||
|
if (preview.owner_user_id === userId) {
|
||||||
this.apiService.getChannel(userId, channelId).subscribe({
|
this.apiService.getChannel(userId, channelId).subscribe({
|
||||||
next: (channel) => {
|
next: (channel) => {
|
||||||
this.channel.set(channel);
|
this.channel.set(channel);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
if (this.isOwner()) {
|
|
||||||
this.loadSubscriptions(channelId);
|
this.loadSubscriptions(channelId);
|
||||||
}
|
|
||||||
this.loadMessages(channelId);
|
this.loadMessages(channelId);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.loading.set(false);
|
||||||
|
this.loadMessages(channelId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSubscriptions(channelId: string): void {
|
loadSubscriptions(channelId: string): void {
|
||||||
@@ -148,14 +162,13 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadMessages(channelId: string, nextPageToken?: string): void {
|
loadMessages(channelId: string, nextPageToken?: string): void {
|
||||||
const userId = this.authService.getUserId();
|
|
||||||
if (!userId) return;
|
|
||||||
|
|
||||||
this.loadingMessages.set(true);
|
this.loadingMessages.set(true);
|
||||||
this.apiService.getChannelMessages(userId, channelId, {
|
this.apiService.getMessages({
|
||||||
|
channel_id: [channelId],
|
||||||
page_size: this.messagesPageSize,
|
page_size: this.messagesPageSize,
|
||||||
next_page_token: nextPageToken,
|
next_page_token: nextPageToken,
|
||||||
trimmed: true
|
trimmed: true,
|
||||||
|
subscription_status: 'all'
|
||||||
}).subscribe({
|
}).subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.messages.set(response.messages);
|
this.messages.set(response.messages);
|
||||||
@@ -210,6 +223,12 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveOwner(ownerId: string): void {
|
||||||
|
this.userCacheService.resolveUser(ownerId).subscribe(resolved => {
|
||||||
|
this.resolvedOwner.set(resolved);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private resolveUserNames(subscriptions: Subscription[]): void {
|
private resolveUserNames(subscriptions: Subscription[]): void {
|
||||||
const userIds = new Set<string>();
|
const userIds = new Set<string>();
|
||||||
for (const sub of subscriptions) {
|
for (const sub of subscriptions) {
|
||||||
@@ -232,9 +251,16 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isOwner(): boolean {
|
isOwner(): boolean {
|
||||||
const channel = this.channel();
|
|
||||||
const userId = this.authService.getUserId();
|
const userId = this.authService.getUserId();
|
||||||
return channel?.owner_user_id === userId;
|
const channel = this.channel();
|
||||||
|
if (channel) return channel.owner_user_id === userId;
|
||||||
|
const preview = this.channelPreview();
|
||||||
|
if (preview) return preview.owner_user_id === userId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
channelData() {
|
||||||
|
return this.channel() ?? this.channelPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit methods
|
// Edit methods
|
||||||
@@ -290,18 +316,20 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSubscriptionStatus(): { label: string; color: string } {
|
getSubscriptionStatus(): { label: string; color: string } {
|
||||||
const channel = this.channel();
|
const data = this.channelData();
|
||||||
if (!channel) return { label: 'Unknown', color: 'default' };
|
if (!data) return { label: 'Unknown', color: 'default' };
|
||||||
|
|
||||||
|
const subscription = 'subscribe_key' in data ? data.subscription : data.subscription;
|
||||||
|
|
||||||
if (this.isOwner()) {
|
if (this.isOwner()) {
|
||||||
if (channel.subscription) {
|
if (subscription) {
|
||||||
return { label: 'Owned & Subscribed', color: 'green' };
|
return { label: 'Owned & Subscribed', color: 'green' };
|
||||||
}
|
}
|
||||||
return { label: 'Owned', color: 'blue' };
|
return { label: 'Owned', color: 'blue' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channel.subscription) {
|
if (subscription) {
|
||||||
if (channel.subscription.confirmed) {
|
if (subscription.confirmed) {
|
||||||
return { label: 'Subscribed', color: 'green' };
|
return { label: 'Subscribed', color: 'green' };
|
||||||
}
|
}
|
||||||
return { label: 'Pending', color: 'orange' };
|
return { label: 'Pending', color: 'orange' };
|
||||||
@@ -377,7 +405,7 @@ export class ChannelDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isUserSubscribed(): boolean {
|
isUserSubscribed(): boolean {
|
||||||
return this.channel()?.subscription !== null;
|
return this.channelData()?.subscription !== null && this.channelData()?.subscription !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSelfSubscription(): void {
|
toggleSelfSubscription(): void {
|
||||||
|
|||||||
@@ -50,50 +50,30 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (channel of channels(); track channel.channel_id) {
|
@for (channel of channels(); track channel.channel_id) {
|
||||||
<tr [class.clickable-row]="isOwned(channel)">
|
<tr class="clickable-row">
|
||||||
<td>
|
<td>
|
||||||
@if (isOwned(channel)) {
|
|
||||||
<a class="cell-link" [routerLink]="['/channels', channel.channel_id]">
|
<a class="cell-link" [routerLink]="['/channels', channel.channel_id]">
|
||||||
<div class="channel-name">{{ channel.display_name }}</div>
|
<div class="channel-name">{{ channel.display_name }}</div>
|
||||||
<div class="channel-id mono">{{ channel.channel_id }}</div>
|
<div class="channel-id mono">{{ channel.channel_id }}</div>
|
||||||
</a>
|
</a>
|
||||||
} @else {
|
|
||||||
<div class="channel-name">{{ channel.display_name }}</div>
|
|
||||||
<div class="channel-id mono">{{ channel.channel_id }}</div>
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (isOwned(channel)) {
|
|
||||||
<a class="cell-link" [routerLink]="['/channels', channel.channel_id]">
|
<a class="cell-link" [routerLink]="['/channels', channel.channel_id]">
|
||||||
<span class="mono">{{ channel.internal_name }}</span>
|
<span class="mono">{{ channel.internal_name }}</span>
|
||||||
</a>
|
</a>
|
||||||
} @else {
|
|
||||||
<span class="mono">{{ channel.internal_name }}</span>
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (isOwned(channel)) {
|
|
||||||
<a class="cell-link" [routerLink]="['/channels', channel.channel_id]">
|
<a class="cell-link" [routerLink]="['/channels', channel.channel_id]">
|
||||||
<div class="channel-name">{{ getOwnerDisplayName(channel.owner_user_id) }}</div>
|
<div class="channel-name">{{ getOwnerDisplayName(channel.owner_user_id) }}</div>
|
||||||
<div class="channel-id mono">{{ channel.owner_user_id }}</div>
|
<div class="channel-id mono">{{ channel.owner_user_id }}</div>
|
||||||
</a>
|
</a>
|
||||||
} @else {
|
|
||||||
<div class="channel-name">{{ getOwnerDisplayName(channel.owner_user_id) }}</div>
|
|
||||||
<div class="channel-id mono">{{ channel.owner_user_id }}</div>
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (isOwned(channel)) {
|
|
||||||
<a class="cell-link" [routerLink]="['/channels', channel.channel_id]">
|
<a class="cell-link" [routerLink]="['/channels', channel.channel_id]">
|
||||||
<nz-tag [nzColor]="getSubscriptionStatus(channel).color">
|
<nz-tag [nzColor]="getSubscriptionStatus(channel).color">
|
||||||
{{ getSubscriptionStatus(channel).label }}
|
{{ getSubscriptionStatus(channel).label }}
|
||||||
</nz-tag>
|
</nz-tag>
|
||||||
</a>
|
</a>
|
||||||
} @else {
|
|
||||||
<nz-tag [nzColor]="getSubscriptionStatus(channel).color">
|
|
||||||
{{ getSubscriptionStatus(channel).label }}
|
|
||||||
</nz-tag>
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (isOwned(channel)) {
|
@if (isOwned(channel)) {
|
||||||
@@ -105,16 +85,11 @@
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (isOwned(channel)) {
|
|
||||||
<a class="cell-link" [routerLink]="['/channels', channel.channel_id]">
|
<a class="cell-link" [routerLink]="['/channels', channel.channel_id]">
|
||||||
{{ channel.messages_sent }}
|
{{ channel.messages_sent }}
|
||||||
</a>
|
</a>
|
||||||
} @else {
|
|
||||||
{{ channel.messages_sent }}
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (isOwned(channel)) {
|
|
||||||
<a class="cell-link" [routerLink]="['/channels', channel.channel_id]">
|
<a class="cell-link" [routerLink]="['/channels', channel.channel_id]">
|
||||||
@if (channel.timestamp_lastsent) {
|
@if (channel.timestamp_lastsent) {
|
||||||
<div class="timestamp-absolute">{{ channel.timestamp_lastsent | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
<div class="timestamp-absolute">{{ channel.timestamp_lastsent | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
||||||
@@ -123,14 +98,6 @@
|
|||||||
<span class="text-muted">Never</span>
|
<span class="text-muted">Never</span>
|
||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
} @else {
|
|
||||||
@if (channel.timestamp_lastsent) {
|
|
||||||
<div class="timestamp-absolute">{{ channel.timestamp_lastsent | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
|
||||||
<div class="timestamp-relative">{{ channel.timestamp_lastsent | relativeTime }}</div>
|
|
||||||
} @else {
|
|
||||||
<span class="text-muted">Never</span>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
@if (expertMode()) {
|
@if (expertMode()) {
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
<div class="loading-container">
|
<div class="loading-container">
|
||||||
<nz-spin nzSimple nzSize="large"></nz-spin>
|
<nz-spin nzSimple nzSize="large"></nz-spin>
|
||||||
</div>
|
</div>
|
||||||
} @else if (client()) {
|
} @else if (clientData()) {
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<button nz-button (click)="goBack()">
|
<button nz-button (click)="goBack()">
|
||||||
<span nz-icon nzType="arrow-left" nzTheme="outline"></span>
|
<span nz-icon nzType="arrow-left" nzTheme="outline"></span>
|
||||||
Back to Clients
|
Back to Clients
|
||||||
</button>
|
</button>
|
||||||
@if (expertMode()) {
|
@if (isOwner() && expertMode()) {
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button
|
<button
|
||||||
nz-button
|
nz-button
|
||||||
@@ -29,36 +29,38 @@
|
|||||||
<div class="client-header">
|
<div class="client-header">
|
||||||
<span
|
<span
|
||||||
nz-icon
|
nz-icon
|
||||||
[nzType]="getClientIcon(client()!.type)"
|
[nzType]="getClientIcon(clientData()!.type)"
|
||||||
nzTheme="outline"
|
nzTheme="outline"
|
||||||
class="client-type-icon"
|
class="client-type-icon"
|
||||||
></span>
|
></span>
|
||||||
<h2 class="client-title">{{ client()!.name || 'Unnamed Client' }}</h2>
|
<h2 class="client-title">{{ clientData()!.name || 'Unnamed Client' }}</h2>
|
||||||
<nz-tag>{{ getClientTypeLabel(client()!.type) }}</nz-tag>
|
<nz-tag>{{ getClientTypeLabel(clientData()!.type) }}</nz-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<scn-metadata-grid>
|
<scn-metadata-grid>
|
||||||
<scn-metadata-value label="Client ID">
|
<scn-metadata-value label="Client ID">
|
||||||
<span class="mono">{{ client()!.client_id }}</span>
|
<span class="mono">{{ clientData()!.client_id }}</span>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Type">
|
<scn-metadata-value label="Type">
|
||||||
<nz-tag>{{ getClientTypeLabel(client()!.type) }}</nz-tag>
|
<nz-tag>{{ getClientTypeLabel(clientData()!.type) }}</nz-tag>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Agent">
|
<scn-metadata-value label="Agent">
|
||||||
<div class="agent-info">
|
<div class="agent-info">
|
||||||
<span>{{ client()!.agent_model }}</span>
|
<span>{{ clientData()!.agent_model }}</span>
|
||||||
<span class="agent-version">v{{ client()!.agent_version }}</span>
|
<span class="agent-version">v{{ clientData()!.agent_version }}</span>
|
||||||
</div>
|
</div>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Created">
|
<scn-metadata-value label="Created">
|
||||||
<div class="timestamp-absolute">{{ client()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
<div class="timestamp-absolute">{{ clientData()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
||||||
<div class="timestamp-relative">{{ client()!.timestamp_created | relativeTime }}</div>
|
<div class="timestamp-relative">{{ clientData()!.timestamp_created | relativeTime }}</div>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
|
@if (client()) {
|
||||||
<scn-metadata-value label="FCM Token">
|
<scn-metadata-value label="FCM Token">
|
||||||
<span class="mono fcm-token" nz-tooltip [nzTooltipTitle]="client()!.fcm_token">
|
<span class="mono fcm-token" nz-tooltip [nzTooltipTitle]="client()!.fcm_token">
|
||||||
{{ client()!.fcm_token }}
|
{{ client()!.fcm_token }}
|
||||||
</span>
|
</span>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
|
}
|
||||||
</scn-metadata-grid>
|
</scn-metadata-grid>
|
||||||
</nz-card>
|
</nz-card>
|
||||||
} @else {
|
} @else {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { ApiService } from '../../../core/services/api.service';
|
|||||||
import { AuthService } from '../../../core/services/auth.service';
|
import { AuthService } from '../../../core/services/auth.service';
|
||||||
import { NotificationService } from '../../../core/services/notification.service';
|
import { NotificationService } from '../../../core/services/notification.service';
|
||||||
import { SettingsService } from '../../../core/services/settings.service';
|
import { SettingsService } from '../../../core/services/settings.service';
|
||||||
import { Client, ClientType, getClientTypeIcon } from '../../../core/models';
|
import { Client, ClientPreview, ClientType, getClientTypeIcon } from '../../../core/models';
|
||||||
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
||||||
import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/components/metadata-grid';
|
import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/components/metadata-grid';
|
||||||
|
|
||||||
@@ -45,6 +45,7 @@ export class ClientDetailComponent implements OnInit {
|
|||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
|
|
||||||
client = signal<Client | null>(null);
|
client = signal<Client | null>(null);
|
||||||
|
clientPreview = signal<ClientPreview | null>(null);
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
expertMode = this.settingsService.expertMode;
|
expertMode = this.settingsService.expertMode;
|
||||||
|
|
||||||
@@ -60,6 +61,10 @@ export class ClientDetailComponent implements OnInit {
|
|||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
|
this.apiService.getClientPreview(clientId).subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
this.clientPreview.set(response.client);
|
||||||
|
if (response.client.user_id === userId) {
|
||||||
this.apiService.getClient(userId, clientId).subscribe({
|
this.apiService.getClient(userId, clientId).subscribe({
|
||||||
next: (client) => {
|
next: (client) => {
|
||||||
this.client.set(client);
|
this.client.set(client);
|
||||||
@@ -69,6 +74,27 @@ export class ClientDetailComponent implements OnInit {
|
|||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clientData() {
|
||||||
|
return this.client() ?? this.clientPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
isOwner(): boolean {
|
||||||
|
const userId = this.authService.getUserId();
|
||||||
|
const client = this.client();
|
||||||
|
if (client) return client.user_id === userId;
|
||||||
|
const preview = this.clientPreview();
|
||||||
|
if (preview) return preview.user_id === userId;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
goBack(): void {
|
goBack(): void {
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
<div class="loading-container">
|
<div class="loading-container">
|
||||||
<nz-spin nzSimple nzSize="large"></nz-spin>
|
<nz-spin nzSimple nzSize="large"></nz-spin>
|
||||||
</div>
|
</div>
|
||||||
} @else if (key()) {
|
} @else if (keyData()) {
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<button nz-button (click)="goBack()">
|
<button nz-button (click)="goBack()">
|
||||||
<span nz-icon nzType="arrow-left" nzTheme="outline"></span>
|
<span nz-icon nzType="arrow-left" nzTheme="outline"></span>
|
||||||
Back to Keys
|
Back to Keys
|
||||||
</button>
|
</button>
|
||||||
|
@if (isOwner()) {
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button nz-button (click)="openEditModal()">
|
<button nz-button (click)="openEditModal()">
|
||||||
<span nz-icon nzType="edit"></span>
|
<span nz-icon nzType="edit"></span>
|
||||||
@@ -27,11 +28,12 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nz-card>
|
<nz-card>
|
||||||
<div class="key-header">
|
<div class="key-header">
|
||||||
<h2 class="key-title">{{ key()!.name }}</h2>
|
<h2 class="key-title">{{ keyData()!.name }}</h2>
|
||||||
@if (isCurrentKey()) {
|
@if (isCurrentKey()) {
|
||||||
<nz-tag nzColor="cyan">Current</nz-tag>
|
<nz-tag nzColor="cyan">Current</nz-tag>
|
||||||
}
|
}
|
||||||
@@ -39,7 +41,7 @@
|
|||||||
|
|
||||||
<scn-metadata-grid>
|
<scn-metadata-grid>
|
||||||
<scn-metadata-value label="Key ID">
|
<scn-metadata-value label="Key ID">
|
||||||
<span class="mono">{{ key()!.keytoken_id }}</span>
|
<span class="mono">{{ keyData()!.keytoken_id }}</span>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Permissions">
|
<scn-metadata-value label="Permissions">
|
||||||
<div class="permissions">
|
<div class="permissions">
|
||||||
@@ -55,11 +57,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
<scn-metadata-value label="Channel Access">
|
<scn-metadata-value label="Channel Access">
|
||||||
@if (key()!.all_channels) {
|
@if (keyData()!.all_channels) {
|
||||||
<nz-tag nzColor="default">All Channels</nz-tag>
|
<nz-tag nzColor="default">All Channels</nz-tag>
|
||||||
} @else if (key()!.channels && key()!.channels.length > 0) {
|
} @else if (keyData()!.channels && keyData()!.channels.length > 0) {
|
||||||
<div class="channel-list">
|
<div class="channel-list">
|
||||||
@for (channelId of key()!.channels; track channelId) {
|
@for (channelId of keyData()!.channels; track channelId) {
|
||||||
<nz-tag nzColor="orange" nz-tooltip [nzTooltipTitle]="channelId">
|
<nz-tag nzColor="orange" nz-tooltip [nzTooltipTitle]="channelId">
|
||||||
{{ getChannelDisplayName(channelId) }}
|
{{ getChannelDisplayName(channelId) }}
|
||||||
</nz-tag>
|
</nz-tag>
|
||||||
@@ -69,6 +71,7 @@
|
|||||||
<span class="text-muted">No channels</span>
|
<span class="text-muted">No channels</span>
|
||||||
}
|
}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
|
@if (key()) {
|
||||||
<scn-metadata-value label="Messages Sent">
|
<scn-metadata-value label="Messages Sent">
|
||||||
{{ key()!.messages_sent }}
|
{{ key()!.messages_sent }}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
@@ -84,12 +87,13 @@
|
|||||||
<span class="text-muted">Never</span>
|
<span class="text-muted">Never</span>
|
||||||
}
|
}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
|
}
|
||||||
<scn-metadata-value label="Owner">
|
<scn-metadata-value label="Owner">
|
||||||
@if (resolvedOwner()) {
|
@if (resolvedOwner()) {
|
||||||
<div class="owner-name">{{ resolvedOwner()!.displayName }}</div>
|
<div class="owner-name">{{ resolvedOwner()!.displayName }}</div>
|
||||||
<div class="owner-id mono">{{ key()!.owner_user_id }}</div>
|
<div class="owner-id mono">{{ keyData()!.owner_user_id }}</div>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="mono">{{ key()!.owner_user_id }}</span>
|
<span class="mono">{{ keyData()!.owner_user_id }}</span>
|
||||||
}
|
}
|
||||||
</scn-metadata-value>
|
</scn-metadata-value>
|
||||||
</scn-metadata-grid>
|
</scn-metadata-grid>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { AuthService } from '../../../core/services/auth.service';
|
|||||||
import { NotificationService } from '../../../core/services/notification.service';
|
import { NotificationService } from '../../../core/services/notification.service';
|
||||||
import { ChannelCacheService, ResolvedChannel } from '../../../core/services/channel-cache.service';
|
import { ChannelCacheService, ResolvedChannel } from '../../../core/services/channel-cache.service';
|
||||||
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
||||||
import { KeyToken, parsePermissions, TokenPermission, ChannelWithSubscription, Message } from '../../../core/models';
|
import { KeyToken, KeyTokenPreview, parsePermissions, TokenPermission, ChannelWithSubscription, Message } from '../../../core/models';
|
||||||
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
||||||
import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/components/metadata-grid';
|
import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/components/metadata-grid';
|
||||||
|
|
||||||
@@ -72,6 +72,7 @@ export class KeyDetailComponent implements OnInit {
|
|||||||
private userCacheService = inject(UserCacheService);
|
private userCacheService = inject(UserCacheService);
|
||||||
|
|
||||||
key = signal<KeyToken | null>(null);
|
key = signal<KeyToken | null>(null);
|
||||||
|
keyPreview = signal<KeyTokenPreview | null>(null);
|
||||||
currentKeyId = signal<string | null>(null);
|
currentKeyId = signal<string | null>(null);
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
channelNames = signal<Map<string, ResolvedChannel>>(new Map());
|
channelNames = signal<Map<string, ResolvedChannel>>(new Map());
|
||||||
@@ -105,8 +106,6 @@ export class KeyDetailComponent implements OnInit {
|
|||||||
const keyId = this.route.snapshot.paramMap.get('id');
|
const keyId = this.route.snapshot.paramMap.get('id');
|
||||||
if (keyId) {
|
if (keyId) {
|
||||||
this.loadKey(keyId);
|
this.loadKey(keyId);
|
||||||
this.loadCurrentKey();
|
|
||||||
this.loadAvailableChannels();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,18 +114,34 @@ export class KeyDetailComponent implements OnInit {
|
|||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
|
this.apiService.getKeyPreview(keyId).subscribe({
|
||||||
|
next: (preview) => {
|
||||||
|
this.keyPreview.set(preview);
|
||||||
|
this.resolveOwner(preview.owner_user_id);
|
||||||
|
this.resolveChannelNamesFromPreview(preview);
|
||||||
|
if (preview.owner_user_id === userId) {
|
||||||
|
this.loadCurrentKey();
|
||||||
|
this.loadAvailableChannels();
|
||||||
this.apiService.getKey(userId, keyId).subscribe({
|
this.apiService.getKey(userId, keyId).subscribe({
|
||||||
next: (key) => {
|
next: (key) => {
|
||||||
this.key.set(key);
|
this.key.set(key);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
this.resolveChannelNames(key);
|
this.resolveChannelNames(key);
|
||||||
this.resolveOwner(key.owner_user_id);
|
|
||||||
this.loadMessages(keyId);
|
this.loadMessages(keyId);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.loading.set(false);
|
||||||
|
this.loadMessages(keyId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMessages(keyId: string, nextPageToken?: string): void {
|
loadMessages(keyId: string, nextPageToken?: string): void {
|
||||||
@@ -217,6 +232,27 @@ export class KeyDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveChannelNamesFromPreview(preview: KeyTokenPreview): void {
|
||||||
|
if (!preview.all_channels && preview.channels && preview.channels.length > 0) {
|
||||||
|
this.channelCacheService.resolveChannels(preview.channels).subscribe(resolved => {
|
||||||
|
this.channelNames.set(resolved);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyData() {
|
||||||
|
return this.key() ?? this.keyPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
isOwner(): boolean {
|
||||||
|
const userId = this.authService.getUserId();
|
||||||
|
const key = this.key();
|
||||||
|
if (key) return key.owner_user_id === userId;
|
||||||
|
const preview = this.keyPreview();
|
||||||
|
if (preview) return preview.owner_user_id === userId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
goBack(): void {
|
goBack(): void {
|
||||||
this.router.navigate(['/keys']);
|
this.router.navigate(['/keys']);
|
||||||
}
|
}
|
||||||
@@ -227,8 +263,8 @@ export class KeyDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPermissions(): TokenPermission[] {
|
getPermissions(): TokenPermission[] {
|
||||||
const key = this.key();
|
const data = this.keyData();
|
||||||
return key ? parsePermissions(key.permissions) : [];
|
return data ? parsePermissions(data.permissions) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getPermissionColor(perm: TokenPermission): string {
|
getPermissionColor(perm: TokenPermission): string {
|
||||||
|
|||||||
@@ -104,19 +104,15 @@
|
|||||||
@for (delivery of deliveriesTable.data; track delivery.delivery_id) {
|
@for (delivery of deliveriesTable.data; track delivery.delivery_id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div class="cell-name">{{ getResolvedClient(delivery.receiver_client_id)?.clientName || '-' }}</div>
|
<div class="cell-name">{{ getResolvedClient(delivery.receiver_client_id)?.client?.name ?? '-' }}</div>
|
||||||
<div class="cell-id mono">{{ delivery.receiver_client_id }}</div>
|
<div class="cell-id mono">{{ delivery.receiver_client_id }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="cell-name">{{ getResolvedClient(delivery.receiver_client_id)?.userName || '-' }}</div>
|
<div class="cell-name">{{ getResolvedClient(delivery.receiver_client_id)?.user?.username ?? '-' }}</div>
|
||||||
<div class="cell-id mono">{{ delivery.receiver_user_id }}</div>
|
<div class="cell-id mono">{{ delivery.receiver_user_id }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (getResolvedClient(delivery.receiver_client_id); as client) {
|
{{ getResolvedClient(delivery.receiver_client_id)?.client?.agent_model ?? '-' }}
|
||||||
{{ client.agentModel }} {{ client.agentVersion }}
|
|
||||||
} @else {
|
|
||||||
-
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<nz-tag [nzColor]="getStatusColor(delivery.status)">
|
<nz-tag [nzColor]="getStatusColor(delivery.status)">
|
||||||
@@ -133,7 +129,8 @@
|
|||||||
nzType="copy"
|
nzType="copy"
|
||||||
class="action-icon"
|
class="action-icon"
|
||||||
nz-tooltip
|
nz-tooltip
|
||||||
nzTooltipTitle="Copy FCM ID"
|
nzTooltipTitle="Copy Message FCM-ID"
|
||||||
|
style="cursor: pointer"
|
||||||
[appCopyToClipboard]="delivery.fcm_message_id"
|
[appCopyToClipboard]="delivery.fcm_message_id"
|
||||||
></span>
|
></span>
|
||||||
} @else {
|
} @else {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { SettingsService } from '../../../core/services/settings.service';
|
|||||||
import { KeyCacheService, ResolvedKey } from '../../../core/services/key-cache.service';
|
import { KeyCacheService, ResolvedKey } from '../../../core/services/key-cache.service';
|
||||||
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
|
||||||
import { ClientCacheService, ResolvedClient } from '../../../core/services/client-cache.service';
|
import { ClientCacheService, ResolvedClient } from '../../../core/services/client-cache.service';
|
||||||
import { Message, Delivery } from '../../../core/models';
|
import { Message, Delivery, ClientPreview, UserPreview } from '../../../core/models';
|
||||||
import { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive';
|
import { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive';
|
||||||
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
||||||
import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/components/metadata-grid';
|
import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/components/metadata-grid';
|
||||||
@@ -59,7 +59,7 @@ export class MessageDetailComponent implements OnInit {
|
|||||||
resolvedKey = signal<ResolvedKey | null>(null);
|
resolvedKey = signal<ResolvedKey | null>(null);
|
||||||
resolvedChannelOwner = signal<ResolvedUser | null>(null);
|
resolvedChannelOwner = signal<ResolvedUser | null>(null);
|
||||||
deliveries = signal<Delivery[]>([]);
|
deliveries = signal<Delivery[]>([]);
|
||||||
resolvedClients = signal<Map<string, ResolvedClient>>(new Map());
|
resolvedClients = signal<Map<string, {client: ClientPreview, user: UserPreview}>>(new Map());
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
deleting = signal(false);
|
deleting = signal(false);
|
||||||
loadingDeliveries = signal(false);
|
loadingDeliveries = signal(false);
|
||||||
@@ -139,7 +139,7 @@ export class MessageDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getResolvedClient(clientId: string): ResolvedClient | undefined {
|
getResolvedClient(clientId: string): {client: ClientPreview, user: UserPreview} | undefined {
|
||||||
return this.resolvedClients().get(clientId);
|
return this.resolvedClients().get(clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user