1- import axios , { AxiosError , AxiosResponse } from 'axios' ;
21import * as config from './config' ;
3- import { detect } from 'detect-browser' ;
4- import { SnackReporter } from './snack/SnackManager' ;
5- import { observable , runInAction , action } from 'mobx' ;
6- import { IClient , IUser } from './types' ;
2+ import { detect } from 'detect-browser' ;
3+ import { SnackReporter } from './snack/SnackManager' ;
4+ import { observable , runInAction , action } from 'mobx' ;
5+ import { IClient , IUser } from './types' ;
6+ import { jsonBody , jsonTransform , ResponseTransformer } from './apiAuth' ;
77
88const tokenKey = 'gotify-login-key' ;
99
@@ -14,10 +14,10 @@ export class CurrentUser {
1414 @observable accessor loggedIn = false ;
1515 @observable accessor refreshKey = 0 ;
1616 @observable accessor authenticating = true ;
17- @observable accessor user : IUser = { name : 'unknown' , admin : false , id : - 1 } ;
17+ @observable accessor user : IUser = { name : 'unknown' , admin : false , id : - 1 } ;
1818 @observable accessor connectionErrorMessage : string | null = null ;
1919
20- public constructor ( private readonly snack : SnackReporter ) { }
20+ public constructor ( private readonly snack : SnackReporter ) { }
2121
2222 public token = ( ) : string => {
2323 if ( this . tokenCache !== null ) {
@@ -33,32 +33,74 @@ 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' ) ) headers . set ( 'X-Gotify-Key' , this . token ( ) ) ;
43+ let response ;
44+ try {
45+ response = await fetch ( url , { ...init , headers } ) ;
46+ } catch ( error ) {
47+ this . snack ( 'Gotify server is not reachable, try refreshing the page.' ) ;
48+ throw error ;
49+ }
50+ if ( response . ok ) {
51+ try {
52+ return xform ( response ) ;
53+ } catch ( error ) {
54+ this . snack ( 'Response transformation failed: ' + error ) ;
55+ throw error ;
56+ }
57+ }
58+ if ( response . status === 401 ) {
59+ this . tryAuthenticate ( ) . then ( ( ) => this . snack ( 'Could not complete request.' ) ) ;
60+ }
61+
62+ let error = 'Unexpected status code: ' + response . status ;
63+ if ( response . status === 400 || response . status === 403 || response . status === 500 ) {
64+ if ( response . headers . get ( 'content-type' ) ?. includes ( 'application/json' ) ) {
65+ const data = await response . json ( ) ;
66+ error = data . error + ': ' + data . errorDescription ;
67+ } else {
68+ const text = await response . text ( ) ;
69+ error = 'Unexpected response: ' + text ;
70+ }
71+ }
72+ this . snack ( error ) ;
73+ throw new Error ( error ) ;
74+ } ;
75+
3676 private readonly setToken = ( token : string ) => {
3777 this . tokenCache = token ;
3878 window . localStorage . setItem ( tokenKey , token ) ;
3979 } ;
4080
41- public register = async ( name : string , pass : string ) : Promise < boolean > =>
42- axios
43- . create ( )
44- . post ( config . get ( 'url' ) + 'user' , { name, pass} )
81+ public register = async ( name : string , pass : string ) : Promise < boolean > => {
82+ runInAction ( ( ) => {
83+ this . loggedIn = false ;
84+ } ) ;
85+ return this . authenticatedFetch (
86+ config . get ( 'url' ) + 'user' ,
87+ jsonBody ( { name, pass } ) ,
88+ jsonTransform
89+ )
4590 . then ( ( ) => {
4691 this . snack ( 'User Created. Logging in...' ) ;
4792 this . login ( name , pass ) ;
4893 return true ;
4994 } )
50- . catch ( ( error : AxiosError < { error ?: string ; errorDescription ?: string } > ) => {
51- if ( ! error || ! error . response ) {
95+ . catch ( ( error ) => {
96+ if ( error instanceof TypeError ) {
5297 this . snack ( 'No network connection or server unavailable.' ) ;
5398 return false ;
5499 }
55- const { data} = error . response ;
56-
57- this . snack (
58- `Register failed: ${ data ?. error ?? 'unknown' } : ${ data ?. errorDescription ?? '' } `
59- ) ;
100+ this . snack ( `Register failed: ${ error ?. message ?? error } ` ) ;
60101 return false ;
61102 } ) ;
103+ } ;
62104
63105 public login = async ( username : string , password : string ) => {
64106 runInAction ( ( ) => {
@@ -67,17 +109,17 @@ export class CurrentUser {
67109 } ) ;
68110 const browser = detect ( ) ;
69111 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 > ) => {
112+ const fetchInit = jsonBody ( { name } ) ;
113+ fetchInit . headers = new Headers ( fetchInit . headers ) ;
114+ fetchInit . headers . set ( 'Authorization' , 'Basic ' + btoa ( username + ':' + password ) ) ;
115+ return this . authenticatedFetch (
116+ config . get ( 'url' ) + 'client ',
117+ fetchInit ,
118+ jsonTransform < IClient >
119+ )
120+ . then ( ( resp ) => {
79121 this . snack ( `A client named '${ name } ' was created for your session.` ) ;
80- this . setToken ( resp . data . token ) ;
122+ this . setToken ( resp . token ) ;
81123 this . tryAuthenticate ( ) . catch ( ( ) => {
82124 console . log (
83125 'create client succeeded, but authenticated with given token failed'
@@ -92,59 +134,58 @@ export class CurrentUser {
92134 ) ;
93135 } ;
94136
95- public tryAuthenticate = async ( ) : Promise < AxiosResponse < IUser > > => {
137+ public tryAuthenticate = async ( ) : Promise < IUser > => {
96138 if ( this . token ( ) === '' ) {
97139 runInAction ( ( ) => {
98140 this . authenticating = false ;
99141 } ) ;
100142 return Promise . reject ( ) ;
101143 }
102144
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- )
145+ return fetch ( config . get ( 'url' ) + 'current/user' , { headers : { 'X-Gotify-Key' : this . token ( ) } } )
146+ . then ( async ( response ) => {
147+ if ( response . ok ) {
148+ const user = await response . json ( ) ;
149+ runInAction ( ( ) => {
150+ this . user = user ;
151+ this . loggedIn = true ;
152+ this . authenticating = false ;
153+ this . connectionErrorMessage = null ;
154+ this . reconnectTime = 7500 ;
155+ } ) ;
156+ return user ;
157+ }
158+ if ( response . status >= 500 ) {
159+ this . connectionError ( `${ response . statusText } (code: ${ response . status } ).` ) ;
160+ return Promise . reject ( new Error ( 'Server error' ) ) ;
161+ }
162+
163+ this . connectionErrorMessage = null ;
164+
165+ if ( response . status >= 400 && response . status < 500 ) {
166+ this . logout ( ) ;
167+ }
168+ throw new Error ( 'Unexpected status code: ' + response . status ) ;
169+ } )
116170 . catch (
117- action ( ( error : AxiosError ) => {
171+ action ( ( error ) => {
118172 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- }
173+ this . connectionError ( 'No network connection or server unavailable.' ) ;
136174 return Promise . reject ( error ) ;
137175 } )
138176 ) ;
139177 } ;
140178
141179 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 ) ) ;
180+ await this . authenticatedFetch ( config . get ( 'url' ) + 'client' , { } , jsonTransform < IClient [ ] > )
181+ . then ( ( resp ) => {
182+ resp . filter ( ( client ) => client . token === this . tokenCache ) . forEach ( ( client ) =>
183+ this . authenticatedFetch (
184+ config . get ( 'url' ) + 'client/' + client . id ,
185+ { } ,
186+ jsonTransform
187+ )
188+ ) ;
148189 } )
149190 . catch ( ( ) => Promise . resolve ( ) ) ;
150191 window . localStorage . removeItem ( tokenKey ) ;
@@ -155,9 +196,15 @@ export class CurrentUser {
155196 } ;
156197
157198 public changePassword = ( pass : string ) => {
158- axios
159- . post ( config . get ( 'url' ) + 'current/user/password' , { pass} )
160- . then ( ( ) => this . snack ( 'Password changed' ) ) ;
199+ this . authenticatedFetch (
200+ config . get ( 'url' ) + 'current/user/password' ,
201+ jsonBody ( { pass } ) ,
202+ jsonTransform
203+ )
204+ . then ( ( ) => this . snack ( 'Password changed' ) )
205+ . catch ( ( error ) => {
206+ this . snack ( `Change password failed: ${ error ?. message ?? error } ` ) ;
207+ } ) ;
161208 } ;
162209
163210 public tryReconnect = ( quiet = false ) => {
0 commit comments