git.s-ol.nu fedidag / 569d322
login/logout s-ol 1 year, 8 months ago
4 changed file(s) with 163 addition(s) and 58 deletion(s). Raw diff Collapse all Expand all
33 import { h, Fragment, Component, render } from 'preact';
44 import { GraphContainerJSONLD, GraphContainerMastodon } from './graph';
55 import { Menu, Discussion, Selection } from './ui';
6 import { UserContainer } from './user';
67
78 class SelectionContainer extends Component {
89 state = {
133134 const graph = search.has('graph') ? search.get('graph') : 'lib/graph.json';
134135
135136 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
137141 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>
156148 );
157149 } else {
158 app = <Menu />;
150 app = (
151 <UserContainer>
152 <Menu />
153 </UserContainer>
154 );
159155 }
160156
161157 render(app, document.body);
00 import { h, Fragment } from 'preact';
1 import { useState, useEffect } from 'preact/hooks';
1 import { useState } from 'preact/hooks';
22 import cn from 'classnames';
3 import { API_PREFIX } from '../config';
3 import { useUser, useUserCtx } from '../user';
44 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 };
135
146 css`
157 a.discussion {
5143 }
5244 `;
5345
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;
5649
5750 return (
5851 <a class="discussion" href={`?document=${id}`}>
7871 }
7972 `;
8073
81 const DiscussionList = ({ discussions, user }) => {
74 const DiscussionList = ({ discussions }) => {
8275 return (
8376 <ul class="discussions">
8477 {discussions.map((discussion) => (
8578 <li>
86 <DiscussionLink {...discussion} user={user} />
79 <DiscussionLink {...discussion} />
8780 </li>
8881 ))}
8982 </ul>
9083 );
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 );
91145 };
92146
93147 css`
96150 }
97151 `;
98152 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();
109154
110155 if (user === null) {
111156 return (
119164 return (
120165 <div class="menu">
121166 <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} />
136169 </div>
137170 );
138171 };
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);
6060 name,
6161 });
6262 }))
63
64 .post('/logout', wrap(async ($req, $res) => $res.cookie('digest', '').json({})))
6365
6466 .get('/list', wrap(async ($req, $res) => {
6567 const { digest } = $req.cookies;
130132
131133 const doc = cheerio.load(await res.text());
132134
135 if (doc.text().indexOf("Invalid discussion ID:") > -1)
136 return $res.status(404).end();
137
133138 type Note = {
134139 type: 'Note';
135140 published: string;