From: Dessalines Date: Mon, 27 Jul 2020 13:23:08 +0000 (-0400) Subject: Remove extra jwt claims (for user settings) (#1025) X-Git-Url: http://these/git/%7B%60%24%7BwebArchiveUrl%7D/%22%7B%7D/readmes/%7B%7D/%22%7Burl%7D/%7BelementUrl%7D?a=commitdiff_plain;h=d1342afe934b206313fa3434fdf6921e6597ad30;p=lemmy.git Remove extra jwt claims (for user settings) (#1025) * Remove extra jwt claims (for user settings) - The JWT token only contains the issuer, and your user id now. - Now only a page refresh is necessary to pick up your settings on all clients, including theme, language, etc. - GetSiteResponse now gives you your user and settings if logged in. - Fixes #773 * Remove extra comment line, I tested nsfw * Adding a todo to add a User_::readSafe() --- diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 62cb1fc4..1a758804 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -942,6 +942,10 @@ Search types are `All, Comments, Posts, Communities, Users, Url` ```rust { op: "GetSite" + data: { + auth: Option, + } + } ``` ##### Response @@ -954,6 +958,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url` banned: Vec, online: usize, // This is currently broken version: String, + my_user: Option, // Gives back your user and settings if logged in } } ``` diff --git a/server/lemmy_db/src/user.rs b/server/lemmy_db/src/user.rs index e5389077..ca454c5f 100644 --- a/server/lemmy_db/src/user.rs +++ b/server/lemmy_db/src/user.rs @@ -6,8 +6,9 @@ use crate::{ }; use bcrypt::{hash, DEFAULT_COST}; use diesel::{dsl::*, result::Error, *}; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Queryable, Identifiable, PartialEq, Debug)] +#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[table_name = "user_"] pub struct User_ { pub id: i32, diff --git a/server/lemmy_db/src/user_view.rs b/server/lemmy_db/src/user_view.rs index d61fe9c5..5e1eb2d4 100644 --- a/server/lemmy_db/src/user_view.rs +++ b/server/lemmy_db/src/user_view.rs @@ -56,14 +56,14 @@ pub struct UserView { pub actor_id: String, pub name: String, pub avatar: Option, - pub email: Option, + pub email: Option, // TODO this shouldn't be in this view pub matrix_user_id: Option, pub bio: Option, pub local: bool, pub admin: bool, pub banned: bool, - pub show_avatars: bool, - pub send_notifications_to_email: bool, + pub show_avatars: bool, // TODO this is a setting, probably doesn't need to be here + pub send_notifications_to_email: bool, // TODO also never used pub published: chrono::NaiveDateTime, pub number_of_posts: i64, pub post_score: i64, diff --git a/server/src/api/claims.rs b/server/src/api/claims.rs index 9118714b..477ff1d9 100644 --- a/server/src/api/claims.rs +++ b/server/src/api/claims.rs @@ -9,15 +9,7 @@ type Jwt = String; #[derive(Debug, Serialize, Deserialize)] pub struct Claims { pub id: i32, - pub username: String, pub iss: String, - pub show_nsfw: bool, - pub theme: String, - pub default_sort_type: i16, - pub default_listing_type: i16, - pub lang: String, - pub avatar: Option, - pub show_avatars: bool, } impl Claims { @@ -36,15 +28,7 @@ impl Claims { pub fn jwt(user: User_, hostname: String) -> Jwt { let my_claims = Claims { id: user.id, - username: user.name.to_owned(), iss: hostname, - show_nsfw: user.show_nsfw, - theme: user.theme.to_owned(), - default_sort_type: user.default_sort_type, - default_listing_type: user.default_listing_type, - lang: user.lang.to_owned(), - avatar: user.avatar.to_owned(), - show_avatars: user.show_avatars.to_owned(), }; encode( &Header::default(), diff --git a/server/src/api/community.rs b/server/src/api/community.rs index c5ae152a..e4a8b6e8 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -591,21 +591,26 @@ impl Perform for Oper { ) -> Result { let data: &ListCommunities = &self.data; - let user_claims: Option = match &data.auth { + // For logged in users, you need to get back subscribed, and settings + let user: Option = match &data.auth { Some(auth) => match Claims::decode(&auth) { - Ok(claims) => Some(claims.claims), + Ok(claims) => { + let user_id = claims.claims.id; + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + Some(user) + } Err(_e) => None, }, None => None, }; - let user_id = match &user_claims { - Some(claims) => Some(claims.id), + let user_id = match &user { + Some(user) => Some(user.id), None => None, }; - let show_nsfw = match &user_claims { - Some(claims) => claims.show_nsfw, + let show_nsfw = match &user { + Some(user) => user.show_nsfw, None => false, }; diff --git a/server/src/api/post.rs b/server/src/api/post.rs index 79881c4b..e346a6c8 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -370,21 +370,26 @@ impl Perform for Oper { ) -> Result { let data: &GetPosts = &self.data; - let user_claims: Option = match &data.auth { + // For logged in users, you need to get back subscribed, and settings + let user: Option = match &data.auth { Some(auth) => match Claims::decode(&auth) { - Ok(claims) => Some(claims.claims), + Ok(claims) => { + let user_id = claims.claims.id; + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + Some(user) + } Err(_e) => None, }, None => None, }; - let user_id = match &user_claims { - Some(claims) => Some(claims.id), + let user_id = match &user { + Some(user) => Some(user.id), None => None, }; - let show_nsfw = match &user_claims { - Some(claims) => claims.show_nsfw, + let show_nsfw = match &user { + Some(user) => user.show_nsfw, None => false, }; diff --git a/server/src/api/site.rs b/server/src/api/site.rs index 85511e6c..3b8b9693 100644 --- a/server/src/api/site.rs +++ b/server/src/api/site.rs @@ -18,6 +18,7 @@ use lemmy_db::{ post_view::*, site::*, site_view::*, + user::*, user_view::*, Crud, SearchType, @@ -98,7 +99,9 @@ pub struct EditSite { } #[derive(Serialize, Deserialize)] -pub struct GetSite {} +pub struct GetSite { + auth: Option, +} #[derive(Serialize, Deserialize, Clone)] pub struct SiteResponse { @@ -112,6 +115,7 @@ pub struct GetSiteResponse { banned: Vec, pub online: usize, version: String, + my_user: Option, } #[derive(Serialize, Deserialize)] @@ -352,7 +356,7 @@ impl Perform for Oper { pool: &DbPool, websocket_info: Option, ) -> Result { - let _data: &GetSite = &self.data; + let data: &GetSite = &self.data; // TODO refactor this a little let res = blocking(pool, move |conn| Site::read(conn, 1)).await?; @@ -415,12 +419,29 @@ impl Perform for Oper { 0 }; + // Giving back your user, if you're logged in + let my_user: Option = match &data.auth { + Some(auth) => match Claims::decode(&auth) { + Ok(claims) => { + let user_id = claims.claims.id; + let mut user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + user.password_encrypted = "".to_string(); + user.private_key = None; + user.public_key = None; + Some(user) + } + Err(_e) => None, + }, + None => None, + }; + Ok(GetSiteResponse { site: site_view, admins, banned, online, version: version::VERSION.to_string(), + my_user, }) } } @@ -614,6 +635,11 @@ impl Perform for Oper { }; let user_id = claims.id; + let mut user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + // TODO add a User_::read_safe() for this. + user.password_encrypted = "".to_string(); + user.private_key = None; + user.public_key = None; let read_site = blocking(pool, move |conn| Site::read(conn, 1)).await??; @@ -664,6 +690,7 @@ impl Perform for Oper { banned, online: 0, version: version::VERSION.to_string(), + my_user: Some(user), }) } } diff --git a/server/src/api/user.rs b/server/src/api/user.rs index 32a16b00..f6548f8c 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -561,21 +561,26 @@ impl Perform for Oper { ) -> Result { let data: &GetUserDetails = &self.data; - let user_claims: Option = match &data.auth { + // For logged in users, you need to get back subscribed, and settings + let user: Option = match &data.auth { Some(auth) => match Claims::decode(&auth) { - Ok(claims) => Some(claims.claims), + Ok(claims) => { + let user_id = claims.claims.id; + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + Some(user) + } Err(_e) => None, }, None => None, }; - let user_id = match &user_claims { - Some(claims) => Some(claims.id), + let user_id = match &user { + Some(user) => Some(user.id), None => None, }; - let show_nsfw = match &user_claims { - Some(claims) => claims.show_nsfw, + let show_nsfw = match &user { + Some(user) => user.show_nsfw, None => false, }; @@ -1188,11 +1193,11 @@ impl Perform for Oper { let subject = &format!( "{} - Private Message from {}", Settings::get().hostname, - claims.username + user.name, ); let html = &format!( "

Private Message


{} - {}

inbox", - claims.username, &content_slurs_removed, hostname + user.name, &content_slurs_removed, hostname ); match send_email(subject, &email, &recipient_user.name, html) { Ok(_o) => _o, diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index 907e9bcc..c9c83cdc 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -559,17 +559,14 @@ export class Inbox extends Component { let data = res.data as GetSiteResponse; this.state.enableDownvotes = data.site.enable_downvotes; this.setState(this.state); - document.title = `/u/${UserService.Instance.user.username} ${i18n.t( + document.title = `/u/${UserService.Instance.user.name} ${i18n.t( 'inbox' )} - ${data.site.name}`; } } sendUnreadCount() { - UserService.Instance.user.unreadCount = this.unreadCount(); - UserService.Instance.sub.next({ - user: UserService.Instance.user, - }); + UserService.Instance.unreadCountSub.next(this.unreadCount()); } unreadCount(): number { diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx index dbcfc1a5..561dc482 100644 --- a/ui/src/components/navbar.tsx +++ b/ui/src/components/navbar.tsx @@ -29,8 +29,9 @@ import { toast, messageToastify, md, + setTheme, } from '../utils'; -import { i18n } from '../i18next'; +import { i18n, i18nextSetup } from '../i18next'; interface NavbarState { isLoggedIn: boolean; @@ -44,14 +45,16 @@ interface NavbarState { admins: Array; searchParam: string; toggleSearch: boolean; + siteLoading: boolean; } export class Navbar extends Component { private wsSub: Subscription; private userSub: Subscription; + private unreadCountSub: Subscription; private searchTextField: RefObject; emptyState: NavbarState = { - isLoggedIn: UserService.Instance.user !== undefined, + isLoggedIn: false, unreadCount: 0, replies: [], mentions: [], @@ -62,22 +65,13 @@ export class Navbar extends Component { admins: [], searchParam: '', toggleSearch: false, + siteLoading: true, }; constructor(props: any, context: any) { super(props, context); this.state = this.emptyState; - // Subscribe to user changes - this.userSub = UserService.Instance.sub.subscribe(user => { - this.state.isLoggedIn = user.user !== undefined; - if (this.state.isLoggedIn) { - this.state.unreadCount = user.user.unreadCount; - this.requestNotificationPermission(); - } - this.setState(this.state); - }); - this.wsSub = WebSocketService.Instance.subject .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .subscribe( @@ -86,17 +80,30 @@ export class Navbar extends Component { () => console.log('complete') ); - if (this.state.isLoggedIn) { - this.requestNotificationPermission(); - // TODO couldn't get re-logging in to re-fetch unreads - this.fetchUnreads(); - } - WebSocketService.Instance.getSite(); this.searchTextField = createRef(); } + componentDidMount() { + // Subscribe to jwt changes + this.userSub = UserService.Instance.jwtSub.subscribe(res => { + // A login + if (res !== undefined) { + this.requestNotificationPermission(); + } else { + this.state.isLoggedIn = false; + } + WebSocketService.Instance.getSite(); + this.setState(this.state); + }); + + // Subscribe to unread count changes + this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(res => { + this.setState({ unreadCount: res }); + }); + } + handleSearchParam(i: Navbar, event: any) { i.state.searchParam = event.target.value; i.setState(i.state); @@ -145,6 +152,7 @@ export class Navbar extends Component { componentWillUnmount() { this.wsSub.unsubscribe(); this.userSub.unsubscribe(); + this.unreadCountSub.unsubscribe(); } // TODO class active corresponding to current page @@ -152,9 +160,17 @@ export class Navbar extends Component { return ( ); @@ -400,38 +425,53 @@ export class Navbar extends Component { this.state.siteName = data.site.name; this.state.version = data.version; this.state.admins = data.admins; - this.setState(this.state); } - } - } - fetchUnreads() { - if (this.state.isLoggedIn) { - let repliesForm: GetRepliesForm = { - sort: SortType[SortType.New], - unread_only: true, - page: 1, - limit: fetchLimit, - }; + // The login + if (data.my_user) { + UserService.Instance.user = data.my_user; + // On the first load, check the unreads + if (this.state.isLoggedIn == false) { + this.requestNotificationPermission(); + this.fetchUnreads(); + setTheme(data.my_user.theme, true); + } + this.state.isLoggedIn = true; + } - let userMentionsForm: GetUserMentionsForm = { - sort: SortType[SortType.New], - unread_only: true, - page: 1, - limit: fetchLimit, - }; + i18nextSetup(); - let privateMessagesForm: GetPrivateMessagesForm = { - unread_only: true, - page: 1, - limit: fetchLimit, - }; + this.state.siteLoading = false; + this.setState(this.state); + } + } - if (this.currentLocation !== '/inbox') { - WebSocketService.Instance.getReplies(repliesForm); - WebSocketService.Instance.getUserMentions(userMentionsForm); - WebSocketService.Instance.getPrivateMessages(privateMessagesForm); - } + fetchUnreads() { + console.log('Fetching unreads...'); + let repliesForm: GetRepliesForm = { + sort: SortType[SortType.New], + unread_only: true, + page: 1, + limit: fetchLimit, + }; + + let userMentionsForm: GetUserMentionsForm = { + sort: SortType[SortType.New], + unread_only: true, + page: 1, + limit: fetchLimit, + }; + + let privateMessagesForm: GetPrivateMessagesForm = { + unread_only: true, + page: 1, + limit: fetchLimit, + }; + + if (this.currentLocation !== '/inbox') { + WebSocketService.Instance.getReplies(repliesForm); + WebSocketService.Instance.getUserMentions(userMentionsForm); + WebSocketService.Instance.getPrivateMessages(privateMessagesForm); } } @@ -440,10 +480,7 @@ export class Navbar extends Component { } sendUnreadCount() { - UserService.Instance.user.unreadCount = this.state.unreadCount; - UserService.Instance.sub.next({ - user: UserService.Instance.user, - }); + UserService.Instance.unreadCountSub.next(this.state.unreadCount); } calculateUnreadCount(): number { diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index f21dd7dc..c066a068 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -174,10 +174,9 @@ export class Post extends Component { auth: null, }; WebSocketService.Instance.markCommentAsRead(form); - UserService.Instance.user.unreadCount--; - UserService.Instance.sub.next({ - user: UserService.Instance.user, - }); + UserService.Instance.unreadCountSub.next( + UserService.Instance.unreadCountSub.value - 1 + ); } } diff --git a/ui/src/i18next.ts b/ui/src/i18next.ts index 3657da33..5d68b180 100644 --- a/ui/src/i18next.ts +++ b/ui/src/i18next.ts @@ -65,15 +65,16 @@ function format(value: any, format: any, lng: any): any { return format === 'uppercase' ? value.toUpperCase() : value; } -i18next.init({ - debug: false, - // load: 'languageOnly', - - // initImmediate: false, - lng: getLanguage(), - fallbackLng: 'en', - resources, - interpolation: { format }, -}); +export function i18nextSetup() { + i18next.init({ + debug: false, + // load: 'languageOnly', + // initImmediate: false, + lng: getLanguage(), + fallbackLng: 'en', + resources, + interpolation: { format }, + }); +} export { i18next as i18n, resources }; diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 559fbca5..b8804522 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -100,18 +100,33 @@ export enum SearchType { Url, } -export interface User { +export interface Claims { id: number; iss: string; - username: string; +} + +export interface User { + id: number; + name: string; + preferred_username?: string; + email?: string; + avatar?: string; + admin: boolean; + banned: boolean; + published: string; + updated?: string; show_nsfw: boolean; theme: string; default_sort_type: SortType; default_listing_type: ListingType; lang: string; - avatar?: string; show_avatars: boolean; - unreadCount?: number; + send_notifications_to_email: boolean; + matrix_user_id?: string; + actor_id: string; + bio?: string; + local: boolean; + last_refreshed_at: string; } export interface UserView { @@ -797,6 +812,10 @@ export interface GetSiteConfig { auth?: string; } +export interface GetSiteForm { + auth?: string; +} + export interface GetSiteConfigResponse { config_hjson: string; } @@ -812,6 +831,7 @@ export interface GetSiteResponse { banned: Array; online: number; version: string; + my_user?: User; } export interface SiteResponse { @@ -998,7 +1018,8 @@ type ResponseType = | AddAdminResponse | PrivateMessageResponse | PrivateMessagesResponse - | GetSiteConfigResponse; + | GetSiteConfigResponse + | GetSiteResponse; export interface WebSocketResponse { op: UserOperation; diff --git a/ui/src/services/UserService.ts b/ui/src/services/UserService.ts index 786d5d07..bf7e4267 100644 --- a/ui/src/services/UserService.ts +++ b/ui/src/services/UserService.ts @@ -1,20 +1,22 @@ import Cookies from 'js-cookie'; -import { User, LoginResponse } from '../interfaces'; +import { User, Claims, LoginResponse } from '../interfaces'; import { setTheme } from '../utils'; import jwt_decode from 'jwt-decode'; -import { Subject } from 'rxjs'; +import { Subject, BehaviorSubject } from 'rxjs'; export class UserService { private static _instance: UserService; public user: User; - public sub: Subject<{ user: User }> = new Subject<{ - user: User; - }>(); + public claims: Claims; + public jwtSub: Subject = new Subject(); + public unreadCountSub: BehaviorSubject = new BehaviorSubject( + 0 + ); private constructor() { let jwt = Cookies.get('jwt'); if (jwt) { - this.setUser(jwt); + this.setClaims(jwt); } else { setTheme(); console.log('No JWT cookie found.'); @@ -22,16 +24,17 @@ export class UserService { } public login(res: LoginResponse) { - this.setUser(res.jwt); + this.setClaims(res.jwt); Cookies.set('jwt', res.jwt, { expires: 365 }); console.log('jwt cookie set'); } public logout() { + this.claims = undefined; this.user = undefined; Cookies.remove('jwt'); setTheme(); - this.sub.next({ user: undefined }); + this.jwtSub.next(undefined); console.log('Logged out.'); } @@ -39,11 +42,9 @@ export class UserService { return Cookies.get('jwt'); } - private setUser(jwt: string) { - this.user = jwt_decode(jwt); - setTheme(this.user.theme, true); - this.sub.next({ user: this.user }); - console.log(this.user); + private setClaims(jwt: string) { + this.claims = jwt_decode(jwt); + this.jwtSub.next(jwt); } public static get Instance() { diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index aabfc4dd..5d991660 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -51,6 +51,7 @@ import { GetCommentsForm, UserJoinForm, GetSiteConfig, + GetSiteForm, SiteConfigForm, MessageType, WebSocketJsonResponse, @@ -316,8 +317,9 @@ export class WebSocketService { this.ws.send(this.wsSendWrapper(UserOperation.EditSite, siteForm)); } - public getSite() { - this.ws.send(this.wsSendWrapper(UserOperation.GetSite, {})); + public getSite(form: GetSiteForm = {}) { + this.setAuth(form, false); + this.ws.send(this.wsSendWrapper(UserOperation.GetSite, form)); } public getSiteConfig() {