mirror of
https://github.com/gotify/server.git
synced 2026-06-21 23:52:46 +08:00
[skip ci]: remove token from api output
This commit is contained in:
@@ -143,6 +143,7 @@ func (a *ApplicationAPI) GetApplications(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
for _, app := range apps {
|
||||
app.Token = ""
|
||||
withResolvedImage(app)
|
||||
}
|
||||
ctx.JSON(200, apps)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
17
ui/src/clipboard.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user