[skip ci]: remove token from api output

This commit is contained in:
Yumechi
2026-06-14 22:15:19 +08:00
parent 131aa218a7
commit a5da5b3bab
13 changed files with 174 additions and 112 deletions

View File

@@ -143,6 +143,7 @@ func (a *ApplicationAPI) GetApplications(ctx *gin.Context) {
return
}
for _, app := range apps {
app.Token = ""
withResolvedImage(app)
}
ctx.JSON(200, apps)

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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<void>;
fOnSubmit: (name: string, description: string, defaultPriority: number) => Promise<string>;
}
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 (
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title" id="app-dialog">
<DialogTitle id="form-dialog-title">Create an application</DialogTitle>
<DialogContent>
<DialogContentText>An application is allowed to send messages.</DialogContentText>
<TextField
autoFocus
margin="dense"
className="name"
label="Name *"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
fullWidth
/>
<TextField
margin="dense"
className="description"
label="Short Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
fullWidth
multiline
/>
<NumberField
margin="dense"
className="priority"
label="Default Priority"
value={defaultPriority}
onChange={(value) => setDefaultPriority(value)}
fullWidth
/>
{returnToken ? (
<DialogContentText>
Your token is:
<Typography variant="body1" style={{fontFamily: 'monospace', fontSize: 16}}>
{returnToken}
</Typography>
</DialogContentText>
) : (
<>
<DialogContentText>
An application is allowed to send messages.
</DialogContentText>
<TextField
autoFocus
margin="dense"
className="name"
label="Name *"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
fullWidth
/>
<TextField
margin="dense"
className="description"
label="Short Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
fullWidth
multiline
/>
<NumberField
margin="dense"
className="priority"
label="Default Priority"
value={defaultPriority}
onChange={(value) => setDefaultPriority(value)}
fullWidth
/>
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={fClose}>Cancel</Button>
<Tooltip title={submitEnabled ? '' : 'name is required'}>
<div>
<Button
className="create"
disabled={!submitEnabled}
onClick={submitAndClose}
color="primary"
variant="contained">
Create
</Button>
</div>
</Tooltip>
{returnToken ? (
<Button onClick={() => copyToClipboard(returnToken)}>Copy to clipboard</Button>
) : (
<Button onClick={fClose}>Cancel</Button>
)}
{returnToken ? (
<Button onClick={fClose} color="primary" variant="contained">
Close
</Button>
) : (
<Tooltip title={submitEnabled ? '' : 'name is required'}>
<div>
<Button
className="create"
disabled={!submitEnabled}
onClick={submitAndNext}
color="primary"
variant="contained">
Create
</Button>
</div>
</Tooltip>
)}
</DialogActions>
</Dialog>
);

View File

@@ -88,14 +88,15 @@ export class AppStore extends BaseStore<IApplication> {
name: string,
description: string,
defaultPriority: number
): Promise<void> => {
await axios.post(`${config.get('url')}application`, {
): Promise<string> => {
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 => {

View File

@@ -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(() => {
<TableCell padding="none" style={{width: 0}} />
<TableCell padding="checkbox" style={{width: 80}} />
<TableCell>Name</TableCell>
<TableCell>Token</TableCell>
<TableCell>Description</TableCell>
<TableCell>Priority</TableCell>
<TableCell>Last Used</TableCell>
@@ -255,9 +253,6 @@ const Row = ({app, fDelete, fUpload, fDeleteImage, fEdit}: IRowProps) => {
</div>
</TableCell>
<TableCell>{app.name}</TableCell>
<TableCell>
<CopyableSecret value={app.token} style={{display: 'flex', alignItems: 'center'}} />
</TableCell>
<TableCell>{app.description}</TableCell>
<TableCell>{app.defaultPriority}</TableCell>
<TableCell>

View File

@@ -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<void>;
fOnSubmit: (name: string, expiresAfterInactivitySeconds: number) => Promise<string>;
}
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 (
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title" id="client-dialog">
<DialogTitle id="form-dialog-title">Create a client</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
className="name"
label="Name *"
type="email"
value={name}
onChange={(e) => setName(e.target.value)}
fullWidth
/>
<NumberField
margin="dense"
className="expires-after"
label="Expires after inactivity (seconds, 0 = never)"
value={expiresAfter}
onChange={(value) => setExpiresAfter(value)}
fullWidth
/>
{returnToken ? (
<DialogContentText>
Your token is:
<Typography variant="body1" style={{fontFamily: 'monospace', fontSize: 16}}>
{returnToken}
</Typography>
</DialogContentText>
) : (
<>
<TextField
autoFocus
margin="dense"
className="name"
label="Name *"
type="email"
value={name}
onChange={(e) => setName(e.target.value)}
fullWidth
/>
<NumberField
margin="dense"
className="expires-after"
label="Expires after inactivity (seconds, 0 = never)"
value={expiresAfter}
onChange={(value) => setExpiresAfter(value)}
fullWidth
/>
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={fClose}>Cancel</Button>
<Tooltip placement={'bottom-start'} title={submitEnabled ? '' : 'name is required'}>
<div>
<Button
className="create"
disabled={!submitEnabled}
onClick={submitAndClose}
color="primary"
variant="contained">
Create
</Button>
</div>
</Tooltip>
{returnToken ? (
<Button onClick={() => copyToClipboard(returnToken)}>Copy to clipboard</Button>
) : (
<Button onClick={fClose}>Cancel</Button>
)}
{returnToken ? (
<Button onClick={fClose} color="primary" variant="contained">
Close
</Button>
) : (
<Tooltip
placement={'bottom-start'}
title={submitEnabled ? '' : 'name is required'}>
<div>
<Button
className="create"
disabled={!submitEnabled}
onClick={submitAndNext}
color="primary"
variant="contained">
Create
</Button>
</div>
</Tooltip>
)}
</DialogActions>
</Dialog>
);

View File

@@ -47,9 +47,10 @@ export class ClientStore extends BaseStore<IClient> {
};
@action
public create = async (name: string, expiresAfterInactivitySeconds = 0): Promise<void> => {
await this.createNoNotifcation(name, expiresAfterInactivitySeconds);
public create = async (name: string, expiresAfterInactivitySeconds = 0): Promise<string> => {
const client = await this.createNoNotifcation(name, expiresAfterInactivitySeconds);
this.snack('Client added');
return client.token;
};
@action

View File

@@ -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(() => {
<TableHead>
<TableRow style={{textAlign: 'center'}}>
<TableCell>Name</TableCell>
<TableCell style={{width: 200}}>Token</TableCell>
<TableCell>Elevation ends</TableCell>
<TableCell>Expires in</TableCell>
<TableCell>Last Used</TableCell>
@@ -69,7 +67,6 @@ const Clients = observer(() => {
<Row
key={client.id}
name={client.name}
value={client.token}
createdAt={client.createdAt}
lastUsed={client.lastUsed}
elevatedUntil={client.elevatedUntil}
@@ -123,7 +120,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) => (
<TableRow>
<TableCell>{name}</TableCell>
<TableCell>
<CopyableSecret
value={value}
style={{display: 'flex', alignItems: 'center', width: 250}}
/>
</TableCell>
<TableCell align="right" title={elevatedUntil}>
<RemainingTime
until={

17
ui/src/clipboard.ts Normal file
View File

@@ -0,0 +1,17 @@
export const copyToClipboard = async (value: string) => {
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);
}
}
};

View File

@@ -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 (
<div style={style}>
<IconButton onClick={copyToClipboard} title="Copy to clipboard" size="large">
<IconButton
onClick={() =>
copyToClipboard(value).finally(() => snackManager.snack('Copied to clipboard'))
}
title="Copy to clipboard"
size="large">
<Copy />
</IconButton>
<IconButton onClick={toggleVisibility} className="toggle-visibility" size="large">