WIP: Auth
This commit is contained in:
		
							parent
							
								
									42016f48ff
								
							
						
					
					
						commit
						1599793de2
					
				
					 5 changed files with 108 additions and 32 deletions
				
			
		|  | @ -1,4 +1,4 @@ | ||||||
| import {topicUrlJsonPoll, fetchLinesIterator, topicUrl} from "./utils"; | import {topicUrlJsonPoll, fetchLinesIterator, topicUrl, topicUrlAuth} from "./utils"; | ||||||
| 
 | 
 | ||||||
| class Api { | class Api { | ||||||
|     async poll(baseUrl, topic) { |     async poll(baseUrl, topic) { | ||||||
|  | @ -19,6 +19,20 @@ class Api { | ||||||
|             body: message |             body: message | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     async auth(baseUrl, topic, user) { | ||||||
|  |         const url = topicUrlAuth(baseUrl, topic); | ||||||
|  |         console.log(`[Api] Checking auth for ${url}`); | ||||||
|  |         const response = await fetch(url); | ||||||
|  |         if (response.status >= 200 && response.status <= 299) { | ||||||
|  |             return true; | ||||||
|  |         } else if (!user && response.status === 404) { | ||||||
|  |             return true; // Special case: Anonymous login to old servers return 404 since /<topic>/auth doesn't exist
 | ||||||
|  |         } else if (response.status === 401 || response.status === 403) { // See server/server.go
 | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         throw new Error(`Unexpected server response ${response.status}`); | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const api = new Api(); | const api = new Api(); | ||||||
|  |  | ||||||
|  | @ -10,8 +10,7 @@ class Subscriptions { | ||||||
| 
 | 
 | ||||||
|     get(subscriptionId) { |     get(subscriptionId) { | ||||||
|         const subscription = this.subscriptions.get(subscriptionId); |         const subscription = this.subscriptions.get(subscriptionId); | ||||||
|         if (subscription === undefined) return null; |         return (subscription) ? subscription : null; | ||||||
|         return subscription; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     update(subscription) { |     update(subscription) { | ||||||
|  | @ -38,8 +37,7 @@ class Subscriptions { | ||||||
| 
 | 
 | ||||||
|     firstOrNull() { |     firstOrNull() { | ||||||
|         const first = this.subscriptions.values().next().value; |         const first = this.subscriptions.values().next().value; | ||||||
|         if (first === undefined) return null; |         return (first) ? first : null; | ||||||
|         return first; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     size() { |     size() { | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ export const topicUrlWs = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/ws` | ||||||
| export const topicUrlWsWithSince = (baseUrl, topic, since) => `${topicUrlWs(baseUrl, topic)}?since=${since}`; | export const topicUrlWsWithSince = (baseUrl, topic, since) => `${topicUrlWs(baseUrl, topic)}?since=${since}`; | ||||||
| export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/json`; | export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/json`; | ||||||
| export const topicUrlJsonPoll = (baseUrl, topic) => `${topicUrlJson(baseUrl, topic)}?poll=1`; | export const topicUrlJsonPoll = (baseUrl, topic) => `${topicUrlJson(baseUrl, topic)}?poll=1`; | ||||||
|  | export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`; | ||||||
| export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); | export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); | ||||||
| export const shortTopicUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic)); | export const shortTopicUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic)); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import Container from "@mui/material/Container"; | import Container from "@mui/material/Container"; | ||||||
| import {CardContent, CardHeader, Stack} from "@mui/material"; | import {CardContent, Stack} from "@mui/material"; | ||||||
| import Card from "@mui/material/Card"; | import Card from "@mui/material/Card"; | ||||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||||
| import * as React from "react"; | import * as React from "react"; | ||||||
| import {formatTitle, formatMessage, unmatchedTags} from "../app/utils"; | import {formatMessage, formatTitle, unmatchedTags} from "../app/utils"; | ||||||
| import IconButton from "@mui/material/IconButton"; | import IconButton from "@mui/material/IconButton"; | ||||||
| import CloseIcon from '@mui/icons-material/Close'; | import CloseIcon from '@mui/icons-material/Close'; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,47 +8,110 @@ import DialogContentText from '@mui/material/DialogContentText'; | ||||||
| import DialogTitle from '@mui/material/DialogTitle'; | import DialogTitle from '@mui/material/DialogTitle'; | ||||||
| import {useState} from "react"; | import {useState} from "react"; | ||||||
| import Subscription from "../app/Subscription"; | import Subscription from "../app/Subscription"; | ||||||
|  | import {useMediaQuery} from "@mui/material"; | ||||||
|  | import theme from "./theme"; | ||||||
|  | import api from "../app/Api"; | ||||||
|  | import {topicUrl} from "../app/utils"; | ||||||
| 
 | 
 | ||||||
| const defaultBaseUrl = "http://127.0.0.1" | const defaultBaseUrl = "http://127.0.0.1" | ||||||
| //const defaultBaseUrl = "https://ntfy.sh"
 | //const defaultBaseUrl = "https://ntfy.sh"
 | ||||||
| 
 | 
 | ||||||
| const SubscribeDialog = (props) => { | const SubscribeDialog = (props) => { | ||||||
|  |     const [baseUrl, setBaseUrl] = useState(defaultBaseUrl); // FIXME
 | ||||||
|     const [topic, setTopic] = useState(""); |     const [topic, setTopic] = useState(""); | ||||||
|  |     const [showLoginPage, setShowLoginPage] = useState(false); | ||||||
|  |     const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         setTopic(''); |         setTopic(''); | ||||||
|         props.onCancel(); |         props.onCancel(); | ||||||
|     } |     } | ||||||
|     const handleSubmit = () => { |     const handleSubmit = async () => { | ||||||
|  |         const success = await api.auth(baseUrl, topic, null); | ||||||
|  |         if (!success) { | ||||||
|  |             console.log(`[SubscribeDialog] Login required for ${topicUrl(baseUrl, topic)}`) | ||||||
|  |             setShowLoginPage(true); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|         const subscription = new Subscription(defaultBaseUrl, topic); |         const subscription = new Subscription(defaultBaseUrl, topic); | ||||||
|         props.onSubmit(subscription); |         props.onSubmit(subscription); | ||||||
|         setTopic(''); |         setTopic(''); | ||||||
|     } |     } | ||||||
|  |     return ( | ||||||
|  |         <Dialog open={props.open} onClose={props.onClose} fullScreen={fullScreen}> | ||||||
|  |             {!showLoginPage && <SubscribePage | ||||||
|  |                 topic={topic} | ||||||
|  |                 setTopic={setTopic} | ||||||
|  |                 onCancel={handleCancel} | ||||||
|  |                 onSubmit={handleSubmit} | ||||||
|  |             />} | ||||||
|  |             {showLoginPage && <LoginPage | ||||||
|  |                 topic={topic} | ||||||
|  |                 onBack={() => setShowLoginPage(false)} | ||||||
|  |             />} | ||||||
|  |         </Dialog> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const SubscribePage = (props) => { | ||||||
|     return ( |     return ( | ||||||
|         <> |         <> | ||||||
|             <Dialog open={props.open} onClose={props.onClose}> |             <DialogTitle>Subscribe to topic</DialogTitle> | ||||||
|                 <DialogTitle>Subscribe to topic</DialogTitle> |             <DialogContent> | ||||||
|                 <DialogContent> |                 <DialogContentText> | ||||||
|                     <DialogContentText> |                     Topics may not be password-protected, so choose a name that's not easy to guess. | ||||||
|                         Topics may not be password-protected, so choose a name that's not easy to guess. |                     Once subscribed, you can PUT/POST notifications. | ||||||
|                         Once subscribed, you can PUT/POST notifications. |                 </DialogContentText> | ||||||
|                     </DialogContentText> |                 <TextField | ||||||
|                     <TextField |                     autoFocus | ||||||
|                         autoFocus |                     margin="dense" | ||||||
|                         margin="dense" |                     id="topic" | ||||||
|                         id="name" |                     label="Topic name, e.g. phil_alerts" | ||||||
|                         label="Topic name, e.g. phil_alerts" |                     value={props.topic} | ||||||
|                         value={topic} |                     onChange={ev => props.setTopic(ev.target.value)} | ||||||
|                         onChange={ev => setTopic(ev.target.value)} |                     type="text" | ||||||
|                         type="text" |                     fullWidth | ||||||
|                         fullWidth |                     variant="standard" | ||||||
|                         variant="standard" |                 /> | ||||||
|                     /> |             </DialogContent> | ||||||
|                 </DialogContent> |             <DialogActions> | ||||||
|                 <DialogActions> |                 <Button onClick={props.onCancel}>Cancel</Button> | ||||||
|                     <Button onClick={handleCancel}>Cancel</Button> |                 <Button onClick={props.onSubmit} disabled={props.topic === ""}>Subscribe</Button> | ||||||
|                     <Button onClick={handleSubmit} autoFocus disabled={topic === ""}>Subscribe</Button> |             </DialogActions> | ||||||
|                 </DialogActions> |         </> | ||||||
|             </Dialog> |     ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const LoginPage = (props) => { | ||||||
|  |     return ( | ||||||
|  |         <> | ||||||
|  |             <DialogTitle>Login required</DialogTitle> | ||||||
|  |             <DialogContent> | ||||||
|  |                 <DialogContentText> | ||||||
|  |                     This topic is password-protected. Please enter username and | ||||||
|  |                     password to subscribe. | ||||||
|  |                 </DialogContentText> | ||||||
|  |                 <TextField | ||||||
|  |                     autoFocus | ||||||
|  |                     margin="dense" | ||||||
|  |                     id="username" | ||||||
|  |                     label="Username, e.g. phil" | ||||||
|  |                     type="text" | ||||||
|  |                     fullWidth | ||||||
|  |                     variant="standard" | ||||||
|  |                 /> | ||||||
|  |                 <TextField | ||||||
|  |                     margin="dense" | ||||||
|  |                     id="password" | ||||||
|  |                     label="Password" | ||||||
|  |                     type="password" | ||||||
|  |                     fullWidth | ||||||
|  |                     variant="standard" | ||||||
|  |                 /> | ||||||
|  |             </DialogContent> | ||||||
|  |             <DialogActions> | ||||||
|  |                 <Button onClick={props.onBack}>Back</Button> | ||||||
|  |                 <Button>Login</Button> | ||||||
|  |             </DialogActions> | ||||||
|         </> |         </> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue