Skip to content

Commit 7ff28ee

Browse files
chore: refactor out axios
1 parent 1818c5d commit 7ff28ee

File tree

14 files changed

+422
-430
lines changed

14 files changed

+422
-430
lines changed

ui/package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"@uiw/codemirror-theme-material": "^4.24.2",
1616
"@uiw/react-codemirror": "^4.24.2",
1717
"@vitejs/plugin-react": "^5.0.0",
18-
"axios": "^1.11.0",
1918
"detect-browser": "^5.3.0",
2019
"fractional-indexing": "^3.2.0",
2120
"mobx": "^6.13.7",
@@ -60,8 +59,7 @@
6059
"rimraf": "^6.0.1",
6160
"tree-kill": "^1.2.0",
6261
"typescript": "^5.9.2",
63-
"typescript-eslint": "^8.38.0",
64-
"wait-on": "^9.0.0"
62+
"typescript-eslint": "^8.38.0"
6563
},
6664
"browserslist": {
6765
"production": [
@@ -75,4 +73,4 @@
7573
"last 1 safari version"
7674
]
7775
}
78-
}
76+
}

ui/src/CurrentUser.ts

Lines changed: 111 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import axios, {AxiosError, AxiosResponse} from 'axios';
21
import * as config from './config';
32
import {detect} from 'detect-browser';
43
import {SnackReporter} from './snack/SnackManager';
54
import {observable, runInAction, action} from 'mobx';
65
import {IClient, IUser} from './types';
6+
import {identityTransform, jsonBody, jsonTransform, ResponseTransformer} from './apiAuth';
77

88
const tokenKey = 'gotify-login-key';
99

@@ -33,32 +33,75 @@ export class CurrentUser {
3333
return '';
3434
};
3535

36+
public authenticatedFetch = async <T>(
37+
url: string,
38+
init: RequestInit,
39+
xform: ResponseTransformer<T>
40+
): Promise<T> => {
41+
const headers = new Headers(init?.headers);
42+
if (this.loggedIn && !headers.has('X-Gotify-Key'))
43+
headers.set('X-Gotify-Key', this.token());
44+
let response;
45+
try {
46+
response = await fetch(url, {...init, headers});
47+
} catch (error) {
48+
this.snack('Gotify server is not reachable, try refreshing the page.');
49+
throw error;
50+
}
51+
if (response.ok) {
52+
try {
53+
return xform(response);
54+
} catch (error) {
55+
this.snack('Response transformation failed: ' + error);
56+
throw error;
57+
}
58+
}
59+
if (response.status === 401) {
60+
this.tryAuthenticate().then(() => this.snack('Could not complete request.'));
61+
}
62+
63+
let error = 'Unexpected status code: ' + response.status;
64+
if (response.status === 400 || response.status === 403 || response.status === 500) {
65+
if (response.headers.get('content-type')?.includes('application/json')) {
66+
const data = await response.json();
67+
error = data.error + ': ' + data.errorDescription;
68+
} else {
69+
const text = await response.text();
70+
error = 'Unexpected response: ' + text;
71+
}
72+
}
73+
this.snack(error);
74+
throw new Error(error);
75+
};
76+
3677
private readonly setToken = (token: string) => {
3778
this.tokenCache = token;
3879
window.localStorage.setItem(tokenKey, token);
3980
};
4081

41-
public register = async (name: string, pass: string): Promise<boolean> =>
42-
axios
43-
.create()
44-
.post(config.get('url') + 'user', {name, pass})
82+
public register = async (name: string, pass: string): Promise<boolean> => {
83+
runInAction(() => {
84+
this.loggedIn = false;
85+
});
86+
return this.authenticatedFetch(
87+
config.get('url') + 'user',
88+
jsonBody({name, pass}),
89+
identityTransform
90+
)
4591
.then(() => {
4692
this.snack('User Created. Logging in...');
4793
this.login(name, pass);
4894
return true;
4995
})
50-
.catch((error: AxiosError<{error?: string; errorDescription?: string}>) => {
51-
if (!error || !error.response) {
96+
.catch((error) => {
97+
if (error instanceof TypeError) {
5298
this.snack('No network connection or server unavailable.');
5399
return false;
54100
}
55-
const {data} = error.response;
56-
57-
this.snack(
58-
`Register failed: ${data?.error ?? 'unknown'}: ${data?.errorDescription ?? ''}`
59-
);
101+
this.snack(`Register failed: ${error?.message ?? error}`);
60102
return false;
61103
});
104+
};
62105

63106
public login = async (username: string, password: string) => {
64107
runInAction(() => {
@@ -67,17 +110,17 @@ export class CurrentUser {
67110
});
68111
const browser = detect();
69112
const name = (browser && browser.name + ' ' + browser.version) || 'unknown browser';
70-
axios
71-
.create()
72-
.request({
73-
url: config.get('url') + 'client',
74-
method: 'POST',
75-
data: {name},
76-
headers: {Authorization: 'Basic ' + btoa(username + ':' + password)},
77-
})
78-
.then((resp: AxiosResponse<IClient>) => {
113+
const fetchInit = jsonBody({name});
114+
fetchInit.headers = new Headers(fetchInit.headers);
115+
fetchInit.headers.set('Authorization', 'Basic ' + btoa(username + ':' + password));
116+
return this.authenticatedFetch(
117+
config.get('url') + 'client',
118+
fetchInit,
119+
jsonTransform<IClient>
120+
)
121+
.then((resp) => {
79122
this.snack(`A client named '${name}' was created for your session.`);
80-
this.setToken(resp.data.token);
123+
this.setToken(resp.token);
81124
this.tryAuthenticate().catch(() => {
82125
console.log(
83126
'create client succeeded, but authenticated with given token failed'
@@ -92,59 +135,58 @@ export class CurrentUser {
92135
);
93136
};
94137

95-
public tryAuthenticate = async (): Promise<AxiosResponse<IUser>> => {
138+
public tryAuthenticate = async (): Promise<IUser> => {
96139
if (this.token() === '') {
97140
runInAction(() => {
98141
this.authenticating = false;
99142
});
100143
return Promise.reject();
101144
}
102145

103-
return axios
104-
.create()
105-
.get(config.get('url') + 'current/user', {headers: {'X-Gotify-Key': this.token()}})
106-
.then(
107-
action((passThrough) => {
108-
this.user = passThrough.data;
109-
this.loggedIn = true;
110-
this.authenticating = false;
111-
this.connectionErrorMessage = null;
112-
this.reconnectTime = 7500;
113-
return passThrough;
114-
})
115-
)
146+
return fetch(config.get('url') + 'current/user', {headers: {'X-Gotify-Key': this.token()}})
147+
.then(async (response) => {
148+
if (response.ok) {
149+
const user = await response.json();
150+
runInAction(() => {
151+
this.user = user;
152+
this.loggedIn = true;
153+
this.authenticating = false;
154+
this.connectionErrorMessage = null;
155+
this.reconnectTime = 7500;
156+
});
157+
return user;
158+
}
159+
if (response.status >= 500) {
160+
this.connectionError(`${response.statusText} (code: ${response.status}).`);
161+
return Promise.reject(new Error('Server error'));
162+
}
163+
164+
this.connectionErrorMessage = null;
165+
166+
if (response.status >= 400 && response.status < 500) {
167+
this.logout();
168+
}
169+
throw new Error('Unexpected status code: ' + response.status);
170+
})
116171
.catch(
117-
action((error: AxiosError) => {
172+
action((error) => {
118173
this.authenticating = false;
119-
if (!error || !error.response) {
120-
this.connectionError('No network connection or server unavailable.');
121-
return Promise.reject(error);
122-
}
123-
124-
if (error.response.status >= 500) {
125-
this.connectionError(
126-
`${error.response.statusText} (code: ${error.response.status}).`
127-
);
128-
return Promise.reject(error);
129-
}
130-
131-
this.connectionErrorMessage = null;
132-
133-
if (error.response.status >= 400 && error.response.status < 500) {
134-
this.logout();
135-
}
174+
this.connectionError('No network connection or server unavailable.');
136175
return Promise.reject(error);
137176
})
138177
);
139178
};
140179

141180
public logout = async () => {
142-
await axios
143-
.get(config.get('url') + 'client')
144-
.then((resp: AxiosResponse<IClient[]>) => {
145-
resp.data
146-
.filter((client) => client.token === this.tokenCache)
147-
.forEach((client) => axios.delete(config.get('url') + 'client/' + client.id));
181+
await this.authenticatedFetch(config.get('url') + 'client', {}, jsonTransform<IClient[]>)
182+
.then((resp) => {
183+
resp.filter((client) => client.token === this.tokenCache).forEach((client) =>
184+
this.authenticatedFetch(
185+
config.get('url') + 'client/' + client.id,
186+
{},
187+
jsonTransform
188+
)
189+
);
148190
})
149191
.catch(() => Promise.resolve());
150192
window.localStorage.removeItem(tokenKey);
@@ -155,9 +197,15 @@ export class CurrentUser {
155197
};
156198

157199
public changePassword = (pass: string) => {
158-
axios
159-
.post(config.get('url') + 'current/user/password', {pass})
160-
.then(() => this.snack('Password changed'));
200+
this.authenticatedFetch(
201+
config.get('url') + 'current/user/password',
202+
jsonBody({pass}),
203+
identityTransform
204+
)
205+
.then(() => this.snack('Password changed'))
206+
.catch((error) => {
207+
this.snack(`Change password failed: ${error?.message ?? error}`);
208+
});
161209
};
162210

163211
public tryReconnect = (quiet = false) => {

ui/src/apiAuth.ts

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,30 @@
1-
import axios from 'axios';
2-
import {CurrentUser} from './CurrentUser';
3-
import {SnackReporter} from './snack/SnackManager';
1+
export type ResponseTransformer<T> = (response: Response) => Promise<T>;
42

5-
export const initAxios = (currentUser: CurrentUser, snack: SnackReporter) => {
6-
axios.interceptors.request.use((config) => {
7-
if (!config.headers.has('x-gotify-key')) {
8-
config.headers['x-gotify-key'] = currentUser.token();
9-
}
10-
return config;
11-
});
3+
export const identityTransform: ResponseTransformer<Response> = (response: Response) =>
4+
Promise.resolve(response);
125

13-
axios.interceptors.response.use(undefined, (error) => {
14-
if (!error.response) {
15-
snack('Gotify server is not reachable, try refreshing the page.');
16-
return Promise.reject(error);
17-
}
6+
export const jsonTransform = <T>(response: Response): Promise<T> => response.json();
187

19-
const status = error.response.status;
8+
export const textTransform = (response: Response): Promise<string> => response.text();
209

21-
if (status === 401) {
22-
currentUser.tryAuthenticate().then(() => snack('Could not complete request.'));
23-
}
10+
export const jsonBody: (body: any) => RequestInit = (body: any) => ({
11+
method: 'POST',
12+
headers: {
13+
'Content-Type': 'application/json',
14+
},
15+
body: JSON.stringify(body),
16+
});
2417

25-
if (status === 400 || status === 403 || status === 500) {
26-
snack(error.response.data.error + ': ' + error.response.data.errorDescription);
27-
}
18+
export const yamlBody: (text: string) => RequestInit = (text: string) => ({
19+
method: 'POST',
20+
headers: {
21+
'Content-Type': 'application/x-yaml',
22+
},
23+
body: text,
24+
});
2825

29-
return Promise.reject(error);
30-
});
31-
};
26+
export const multipartBody: (body: FormData) => RequestInit = (body: FormData) => ({
27+
method: 'POST',
28+
headers: { 'content-type': 'multipart/form-data' },
29+
body,
30+
});

0 commit comments

Comments
 (0)