diff --git a/api/application.go b/api/application.go index d6c987c..3ff6112 100644 --- a/api/application.go +++ b/api/application.go @@ -143,6 +143,7 @@ func (a *ApplicationAPI) GetApplications(ctx *gin.Context) { return } for _, app := range apps { + app.Token = "" withResolvedImage(app) } ctx.JSON(200, apps) diff --git a/api/application_test.go b/api/application_test.go index 2fa7a1d..4787cc6 100644 --- a/api/application_test.go +++ b/api/application_test.go @@ -278,6 +278,8 @@ func (s *ApplicationSuite) Test_GetApplications() { assert.Equal(s.T(), 200, s.recorder.Code) first.Image = "static/defaultapp.png" second.Image = "static/defaultapp.png" + first.Token = "" + second.Token = "" test.BodyEquals(s.T(), []*model.Application{first, second}, s.recorder) } @@ -296,6 +298,8 @@ func (s *ApplicationSuite) Test_GetApplications_WithImage() { assert.Equal(s.T(), 200, s.recorder.Code) first.Image = "image/abcd.jpg" second.Image = "static/defaultapp.png" + first.Token = "" + second.Token = "" test.BodyEquals(s.T(), []*model.Application{first, second}, s.recorder) } diff --git a/api/client.go b/api/client.go index f81535f..64af8a7 100644 --- a/api/client.go +++ b/api/client.go @@ -200,6 +200,7 @@ func (a *ClientAPI) GetClients(ctx *gin.Context) { } now := time.Now() for _, client := range clients { + client.Token = "" if client.ElevatedUntil != nil && !now.Before(*client.ElevatedUntil) { client.ElevatedUntil = nil } diff --git a/api/client_test.go b/api/client_test.go index a55998c..585a2d6 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -175,6 +175,8 @@ func (s *ClientSuite) Test_GetClients() { s.a.GetClients(s.ctx) assert.Equal(s.T(), 200, s.recorder.Code) + first.Token = "" + second.Token = "" test.BodyEquals(s.T(), []*model.Client{first, second}, s.recorder) } diff --git a/auth/authentication.go b/auth/authentication.go index 532cd18..5da712d 100644 --- a/auth/authentication.go +++ b/auth/authentication.go @@ -148,6 +148,7 @@ func (a *Auth) handleUser(checks ...func(*model.User) (authState, error)) func(c func (a *Auth) handleClient(checks ...func(*model.Client) (authState, error)) func(ctx *gin.Context) (authState, error) { return func(ctx *gin.Context) (authState, error) { token, isCookie := a.readTokenFromRequest(ctx) + originalToken := token if token == "" { return authStateSkip, nil } @@ -173,7 +174,7 @@ func (a *Auth) handleClient(checks ...func(*model.Client) (authState, error)) fu return authStateSkip, err } if isCookie { - SetCookie(ctx.Writer, token, CookieMaxAge, a.SecureCookie) + SetCookie(ctx.Writer, originalToken, CookieMaxAge, a.SecureCookie) } } @@ -189,6 +190,7 @@ func (a *Auth) handleClient(checks ...func(*model.Client) (authState, error)) fu func (a *Auth) handleApplication(ctx *gin.Context) (authState, error) { token, isCookie := a.readTokenFromRequest(ctx) + originalToken := token if token == "" { return authStateSkip, nil } @@ -214,7 +216,7 @@ func (a *Auth) handleApplication(ctx *gin.Context) (authState, error) { return authStateSkip, err } if isCookie { - SetCookie(ctx.Writer, token, CookieMaxAge, a.SecureCookie) + SetCookie(ctx.Writer, originalToken, CookieMaxAge, a.SecureCookie) } } diff --git a/ui/src/application/AddApplicationDialog.tsx b/ui/src/application/AddApplicationDialog.tsx index 99d465e..e1485ed 100644 --- a/ui/src/application/AddApplicationDialog.tsx +++ b/ui/src/application/AddApplicationDialog.tsx @@ -8,70 +8,96 @@ import TextField from '@mui/material/TextField'; import Tooltip from '@mui/material/Tooltip'; import {NumberField} from '../common/NumberField'; import React, {useState} from 'react'; +import {Typography} from '@mui/material'; +import {copyToClipboard} from '../clipboard'; interface IProps { fClose: VoidFunction; - fOnSubmit: (name: string, description: string, defaultPriority: number) => Promise; + fOnSubmit: (name: string, description: string, defaultPriority: number) => Promise; } export const AddApplicationDialog = ({fClose, fOnSubmit}: IProps) => { + const [returnToken, setReturnToken] = useState(''); const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [defaultPriority, setDefaultPriority] = useState(0); const submitEnabled = name.length !== 0; - const submitAndClose = async () => { - await fOnSubmit(name, description, defaultPriority); - fClose(); + const submitAndNext = async () => { + const token = await fOnSubmit(name, description, defaultPriority); + setReturnToken(token); }; return ( Create an application - An application is allowed to send messages. - setName(e.target.value)} - fullWidth - /> - setDescription(e.target.value)} - fullWidth - multiline - /> - setDefaultPriority(value)} - fullWidth - /> + {returnToken ? ( + + Your token is: + + {returnToken} + + + ) : ( + <> + + An application is allowed to send messages. + + setName(e.target.value)} + fullWidth + /> + setDescription(e.target.value)} + fullWidth + multiline + /> + setDefaultPriority(value)} + fullWidth + /> + + )} - - -
- -
-
+ {returnToken ? ( + + ) : ( + + )} + {returnToken ? ( + + ) : ( + +
+ +
+
+ )}
); diff --git a/ui/src/application/AppStore.ts b/ui/src/application/AppStore.ts index f14eb6c..9fbb558 100644 --- a/ui/src/application/AppStore.ts +++ b/ui/src/application/AppStore.ts @@ -88,14 +88,15 @@ export class AppStore extends BaseStore { name: string, description: string, defaultPriority: number - ): Promise => { - await axios.post(`${config.get('url')}application`, { + ): Promise => { + const response = await axios.post(`${config.get('url')}application`, { name, description, defaultPriority, }); await this.refresh(); this.snack('Application created'); + return response.data.token; }; public getName = (id: number): string => { diff --git a/ui/src/application/Applications.tsx b/ui/src/application/Applications.tsx index af44d01..a535b58 100644 --- a/ui/src/application/Applications.tsx +++ b/ui/src/application/Applications.tsx @@ -26,7 +26,6 @@ import {CSS} from '@dnd-kit/utilities'; import ConfirmDialog from '../common/ConfirmDialog'; import DefaultPage from '../common/DefaultPage'; -import CopyableSecret from '../common/CopyableSecret'; import {AddApplicationDialog} from './AddApplicationDialog'; import * as config from '../config'; import {UpdateApplicationDialog} from './UpdateApplicationDialog'; @@ -124,7 +123,6 @@ const Applications = observer(() => { Name - Token Description Priority Last Used @@ -255,9 +253,6 @@ const Row = ({app, fDelete, fUpload, fDeleteImage, fEdit}: IRowProps) => { {app.name} - - - {app.description} {app.defaultPriority} diff --git a/ui/src/client/AddClientDialog.tsx b/ui/src/client/AddClientDialog.tsx index 22cb320..7a5c066 100644 --- a/ui/src/client/AddClientDialog.tsx +++ b/ui/src/client/AddClientDialog.tsx @@ -7,59 +7,85 @@ import DialogTitle from '@mui/material/DialogTitle'; import TextField from '@mui/material/TextField'; import Tooltip from '@mui/material/Tooltip'; import {NumberField} from '../common/NumberField'; +import {DialogContentText, Typography} from '@mui/material'; +import {copyToClipboard} from '../clipboard'; interface IProps { fClose: VoidFunction; - fOnSubmit: (name: string, expiresAfterInactivitySeconds: number) => Promise; + fOnSubmit: (name: string, expiresAfterInactivitySeconds: number) => Promise; } const AddClientDialog = ({fClose, fOnSubmit}: IProps) => { + const [returnToken, setReturnToken] = useState(''); const [name, setName] = useState(''); const [expiresAfter, setExpiresAfter] = useState(0); const submitEnabled = name.length !== 0; - const submitAndClose = async () => { - await fOnSubmit(name, Math.max(0, expiresAfter)); - fClose(); + const submitAndNext = async () => { + const token = await fOnSubmit(name, Math.max(0, expiresAfter)); + setReturnToken(token); }; return ( Create a client - setName(e.target.value)} - fullWidth - /> - setExpiresAfter(value)} - fullWidth - /> + {returnToken ? ( + + Your token is: + + {returnToken} + + + ) : ( + <> + setName(e.target.value)} + fullWidth + /> + setExpiresAfter(value)} + fullWidth + /> + + )} - - -
- -
-
+ {returnToken ? ( + + ) : ( + + )} + {returnToken ? ( + + ) : ( + +
+ +
+
+ )}
); diff --git a/ui/src/client/ClientStore.ts b/ui/src/client/ClientStore.ts index b96dff5..115fd56 100644 --- a/ui/src/client/ClientStore.ts +++ b/ui/src/client/ClientStore.ts @@ -47,9 +47,10 @@ export class ClientStore extends BaseStore { }; @action - public create = async (name: string, expiresAfterInactivitySeconds = 0): Promise => { - await this.createNoNotifcation(name, expiresAfterInactivitySeconds); + public create = async (name: string, expiresAfterInactivitySeconds = 0): Promise => { + const client = await this.createNoNotifcation(name, expiresAfterInactivitySeconds); this.snack('Client added'); + return client.token; }; @action diff --git a/ui/src/client/Clients.tsx b/ui/src/client/Clients.tsx index 4c4d33c..0185768 100644 --- a/ui/src/client/Clients.tsx +++ b/ui/src/client/Clients.tsx @@ -18,7 +18,6 @@ import AddClientDialog from './AddClientDialog'; import UpdateClientDialog from './UpdateClientDialog'; import ElevateClientDialog from './ElevateClientDialog'; import {IClient} from '../types'; -import CopyableSecret from '../common/CopyableSecret'; import {LastUsedCell} from '../common/LastUsedCell'; import {formatDate} from '../common/TimeAgoFormatter'; import {RemainingTime} from '../common/RemainingTime'; @@ -54,7 +53,6 @@ const Clients = observer(() => { Name - Token Elevation ends Expires in Last Used @@ -69,7 +67,6 @@ const Clients = observer(() => { { interface IRowProps { name: string; - value: string; createdAt: string; lastUsed: string | null; elevatedUntil?: string; @@ -135,7 +131,6 @@ interface IRowProps { const Row = ({ name, - value, createdAt, lastUsed, elevatedUntil, @@ -146,12 +141,6 @@ const Row = ({ }: IRowProps) => ( {name} - - - { + try { + await navigator.clipboard.writeText(value); + } catch (error) { + console.error('Failed to copy to clipboard:', error); + try { + const elem = document.createElement('textarea'); + elem.value = value; + document.body.appendChild(elem); + elem.select(); + document.execCommand('copy'); + document.body.removeChild(elem); + } catch (error) { + console.error('Failed to copy to clipboard (fallback):', error); + } + } +}; diff --git a/ui/src/common/CopyableSecret.tsx b/ui/src/common/CopyableSecret.tsx index 0c1dfbd..6acb9bf 100644 --- a/ui/src/common/CopyableSecret.tsx +++ b/ui/src/common/CopyableSecret.tsx @@ -5,6 +5,7 @@ import Copy from '@mui/icons-material/FileCopyOutlined'; import VisibilityOff from '@mui/icons-material/VisibilityOff'; import React, {CSSProperties} from 'react'; import {useStores} from '../stores'; +import {copyToClipboard} from '../clipboard'; interface IProps { value: string; @@ -16,18 +17,14 @@ const CopyableSecret = ({value, style}: IProps) => { const text = visible ? value : '•••••••••••••••'; const {snackManager} = useStores(); const toggleVisibility = () => setVisible((b) => !b); - const copyToClipboard = async () => { - try { - await navigator.clipboard.writeText(value); - snackManager.snack('Copied to clipboard'); - } catch (error) { - console.error('Failed to copy to clipboard:', error); - snackManager.snack('Failed to copy to clipboard'); - } - }; return (
- + + copyToClipboard(value).finally(() => snackManager.snack('Copied to clipboard')) + } + title="Copy to clipboard" + size="large">