Login page of "subscribe dialog", still WIP, but looking nice
This commit is contained in:
		
							parent
							
								
									1599793de2
								
							
						
					
					
						commit
						6d343c0f1a
					
				
					 8 changed files with 366 additions and 11 deletions
				
			
		|  | @ -23,7 +23,10 @@ class Api { | |||
|     async auth(baseUrl, topic, user) { | ||||
|         const url = topicUrlAuth(baseUrl, topic); | ||||
|         console.log(`[Api] Checking auth for ${url}`); | ||||
|         const response = await fetch(url); | ||||
|         const headers = this.maybeAddAuthorization({}, user); | ||||
|         const response = await fetch(url, { | ||||
|             headers: headers | ||||
|         }); | ||||
|         if (response.status >= 200 && response.status <= 299) { | ||||
|             return true; | ||||
|         } else if (!user && response.status === 404) { | ||||
|  | @ -33,6 +36,14 @@ class Api { | |||
|         } | ||||
|         throw new Error(`Unexpected server response ${response.status}`); | ||||
|     } | ||||
| 
 | ||||
|     maybeAddAuthorization(headers, user) { | ||||
|         if (user) { | ||||
|             const encoded = new Buffer(`${user.username}:${user.password}`).toString('base64'); | ||||
|             headers['Authorization'] = `Basic ${encoded}`; | ||||
|         } | ||||
|         return headers; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| const api = new Api(); | ||||
|  |  | |||
|  | @ -4,11 +4,11 @@ import Subscriptions from "./Subscriptions"; | |||
| export class Repository { | ||||
|     loadSubscriptions() { | ||||
|         console.log(`[Repository] Loading subscriptions from localStorage`); | ||||
| 
 | ||||
|         const subscriptions = new Subscriptions(); | ||||
|         const serialized = localStorage.getItem('subscriptions'); | ||||
|         if (serialized === null) return subscriptions; | ||||
| 
 | ||||
|         if (serialized === null) { | ||||
|             return subscriptions; | ||||
|         } | ||||
|         try { | ||||
|             const serializedSubscriptions = JSON.parse(serialized); | ||||
|             serializedSubscriptions.forEach(s => { | ||||
|  | @ -26,7 +26,6 @@ export class Repository { | |||
| 
 | ||||
|     saveSubscriptions(subscriptions) { | ||||
|         console.log(`[Repository] Saving ${subscriptions.size()} subscription(s) to localStorage`); | ||||
| 
 | ||||
|         const serialized = JSON.stringify(subscriptions.map( (id, subscription) => { | ||||
|             return { | ||||
|                 baseUrl: subscription.baseUrl, | ||||
|  | @ -37,6 +36,30 @@ export class Repository { | |||
|         })); | ||||
|         localStorage.setItem('subscriptions', serialized); | ||||
|     } | ||||
| 
 | ||||
|     loadUsers() { | ||||
|         console.log(`[Repository] Loading users from localStorage`); | ||||
|         const serialized = localStorage.getItem('users'); | ||||
|         if (serialized === null) { | ||||
|             return {}; | ||||
|         } | ||||
|         try { | ||||
|             return JSON.parse(serialized); | ||||
|         } catch (e) { | ||||
|             console.log(`[Repository] Unable to deserialize users: ${e.message}`); | ||||
|             return {}; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     saveUser(baseUrl, username, password) { | ||||
|         console.log(`[Repository] Saving users to localStorage`); | ||||
|         const users = this.loadUsers(); | ||||
|         users[baseUrl] = { | ||||
|             username: username, | ||||
|             password: password | ||||
|         }; | ||||
|         localStorage.setItem('users', users); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| const repository = new Repository(); | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ import {useMediaQuery} from "@mui/material"; | |||
| import theme from "./theme"; | ||||
| import api from "../app/Api"; | ||||
| import {topicUrl} from "../app/utils"; | ||||
| import useStyles from "./styles"; | ||||
| 
 | ||||
| const defaultBaseUrl = "http://127.0.0.1" | ||||
| //const defaultBaseUrl = "https://ntfy.sh"
 | ||||
|  | @ -19,6 +20,7 @@ const defaultBaseUrl = "http://127.0.0.1" | |||
| const SubscribeDialog = (props) => { | ||||
|     const [baseUrl, setBaseUrl] = useState(defaultBaseUrl); // FIXME
 | ||||
|     const [topic, setTopic] = useState(""); | ||||
|     const [user, setUser] = useState(null); | ||||
|     const [showLoginPage, setShowLoginPage] = useState(false); | ||||
|     const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); | ||||
|     const handleCancel = () => { | ||||
|  | @ -45,8 +47,10 @@ const SubscribeDialog = (props) => { | |||
|                 onSubmit={handleSubmit} | ||||
|             />} | ||||
|             {showLoginPage && <LoginPage | ||||
|                 baseUrl={baseUrl} | ||||
|                 topic={topic} | ||||
|                 onBack={() => setShowLoginPage(false)} | ||||
|                 onSubmit={handleSubmit} | ||||
|             />} | ||||
|         </Dialog> | ||||
|     ); | ||||
|  | @ -82,6 +86,22 @@ const SubscribePage = (props) => { | |||
| }; | ||||
| 
 | ||||
| const LoginPage = (props) => { | ||||
|     const styles = useStyles(); | ||||
|     const [username, setUsername] = useState(""); | ||||
|     const [password, setPassword] = useState(""); | ||||
|     const [errorText, setErrorText] = useState(""); | ||||
|     const baseUrl = props.baseUrl; | ||||
|     const topic = props.topic; | ||||
|     const handleLogin = async () => { | ||||
|         const user = {username: username, password: password}; | ||||
|         const success = await api.auth(baseUrl, topic, user); | ||||
|         if (!success) { | ||||
|             console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); | ||||
|             setErrorText(`User ${username} not authorized`); | ||||
|             return; | ||||
|         } | ||||
|         console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} successful for user ${username}`); | ||||
|     }; | ||||
|     return ( | ||||
|         <> | ||||
|             <DialogTitle>Login required</DialogTitle> | ||||
|  | @ -95,6 +115,8 @@ const LoginPage = (props) => { | |||
|                     margin="dense" | ||||
|                     id="username" | ||||
|                     label="Username, e.g. phil" | ||||
|                     value={username} | ||||
|                     onChange={ev => setUsername(ev.target.value)} | ||||
|                     type="text" | ||||
|                     fullWidth | ||||
|                     variant="standard" | ||||
|  | @ -104,14 +126,21 @@ const LoginPage = (props) => { | |||
|                     id="password" | ||||
|                     label="Password" | ||||
|                     type="password" | ||||
|                     value={password} | ||||
|                     onChange={ev => setPassword(ev.target.value)} | ||||
|                     fullWidth | ||||
|                     variant="standard" | ||||
|                 /> | ||||
|             </DialogContent> | ||||
|             <DialogActions> | ||||
|                 <Button onClick={props.onBack}>Back</Button> | ||||
|                 <Button>Login</Button> | ||||
|             </DialogActions> | ||||
|             <div className={styles.bottomBar}> | ||||
|                 <DialogContentText className={styles.statusText}> | ||||
|                     {errorText} | ||||
|                 </DialogContentText> | ||||
|                 <DialogActions> | ||||
|                     <Button onClick={props.onBack}>Back</Button> | ||||
|                     <Button onClick={handleLogin}>Login</Button> | ||||
|                 </DialogActions> | ||||
|             </div> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
|  |  | |||
							
								
								
									
										18
									
								
								web/src/components/styles.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								web/src/components/styles.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| import {makeStyles} from "@mui/styles"; | ||||
| 
 | ||||
| const useStyles = makeStyles(theme => ({ | ||||
|   bottomBar: { | ||||
|     display: 'flex', | ||||
|     flexDirection: 'row', | ||||
|     justifyContent: 'space-between', | ||||
|     paddingLeft: '24px', | ||||
|     paddingTop: '8px 24px', | ||||
|     paddingBottom: '8px 24px', | ||||
|   }, | ||||
|   statusText: { | ||||
|     margin: '0px', | ||||
|     paddingTop: '8px', | ||||
|   } | ||||
| })); | ||||
| 
 | ||||
| export default useStyles; | ||||
|  | @ -1,7 +1,6 @@ | |||
| import { red } from '@mui/material/colors'; | ||||
| import { createTheme } from '@mui/material/styles'; | ||||
| 
 | ||||
| // A custom theme for this app
 | ||||
| const theme = createTheme({ | ||||
|   palette: { | ||||
|     primary: { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue