login/logout
s-ol
1 year, 8 months ago
3 | 3 | import { h, Fragment, Component, render } from 'preact'; |
4 | 4 | import { GraphContainerJSONLD, GraphContainerMastodon } from './graph'; |
5 | 5 | import { Menu, Discussion, Selection } from './ui'; |
6 | import { UserContainer } from './user'; | |
6 | 7 | |
7 | 8 | class SelectionContainer extends Component { |
8 | 9 | state = { |
133 | 134 | const graph = search.has('graph') ? search.get('graph') : 'lib/graph.json'; |
134 | 135 | |
135 | 136 | let app; |
136 | if (search.has('document')) { | |
137 | if (search.has('document') || search.has('note') || search.has('graph')) { | |
138 | const Container = search.has('document') ? GraphContainerJSONLD : GraphContainerMastodon; | |
139 | const url = search.get('document') || search.get('note') || search.get('graph'); | |
140 | ||
137 | 141 | app = ( |
138 | <GraphContainerJSONLD | |
139 | url={search.get('document')} | |
140 | render={graphRender} | |
141 | /> | |
142 | ); | |
143 | } else if (search.has('note')) { | |
144 | app = ( | |
145 | <GraphContainerMastodon | |
146 | url={search.get('note')} | |
147 | render={graphRender} | |
148 | /> | |
149 | ); | |
150 | } else if (search.has('graph')) { | |
151 | app = ( | |
152 | <GraphContainerMastodon | |
153 | url={search.get('graph')} | |
154 | render={graphRender} | |
155 | /> | |
142 | <UserContainer> | |
143 | <Container | |
144 | url={url} | |
145 | render={graphRender} | |
146 | /> | |
147 | </UserContainer> | |
156 | 148 | ); |
157 | 149 | } else { |
158 | app = <Menu />; | |
150 | app = ( | |
151 | <UserContainer> | |
152 | <Menu /> | |
153 | </UserContainer> | |
154 | ); | |
159 | 155 | } |
160 | 156 | |
161 | 157 | render(app, document.body); |
0 | 0 | import { h, Fragment } from 'preact'; |
1 | import { useState, useEffect } from 'preact/hooks'; | |
1 | import { useState } from 'preact/hooks'; | |
2 | 2 | import cn from 'classnames'; |
3 | import { API_PREFIX } from '../config'; | |
3 | import { useUser, useUserCtx } from '../user'; | |
4 | 4 | import css from './css'; |
5 | ||
6 | const fetchJSON = async (url) => { | |
7 | const res = await fetch(API_PREFIX + url, { credentials: 'include' }); | |
8 | if (res.status !== 200) | |
9 | throw new Error("wrong status"); | |
10 | ||
11 | return res.json(); | |
12 | }; | |
13 | 5 | |
14 | 6 | css` |
15 | 7 | a.discussion { |
51 | 43 | } |
52 | 44 | `; |
53 | 45 | |
54 | const DiscussionLink = ({ id, name, url, attributedTo, user }) => { | |
55 | const writable = attributedTo && attributedTo.indexOf(user) > -1; | |
46 | const DiscussionLink = ({ id, name, url, attributedTo }) => { | |
47 | const { user } = useUser(); | |
48 | const writable = user && attributedTo && attributedTo.indexOf(user.id) > -1; | |
56 | 49 | |
57 | 50 | return ( |
58 | 51 | <a class="discussion" href={`?document=${id}`}> |
78 | 71 | } |
79 | 72 | `; |
80 | 73 | |
81 | const DiscussionList = ({ discussions, user }) => { | |
74 | const DiscussionList = ({ discussions }) => { | |
82 | 75 | return ( |
83 | 76 | <ul class="discussions"> |
84 | 77 | {discussions.map((discussion) => ( |
85 | 78 | <li> |
86 | <DiscussionLink {...discussion} user={user} /> | |
79 | <DiscussionLink {...discussion} /> | |
87 | 80 | </li> |
88 | 81 | ))} |
89 | 82 | </ul> |
90 | 83 | ); |
84 | }; | |
85 | ||
86 | css` | |
87 | div.menu div.user { | |
88 | display: flex; | |
89 | align-items: baseline; | |
90 | gap: 1em; | |
91 | } | |
92 | `; | |
93 | const UserMenu = () => { | |
94 | const [showLogin, setLogin] = useState(false); | |
95 | const { state: { user }, dispatch } = useUserCtx(); | |
96 | ||
97 | if (showLogin) { | |
98 | return ( | |
99 | <form class="user" onSubmit={(e) => { | |
100 | e.preventDefault(); | |
101 | ||
102 | const elem = e.target.elements; | |
103 | const username = elem.namedItem('username').value; | |
104 | const password = elem.namedItem('password').value; | |
105 | ||
106 | dispatch({ type: 'login', username, password }) | |
107 | .then( | |
108 | () => setLogin(false), | |
109 | (err) => setLogin("error - wrong username/password?"), | |
110 | ); | |
111 | ||
112 | }}> | |
113 | {showLogin !== true ? <p>{showLogin}</p> : null} | |
114 | <div> | |
115 | <label>username</label> | |
116 | <input name="username" autocomplete="username" required /> | |
117 | </div> | |
118 | <div> | |
119 | <label>password</label> | |
120 | <input name="password" autocomplete="password" type="password" required /> | |
121 | </div> | |
122 | <input type="submit" value="login" /> | |
123 | <button onClick={(e) => { e.preventDefault(); setLogin(false)}}>cancel</button> | |
124 | </form> | |
125 | ); | |
126 | } | |
127 | ||
128 | return ( | |
129 | <div class="user"> | |
130 | {user && user.name !== 'Guest' | |
131 | ? ( | |
132 | <> | |
133 | <span>logged in as {user.name}</span> | |
134 | <button onClick={() => dispatch({ type: 'logout' })}>log out</button> | |
135 | </> | |
136 | ) | |
137 | : ( | |
138 | <> | |
139 | <span>not logged in</span> | |
140 | <button onClick={() => setLogin(true)}>log in</button> | |
141 | </> | |
142 | )} | |
143 | </div> | |
144 | ); | |
91 | 145 | }; |
92 | 146 | |
93 | 147 | css` |
96 | 150 | } |
97 | 151 | `; |
98 | 152 | export const Menu = () => { |
99 | const [user, setUser] = useState(null); | |
100 | const [discussions, setDiscussions] = useState([]); | |
101 | ||
102 | useEffect(() => { | |
103 | fetchJSON('/discdag/list') | |
104 | .then(({ user, discussions }) => { | |
105 | setUser(user ?? null); | |
106 | setDiscussions(discussions); | |
107 | }); | |
108 | }, []); | |
153 | const { user, discussions } = useUser(); | |
109 | 154 | |
110 | 155 | if (user === null) { |
111 | 156 | return ( |
119 | 164 | return ( |
120 | 165 | <div class="menu"> |
121 | 166 | <h2>Discussions</h2> |
122 | {/*user && user.name !== 'Guest' | |
123 | ? ( | |
124 | <> | |
125 | <span>logged in as {user.name}</span> | |
126 | <button>log out</button> | |
127 | </> | |
128 | ) | |
129 | : ( | |
130 | <> | |
131 | <span>not logged in</span> | |
132 | <button>log in</button> | |
133 | </> | |
134 | )*/} | |
135 | <DiscussionList discussions={discussions} user={user?.id} /> | |
167 | <UserMenu /> | |
168 | <DiscussionList discussions={discussions} /> | |
136 | 169 | </div> |
137 | 170 | ); |
138 | 171 | }; |
0 | import { h, createContext } from 'preact'; | |
1 | import { useRef, useState, useCallback, useEffect, useContext } from 'preact/hooks'; | |
2 | import { API_PREFIX } from './config'; | |
3 | ||
4 | const fetchJSON = async (url, method='GET', data=undefined) => { | |
5 | const res = await fetch(API_PREFIX + url, { | |
6 | method, | |
7 | credentials: 'include', | |
8 | headers: { 'content-type': data && 'application/json' }, | |
9 | body: data && JSON.stringify(data), | |
10 | }); | |
11 | ||
12 | if (res.status !== 200) | |
13 | throw new Error("wrong status"); | |
14 | ||
15 | return res.json(); | |
16 | }; | |
17 | ||
18 | const initialState = { user: null, discussions: [] }; | |
19 | const reducer = async (action, ref) => { | |
20 | switch (action.type) { | |
21 | case 'login': { | |
22 | const { username, password } = action; | |
23 | await fetchJSON('/discdag/login', 'POST', { username, password }); | |
24 | return await reducer({ type: 'refresh' }, ref); | |
25 | } | |
26 | ||
27 | case 'logout': { | |
28 | await fetchJSON('/discdag/logout', 'POST'); | |
29 | return await reducer({ type: 'refresh' }, ref); | |
30 | } | |
31 | ||
32 | case 'refresh': { | |
33 | const { user, discussions } = await fetchJSON('/discdag/list'); | |
34 | return { ...ref.current, user, discussions }; | |
35 | } | |
36 | ||
37 | default: | |
38 | throw new Error('Unexpected action'); | |
39 | } | |
40 | }; | |
41 | ||
42 | const UserContext = createContext({ | |
43 | state: initialState, | |
44 | dispatch: () => { | |
45 | throw new Error("no UserContext Provider!"); | |
46 | }, | |
47 | }); | |
48 | ||
49 | export const UserContainer = ({ children }) => { | |
50 | const ref = useRef(); | |
51 | const [state, setState] = useState(initialState); | |
52 | ref.current = state; | |
53 | ||
54 | const dispatch = useCallback(async (action) => { | |
55 | const nextState = await reducer(action, ref); | |
56 | setState(nextState); | |
57 | }, [ref]); | |
58 | ||
59 | useEffect(() => dispatch({ type: 'refresh' }), []); | |
60 | ||
61 | return ( | |
62 | <UserContext.Provider value={{ state, dispatch }}> | |
63 | {children} | |
64 | </UserContext.Provider> | |
65 | ); | |
66 | }; | |
67 | ||
68 | export const useUser = () => useContext(UserContext).state; | |
69 | export const useDispatch = () => useContext(UserContext).dispatch; | |
70 | export const useUserCtx = () => useContext(UserContext); |
60 | 60 | name, |
61 | 61 | }); |
62 | 62 | })) |
63 | ||
64 | .post('/logout', wrap(async ($req, $res) => $res.cookie('digest', '').json({}))) | |
63 | 65 | |
64 | 66 | .get('/list', wrap(async ($req, $res) => { |
65 | 67 | const { digest } = $req.cookies; |
130 | 132 | |
131 | 133 | const doc = cheerio.load(await res.text()); |
132 | 134 | |
135 | if (doc.text().indexOf("Invalid discussion ID:") > -1) | |
136 | return $res.status(404).end(); | |
137 | ||
133 | 138 | type Note = { |
134 | 139 | type: 'Note'; |
135 | 140 | published: string; |