Browse Source

Figure out user manager for account user

binwiederhier 3 years ago
parent
commit
95a8e64fbb

+ 9 - 5
server/server.go

@@ -36,14 +36,19 @@ import (
 
 /*
 	TODO
-		use token auth in "SubscribeDialog"
-		upload files based on user limit
 		database migration
-		publishXHR + poll should pick current user, not from userManager
 		reserve topics
 		purge accounts that were not logged into in X
 		reset daily limits for users
-		store users
+		"user list" shows * twice
+		"ntfy access everyone user4topic <bla>" twice -> UNIQUE constraint error
+		Account usage not updated "in real time"
+		Sync:
+			- "mute" setting
+			- figure out what settings are "web" or "phone"
+		UI:
+		- Subscription dotmenu dropdown: Move to nav bar, or make same as profile dropdown
+		- "Logout and delete local storage" option
 		Pages:
 		- Home
 		- Password reset
@@ -52,7 +57,6 @@ import (
 		-
 		Polishing:
 			aria-label for everything
-
 		Tests:
 		- APIs
 		- CRUD tokens

+ 5 - 5
user/manager.go

@@ -83,11 +83,11 @@ const (
 		WHERE t.token = ?
 	`
 	selectTopicPermsQuery = `
-		SELECT read, write 
-		FROM user_access
-		JOIN user ON user.user = '*' OR user.user = ?
-		WHERE ? LIKE user_access.topic
-		ORDER BY user.user DESC
+		SELECT read, write
+		FROM user_access a
+		JOIN user u ON u.id = a.user_id
+		WHERE (u.user = '*' OR u.user = ?) AND ? LIKE a.topic
+		ORDER BY u.user DESC
 	`
 )
 

+ 1 - 0
web/public/static/langs/en.json

@@ -175,6 +175,7 @@
   "prefs_notifications_delete_after_one_month_description": "Notifications are auto-deleted after one month",
   "prefs_users_title": "Manage users",
   "prefs_users_description": "Add/remove users for your protected topics here. Please note that username and password are stored in the browser's local storage.",
+  "prefs_users_description_no_sync": "Users and passwords are not synchronized to your account.",
   "prefs_users_table": "Users table",
   "prefs_users_add_button": "Add user",
   "prefs_users_edit_button": "Edit user",

+ 16 - 12
web/src/app/AccountApi.js

@@ -6,8 +6,8 @@ import {
     accountTokenUrl,
     accountUrl,
     fetchLinesIterator,
-    maybeWithBasicAuth,
-    maybeWithBearerAuth,
+    withBasicAuth,
+    withBearerAuth,
     topicShortUrl,
     topicUrl,
     topicUrlAuth,
@@ -31,7 +31,7 @@ class AccountApi {
         console.log(`[AccountApi] Checking auth for ${url}`);
         const response = await fetch(url, {
             method: "POST",
-            headers: maybeWithBasicAuth({}, user)
+            headers: withBasicAuth({}, user.username, user.password)
         });
         if (response.status === 401 || response.status === 403) {
             throw new UnauthorizedError();
@@ -50,7 +50,7 @@ class AccountApi {
         console.log(`[AccountApi] Logging out from ${url} using token ${token}`);
         const response = await fetch(url, {
             method: "DELETE",
-            headers: maybeWithBearerAuth({}, token)
+            headers: withBearerAuth({}, token)
         });
         if (response.status === 401 || response.status === 403) {
             throw new UnauthorizedError();
@@ -83,7 +83,7 @@ class AccountApi {
         const url = accountUrl(config.baseUrl);
         console.log(`[AccountApi] Fetching user account ${url}`);
         const response = await fetch(url, {
-            headers: maybeWithBearerAuth({}, session.token())
+            headers: withBearerAuth({}, session.token())
         });
         if (response.status === 401 || response.status === 403) {
             throw new UnauthorizedError();
@@ -100,7 +100,7 @@ class AccountApi {
         console.log(`[AccountApi] Deleting user account ${url}`);
         const response = await fetch(url, {
             method: "DELETE",
-            headers: maybeWithBearerAuth({}, session.token())
+            headers: withBearerAuth({}, session.token())
         });
         if (response.status === 401 || response.status === 403) {
             throw new UnauthorizedError();
@@ -114,7 +114,7 @@ class AccountApi {
         console.log(`[AccountApi] Changing account password ${url}`);
         const response = await fetch(url, {
             method: "POST",
-            headers: maybeWithBearerAuth({}, session.token()),
+            headers: withBearerAuth({}, session.token()),
             body: JSON.stringify({
                 password: newPassword
             })
@@ -131,7 +131,7 @@ class AccountApi {
         console.log(`[AccountApi] Extending user access token ${url}`);
         const response = await fetch(url, {
             method: "PATCH",
-            headers: maybeWithBearerAuth({}, session.token())
+            headers: withBearerAuth({}, session.token())
         });
         if (response.status === 401 || response.status === 403) {
             throw new UnauthorizedError();
@@ -146,7 +146,7 @@ class AccountApi {
         console.log(`[AccountApi] Updating user account ${url}: ${body}`);
         const response = await fetch(url, {
             method: "PATCH",
-            headers: maybeWithBearerAuth({}, session.token()),
+            headers: withBearerAuth({}, session.token()),
             body: body
         });
         if (response.status === 401 || response.status === 403) {
@@ -162,7 +162,7 @@ class AccountApi {
         console.log(`[AccountApi] Adding user subscription ${url}: ${body}`);
         const response = await fetch(url, {
             method: "POST",
-            headers: maybeWithBearerAuth({}, session.token()),
+            headers: withBearerAuth({}, session.token()),
             body: body
         });
         if (response.status === 401 || response.status === 403) {
@@ -181,7 +181,7 @@ class AccountApi {
         console.log(`[AccountApi] Updating user subscription ${url}: ${body}`);
         const response = await fetch(url, {
             method: "PATCH",
-            headers: maybeWithBearerAuth({}, session.token()),
+            headers: withBearerAuth({}, session.token()),
             body: body
         });
         if (response.status === 401 || response.status === 403) {
@@ -199,7 +199,7 @@ class AccountApi {
         console.log(`[AccountApi] Removing user subscription ${url}`);
         const response = await fetch(url, {
             method: "DELETE",
-            headers: maybeWithBearerAuth({}, session.token())
+            headers: withBearerAuth({}, session.token())
         });
         if (response.status === 401 || response.status === 403) {
             throw new UnauthorizedError();
@@ -208,6 +208,10 @@ class AccountApi {
         }
     }
 
+    sync() {
+        // TODO
+    }
+
     startWorker() {
         if (this.timer !== null) {
             return;

+ 6 - 6
web/src/app/Api.js

@@ -5,9 +5,9 @@ import {
     accountSubscriptionUrl,
     accountTokenUrl,
     accountUrl,
-    fetchLinesIterator,
-    maybeWithBasicAuth,
-    maybeWithBearerAuth,
+    fetchLinesIterator, maybeWithAuth,
+    withBasicAuth,
+    withBearerAuth,
     topicShortUrl,
     topicUrl,
     topicUrlAuth,
@@ -24,7 +24,7 @@ class Api {
             ? topicUrlJsonPollWithSince(baseUrl, topic, since)
             : topicUrlJsonPoll(baseUrl, topic);
         const messages = [];
-        const headers = maybeWithBasicAuth({}, user);
+        const headers = maybeWithAuth({}, user);
         console.log(`[Api] Polling ${url}`);
         for await (let line of fetchLinesIterator(url, headers)) {
             console.log(`[Api, ${shortUrl}] Received message ${line}`);
@@ -45,7 +45,7 @@ class Api {
         const response = await fetch(baseUrl, {
             method: 'PUT',
             body: JSON.stringify(body),
-            headers: maybeWithBasicAuth(headers, user)
+            headers: maybeWithAuth(headers, user)
         });
         if (response.status < 200 || response.status > 299) {
             throw new Error(`Unexpected response: ${response.status}`);
@@ -111,7 +111,7 @@ class Api {
         const url = topicUrlAuth(baseUrl, topic);
         console.log(`[Api] Checking auth for ${url}`);
         const response = await fetch(url, {
-            headers: maybeWithBasicAuth({}, user)
+            headers: maybeWithAuth({}, user)
         });
         if (response.status >= 200 && response.status <= 299) {
             return true;

+ 9 - 3
web/src/app/Connection.js

@@ -1,4 +1,4 @@
-import {basicAuth, encodeBase64Url, topicShortUrl, topicUrlWs} from "./utils";
+import {basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs} from "./utils";
 
 const retryBackoffSeconds = [5, 10, 15, 20, 30];
 
@@ -96,12 +96,18 @@ class Connection {
             params.push(`since=${this.since}`);
         }
         if (this.user) {
-            const auth = encodeBase64Url(basicAuth(this.user.username, this.user.password));
-            params.push(`auth=${auth}`);
+            params.push(`auth=${this.authParam()}`);
         }
         const wsUrl = topicUrlWs(this.baseUrl, this.topic);
         return (params.length === 0) ? wsUrl : `${wsUrl}?${params.join('&')}`;
     }
+
+    authParam() {
+        if (this.user.password) {
+            return encodeBase64Url(basicAuth(this.user.username, this.user.password));
+        }
+        return encodeBase64Url(bearerAuth(this.user.token));
+    }
 }
 
 export class ConnectionState {

+ 1 - 1
web/src/app/ConnectionManager.js

@@ -109,7 +109,7 @@ class ConnectionManager {
 
 const makeConnectionId = async (subscription, user) => {
     return (user)
-        ? hashCode(`${subscription.id}|${user.username}|${user.password}`)
+        ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`)
         : hashCode(`${subscription.id}`);
 }
 

+ 26 - 1
web/src/app/UserManager.js

@@ -1,21 +1,46 @@
 import db from "./db";
+import session from "./Session";
 
 class UserManager {
     async all() {
-        return db.users.toArray();
+        const users = await db.users.toArray();
+        if (session.exists()) {
+            users.unshift(this.localUser());
+        }
+        return users;
     }
 
     async get(baseUrl) {
+        if (session.exists() && baseUrl === config.baseUrl) {
+            return this.localUser();
+        }
         return db.users.get(baseUrl);
     }
 
     async save(user) {
+        if (user.baseUrl === config.baseUrl) {
+            return;
+        }
         await db.users.put(user);
     }
 
     async delete(baseUrl) {
+        if (session.exists() && baseUrl === config.baseUrl) {
+            return;
+        }
         await db.users.delete(baseUrl);
     }
+
+    localUser() {
+        if (!session.exists()) {
+            return null;
+        }
+        return {
+            baseUrl: config.baseUrl,
+            username: session.username(),
+            token: session.token() // Not "password"!
+        };
+    }
 }
 
 const userManager = new UserManager();

+ 16 - 7
web/src/app/utils.js

@@ -99,17 +99,17 @@ export const unmatchedTags = (tags) => {
     else return tags.filter(tag => !(tag in emojis));
 }
 
-export const maybeWithBasicAuth = (headers, user) => {
-    if (user) {
-        headers['Authorization'] = `Basic ${encodeBase64(`${user.username}:${user.password}`)}`;
+export const maybeWithAuth = (headers, user) => {
+    if (user && user.password) {
+        return withBasicAuth(headers, user.username, user.password);
+    } else if (user && user.token) {
+        return withBearerAuth(headers, user.token);
     }
     return headers;
 }
 
-export const maybeWithBearerAuth = (headers, token) => {
-    if (token) {
-        headers['Authorization'] = `Bearer ${token}`;
-    }
+export const withBasicAuth = (headers, username, password) => {
+    headers['Authorization'] = basicAuth(username, password);
     return headers;
 }
 
@@ -117,6 +117,15 @@ export const basicAuth = (username, password) => {
     return `Basic ${encodeBase64(`${username}:${password}`)}`;
 }
 
+export const withBearerAuth = (headers, token) => {
+    headers['Authorization'] = bearerAuth(token);
+    return headers;
+}
+
+export const bearerAuth = (token) => {
+    return `Bearer ${token}`;
+}
+
 export const encodeBase64 = (s) => {
     return Base64.encode(s);
 }

+ 4 - 7
web/src/components/Account.js

@@ -88,7 +88,7 @@ const Stats = () => {
                     </div>
                     <LinearProgress variant="determinate" value={account.limits.emails > 0 ? normalize(account.stats.emails, account.limits.emails) : 100} />
                 </Pref>
-                <Pref labelId={"attachments"} title={t("Attachment storage")} subtitle={t("5 MB per file")}>
+                <Pref labelId={"attachments"} title={t("Attachment storage")} subtitle={t("{{filesize}} per file", { filesize: formatBytes(account.limits.attachment_file_size) })}>
                     <div>
                         <Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography>
                         <Typography variant="body2" sx={{float: "right"}}>{account.limits.attachment_total_size > 0 ? t("of {{limit}}", { limit: formatBytes(account.limits.attachment_total_size) }) : t("Unlimited")}</Typography>
@@ -153,8 +153,7 @@ const ChangePassword = () => {
         } catch (e) {
             console.log(`[Account] Error changing password`, e);
             if ((e instanceof UnauthorizedError)) {
-                session.reset();
-                window.location.href = routes.login;
+                session.resetAndRedirect(routes.login);
             }
             // TODO show error
         }
@@ -238,13 +237,11 @@ const DeleteAccount = () => {
             setDialogOpen(false);
             console.debug(`[Account] Account deleted`);
             // TODO delete local storage
-            session.reset();
-            window.location.href = routes.app;
+            session.resetAndRedirect(routes.app);
         } catch (e) {
             console.log(`[Account] Error deleting account`, e);
             if ((e instanceof UnauthorizedError)) {
-                session.reset();
-                window.location.href = routes.login;
+                session.resetAndRedirect(routes.login);
             }
             // TODO show error
         }

+ 2 - 4
web/src/components/ActionBar.js

@@ -124,8 +124,7 @@ const SettingsIcons = (props) => {
             } catch (e) {
                 console.log(`[ActionBar] Error unsubscribing`, e);
                 if ((e instanceof UnauthorizedError)) {
-                    session.reset();
-                    window.location.href = routes.login;
+                    session.resetAndRedirect(routes.login);
                 }
             }
         }
@@ -272,8 +271,7 @@ const ProfileIcon = (props) => {
         try {
             await accountApi.logout();
         } finally {
-            session.reset();
-            window.location.href = routes.app;
+            session.resetAndRedirect(routes.app);
         }
     };
     return (

+ 1 - 2
web/src/components/App.js

@@ -123,8 +123,7 @@ const Layout = () => {
             } catch (e) {
                 console.log(`[App] Error fetching account`, e);
                 if ((e instanceof UnauthorizedError)) {
-                    session.reset();
-                    window.location.href = routes.login;
+                    session.resetAndRedirect(routes.login);
                 }
             }
         })();

+ 43 - 45
web/src/components/Preferences.js

@@ -270,6 +270,7 @@ const Users = () => {
                 </Typography>
                 <Paragraph>
                     {t("prefs_users_description")}
+                    {session.exists() && <>{" " + t("prefs_users_description_no_sync")}</>}
                 </Paragraph>
                 {users?.length > 0 && <UserTable users={users}/>}
             </CardContent>
@@ -319,52 +320,49 @@ const UserTable = (props) => {
         }
     };
     return (
-        <div>
-            <Table size="small" aria-label={t("prefs_users_table")}>
-                <TableHead>
-                    <TableRow>
-                        <TableCell sx={{paddingLeft: 0}}>{t("prefs_users_table_user_header")}</TableCell>
-                        <TableCell>{t("prefs_users_table_base_url_header")}</TableCell>
-                        <TableCell/>
+        <Table size="small" aria-label={t("prefs_users_table")}>
+            <TableHead>
+                <TableRow>
+                    <TableCell sx={{paddingLeft: 0}}>{t("prefs_users_table_user_header")}</TableCell>
+                    <TableCell>{t("prefs_users_table_base_url_header")}</TableCell>
+                    <TableCell/>
+                </TableRow>
+            </TableHead>
+            <TableBody>
+                {props.users?.map(user => (
+                    <TableRow
+                        key={user.baseUrl}
+                        sx={{'&:last-child td, &:last-child th': {border: 0}}}
+                    >
+                        <TableCell component="th" scope="row" sx={{paddingLeft: 0}}
+                                   aria-label={t("prefs_users_table_user_header")}>{user.username}</TableCell>
+                        <TableCell aria-label={t("prefs_users_table_base_url_header")}>{user.baseUrl}</TableCell>
+                        <TableCell align="right">
+                            {user.baseUrl !== config.baseUrl &&
+                                <>
+                                    <IconButton onClick={() => handleEditClick(user)}
+                                                aria-label={t("prefs_users_edit_button")}>
+                                        <EditIcon/>
+                                    </IconButton>
+                                    <IconButton onClick={() => handleDeleteClick(user)}
+                                                aria-label={t("prefs_users_delete_button")}>
+                                        <CloseIcon/>
+                                    </IconButton>
+                                </>
+                            }
+                        </TableCell>
                     </TableRow>
-                </TableHead>
-                <TableBody>
-                    {props.users?.map(user => (
-                        <TableRow
-                            key={user.baseUrl}
-                            sx={{'&:last-child td, &:last-child th': {border: 0}}}
-                        >
-                            <TableCell component="th" scope="row" sx={{paddingLeft: 0}}
-                                       aria-label={t("prefs_users_table_user_header")}>{user.username}</TableCell>
-                            <TableCell aria-label={t("prefs_users_table_base_url_header")}>{user.baseUrl}</TableCell>
-                            <TableCell align="right">
-                                <IconButton onClick={() => handleEditClick(user)}
-                                            aria-label={t("prefs_users_edit_button")}>
-                                    <EditIcon/>
-                                </IconButton>
-                                <IconButton onClick={() => handleDeleteClick(user)}
-                                            aria-label={t("prefs_users_delete_button")}>
-                                    <CloseIcon/>
-                                </IconButton>
-                            </TableCell>
-                        </TableRow>
-                    ))}
-                </TableBody>
-                <UserDialog
-                    key={`userEditDialog${dialogKey}`}
-                    open={dialogOpen}
-                    user={dialogUser}
-                    users={props.users}
-                    onCancel={handleDialogCancel}
-                    onSubmit={handleDialogSubmit}
-                />
-            </Table>
-            {session.exists() &&
-                <Typography>
-                    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-                </Typography>
-            }
-        </div>
+                ))}
+            </TableBody>
+            <UserDialog
+                key={`userEditDialog${dialogKey}`}
+                open={dialogOpen}
+                user={dialogUser}
+                users={props.users}
+                onCancel={handleDialogCancel}
+                onSubmit={handleDialogSubmit}
+            />
+        </Table>
     );
 };
 

+ 11 - 4
web/src/components/PublishDialog.js

@@ -17,7 +17,15 @@ import IconButton from "@mui/material/IconButton";
 import InsertEmoticonIcon from '@mui/icons-material/InsertEmoticon';
 import {Close} from "@mui/icons-material";
 import MenuItem from "@mui/material/MenuItem";
-import {formatBytes, maybeWithBasicAuth, topicShortUrl, topicUrl, validTopic, validUrl} from "../app/utils";
+import {
+    formatBytes,
+    maybeWithAuth,
+    withBasicAuth,
+    topicShortUrl,
+    topicUrl,
+    validTopic,
+    validUrl
+} from "../app/utils";
 import Box from "@mui/material/Box";
 import AttachmentIcon from "./AttachmentIcon";
 import DialogFooter from "./DialogFooter";
@@ -132,7 +140,7 @@ const PublishDialog = (props) => {
         const body = (attachFile) ? attachFile : message;
         try {
             const user = await userManager.get(baseUrl);
-            const headers = maybeWithBasicAuth({}, user);
+            const headers = maybeWithAuth({}, user);
             const progressFn = (ev) => {
                 if (ev.loaded > 0 && ev.total > 0) {
                     setStatus(t("publish_dialog_progress_uploading_detail", {
@@ -180,8 +188,7 @@ const PublishDialog = (props) => {
         } catch (e) {
             console.log(`[PublishDialog] Retrieving attachment limits failed`, e);
             if ((e instanceof UnauthorizedError)) {
-                session.reset();
-                window.location.href = routes.login;
+                session.resetAndRedirect(routes.login);
             } else {
                 setAttachFileError(""); // Reset error (rely on server-side checking)
             }

+ 1 - 2
web/src/components/SubscribeDialog.js

@@ -40,8 +40,7 @@ const SubscribeDialog = (props) => {
             } catch (e) {
                 console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e);
                 if ((e instanceof UnauthorizedError)) {
-                    session.reset();
-                    window.location.href = routes.login;
+                    session.resetAndRedirect(routes.login);
                 }
             }
         }

+ 1 - 2
web/src/components/hooks.js

@@ -74,8 +74,7 @@ export const useAutoSubscribe = (subscriptions, selected) => {
                     } catch (e) {
                         console.log(`[App] Auto-subscribing failed`, e);
                         if ((e instanceof UnauthorizedError)) {
-                            session.reset();
-                            window.location.href = routes.login;
+                            session.resetAndRedirect(routes.login);
                         }
                     }
                 }