diff options
| author | s-ol <s+removethis@s-ol.nu> | 2022-01-14 15:53:47 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2022-01-14 15:53:47 +0000 |
| commit | 569d322f516ddb7271f89222f11ede68caf7fefb (patch) | |
| tree | d826b0bab30595af161f588fc4b47068ce8be3c7 | |
| parent | small fixes (diff) | |
| download | fedidag-569d322f516ddb7271f89222f11ede68caf7fefb.tar.gz fedidag-569d322f516ddb7271f89222f11ede68caf7fefb.zip | |
login/logout
| -rw-r--r-- | client/src/index.js | 34 | ||||
| -rw-r--r-- | client/src/ui/Menu.js | 109 | ||||
| -rw-r--r-- | client/src/user.js | 71 | ||||
| -rw-r--r-- | server/main.ts | 5 |
4 files changed, 162 insertions, 57 deletions
diff --git a/client/src/index.js b/client/src/index.js index a30dbf1..0ca8b1c 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -4,6 +4,7 @@ import 'regenerator-runtime/runtime'; import { h, Fragment, Component, render } from 'preact'; import { GraphContainerJSONLD, GraphContainerMastodon } from './graph'; import { Menu, Discussion, Selection } from './ui'; +import { UserContainer } from './user'; class SelectionContainer extends Component { state = { @@ -134,29 +135,24 @@ const search = new URLSearchParams(window.location.search); const graph = search.has('graph') ? search.get('graph') : 'lib/graph.json'; let app; -if (search.has('document')) { - app = ( - <GraphContainerJSONLD - url={search.get('document')} - render={graphRender} - /> - ); -} else if (search.has('note')) { +if (search.has('document') || search.has('note') || search.has('graph')) { + const Container = search.has('document') ? GraphContainerJSONLD : GraphContainerMastodon; + const url = search.get('document') || search.get('note') || search.get('graph'); + app = ( - <GraphContainerMastodon - url={search.get('note')} - render={graphRender} - /> + <UserContainer> + <Container + url={url} + render={graphRender} + /> + </UserContainer> ); -} else if (search.has('graph')) { +} else { app = ( - <GraphContainerMastodon - url={search.get('graph')} - render={graphRender} - /> + <UserContainer> + <Menu /> + </UserContainer> ); -} else { - app = <Menu />; } render(app, document.body); diff --git a/client/src/ui/Menu.js b/client/src/ui/Menu.js index 6f32280..f3e912d 100644 --- a/client/src/ui/Menu.js +++ b/client/src/ui/Menu.js @@ -1,17 +1,9 @@ import { h, Fragment } from 'preact'; -import { useState, useEffect } from 'preact/hooks'; +import { useState } from 'preact/hooks'; import cn from 'classnames'; -import { API_PREFIX } from '../config'; +import { useUser, useUserCtx } from '../user'; import css from './css'; -const fetchJSON = async (url) => { - const res = await fetch(API_PREFIX + url, { credentials: 'include' }); - if (res.status !== 200) - throw new Error("wrong status"); - - return res.json(); -}; - css` a.discussion { display: flex; @@ -52,8 +44,9 @@ a.discussion > a:hover { } `; -const DiscussionLink = ({ id, name, url, attributedTo, user }) => { - const writable = attributedTo && attributedTo.indexOf(user) > -1; +const DiscussionLink = ({ id, name, url, attributedTo }) => { + const { user } = useUser(); + const writable = user && attributedTo && attributedTo.indexOf(user.id) > -1; return ( <a class="discussion" href={`?document=${id}`}> @@ -79,12 +72,12 @@ ul.discussions > li { } `; -const DiscussionList = ({ discussions, user }) => { +const DiscussionList = ({ discussions }) => { return ( <ul class="discussions"> {discussions.map((discussion) => ( <li> - <DiscussionLink {...discussion} user={user} /> + <DiscussionLink {...discussion} /> </li> ))} </ul> @@ -92,21 +85,73 @@ const DiscussionList = ({ discussions, user }) => { }; css` +div.menu div.user { + display: flex; + align-items: baseline; + gap: 1em; +} +`; +const UserMenu = () => { + const [showLogin, setLogin] = useState(false); + const { state: { user }, dispatch } = useUserCtx(); + + if (showLogin) { + return ( + <form class="user" onSubmit={(e) => { + e.preventDefault(); + + const elem = e.target.elements; + const username = elem.namedItem('username').value; + const password = elem.namedItem('password').value; + + dispatch({ type: 'login', username, password }) + .then( + () => setLogin(false), + (err) => setLogin("error - wrong username/password?"), + ); + + }}> + {showLogin !== true ? <p>{showLogin}</p> : null} + <div> + <label>username</label> + <input name="username" autocomplete="username" required /> + </div> + <div> + <label>password</label> + <input name="password" autocomplete="password" type="password" required /> + </div> + <input type="submit" value="login" /> + <button onClick={(e) => { e.preventDefault(); setLogin(false)}}>cancel</button> + </form> + ); + } + + return ( + <div class="user"> + {user && user.name !== 'Guest' + ? ( + <> + <span>logged in as {user.name}</span> + <button onClick={() => dispatch({ type: 'logout' })}>log out</button> + </> + ) + : ( + <> + <span>not logged in</span> + <button onClick={() => setLogin(true)}>log in</button> + </> + )} + </div> + ); +}; + +css` div.menu { margin: 1rem 2rem; } `; export const Menu = () => { - const [user, setUser] = useState(null); - const [discussions, setDiscussions] = useState([]); - - useEffect(() => { - fetchJSON('/discdag/list') - .then(({ user, discussions }) => { - setUser(user ?? null); - setDiscussions(discussions); - }); - }, []); + const { user, discussions } = useUser(); if (user === null) { return ( @@ -120,20 +165,8 @@ export const Menu = () => { return ( <div class="menu"> <h2>Discussions</h2> - {/*user && user.name !== 'Guest' - ? ( - <> - <span>logged in as {user.name}</span> - <button>log out</button> - </> - ) - : ( - <> - <span>not logged in</span> - <button>log in</button> - </> - )*/} - <DiscussionList discussions={discussions} user={user?.id} /> + <UserMenu /> + <DiscussionList discussions={discussions} /> </div> ); }; diff --git a/client/src/user.js b/client/src/user.js new file mode 100644 index 0000000..dfa6384 --- /dev/null +++ b/client/src/user.js @@ -0,0 +1,71 @@ +import { h, createContext } from 'preact'; +import { useRef, useState, useCallback, useEffect, useContext } from 'preact/hooks'; +import { API_PREFIX } from './config'; + +const fetchJSON = async (url, method='GET', data=undefined) => { + const res = await fetch(API_PREFIX + url, { + method, + credentials: 'include', + headers: { 'content-type': data && 'application/json' }, + body: data && JSON.stringify(data), + }); + + if (res.status !== 200) + throw new Error("wrong status"); + + return res.json(); +}; + +const initialState = { user: null, discussions: [] }; +const reducer = async (action, ref) => { + switch (action.type) { + case 'login': { + const { username, password } = action; + await fetchJSON('/discdag/login', 'POST', { username, password }); + return await reducer({ type: 'refresh' }, ref); + } + + case 'logout': { + await fetchJSON('/discdag/logout', 'POST'); + return await reducer({ type: 'refresh' }, ref); + } + + case 'refresh': { + const { user, discussions } = await fetchJSON('/discdag/list'); + return { ...ref.current, user, discussions }; + } + + default: + throw new Error('Unexpected action'); + } +}; + +const UserContext = createContext({ + state: initialState, + dispatch: () => { + throw new Error("no UserContext Provider!"); + }, +}); + +export const UserContainer = ({ children }) => { + const ref = useRef(); + const [state, setState] = useState(initialState); + ref.current = state; + + const dispatch = useCallback(async (action) => { + const nextState = await reducer(action, ref); + setState(nextState); + }, [ref]); + + useEffect(() => dispatch({ type: 'refresh' }), []); + + return ( + <UserContext.Provider value={{ state, dispatch }}> + {children} + </UserContext.Provider> + ); +}; + +export const useUser = () => useContext(UserContext).state; +export const useDispatch = () => useContext(UserContext).dispatch; +export const useUserCtx = () => useContext(UserContext); diff --git a/server/main.ts b/server/main.ts index 9dc6191..3a28ee9 100644 --- a/server/main.ts +++ b/server/main.ts @@ -62,6 +62,8 @@ const discdag = express() }); })) + .post('/logout', wrap(async ($req, $res) => $res.cookie('digest', '').json({}))) + .get('/list', wrap(async ($req, $res) => { const { digest } = $req.cookies; @@ -131,6 +133,9 @@ const discdag = express() const doc = cheerio.load(await res.text()); + if (doc.text().indexOf("Invalid discussion ID:") > -1) + return $res.status(404).end(); + type Note = { type: 'Note'; published: string; |
