From d1342afe934b206313fa3434fdf6921e6597ad30 Mon Sep 17 00:00:00 2001
From: Dessalines <dessalines@users.noreply.github.com>
Date: Mon, 27 Jul 2020 09:23:08 -0400
Subject: [PATCH] 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()
---
 docs/src/contributing_websocket_http_api.md |   5 +
 server/lemmy_db/src/user.rs                 |   3 +-
 server/lemmy_db/src/user_view.rs            |   6 +-
 server/src/api/claims.rs                    |  16 -
 server/src/api/community.rs                 |  17 +-
 server/src/api/post.rs                      |  17 +-
 server/src/api/site.rs                      |  31 +-
 server/src/api/user.rs                      |  21 +-
 ui/src/components/inbox.tsx                 |   7 +-
 ui/src/components/navbar.tsx                | 391 +++++++++++---------
 ui/src/components/post.tsx                  |   7 +-
 ui/src/i18next.ts                           |  21 +-
 ui/src/interfaces.ts                        |  31 +-
 ui/src/services/UserService.ts              |  27 +-
 ui/src/services/WebSocketService.ts         |   6 +-
 15 files changed, 348 insertions(+), 258 deletions(-)

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<String>,
+  }
+
 }
 ```
 ##### Response
@@ -954,6 +958,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
     banned: Vec<UserView>,
     online: usize, // This is currently broken
     version: String,
+    my_user: Option<User_>, // 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<String>,
-  pub email: Option<String>,
+  pub email: Option<String>, // TODO this shouldn't be in this view
   pub matrix_user_id: Option<String>,
   pub bio: Option<String>,
   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<String>,
-  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<ListCommunities> {
   ) -> Result<ListCommunitiesResponse, LemmyError> {
     let data: &ListCommunities = &self.data;
 
-    let user_claims: Option<Claims> = match &data.auth {
+    // For logged in users, you need to get back subscribed, and settings
+    let user: Option<User_> = 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<GetPosts> {
   ) -> Result<GetPostsResponse, LemmyError> {
     let data: &GetPosts = &self.data;
 
-    let user_claims: Option<Claims> = match &data.auth {
+    // For logged in users, you need to get back subscribed, and settings
+    let user: Option<User_> = 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<String>,
+}
 
 #[derive(Serialize, Deserialize, Clone)]
 pub struct SiteResponse {
@@ -112,6 +115,7 @@ pub struct GetSiteResponse {
   banned: Vec<UserView>,
   pub online: usize,
   version: String,
+  my_user: Option<User_>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -352,7 +356,7 @@ impl Perform for Oper<GetSite> {
     pool: &DbPool,
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetSiteResponse, LemmyError> {
-    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<GetSite> {
       0
     };
 
+    // Giving back your user, if you're logged in
+    let my_user: Option<User_> = 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<TransferSite> {
     };
 
     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<TransferSite> {
       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<GetUserDetails> {
   ) -> Result<GetUserDetailsResponse, LemmyError> {
     let data: &GetUserDetails = &self.data;
 
-    let user_claims: Option<Claims> = match &data.auth {
+    // For logged in users, you need to get back subscribed, and settings
+    let user: Option<User_> = 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<CreatePrivateMessage> {
         let subject = &format!(
           "{} - Private Message from {}",
           Settings::get().hostname,
-          claims.username
+          user.name,
         );
         let html = &format!(
           "<h1>Private Message</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
-          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<any, InboxState> {
       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<UserView>;
   searchParam: string;
   toggleSearch: boolean;
+  siteLoading: boolean;
 }
 
 export class Navbar extends Component<any, NavbarState> {
   private wsSub: Subscription;
   private userSub: Subscription;
+  private unreadCountSub: Subscription;
   private searchTextField: RefObject<HTMLInputElement>;
   emptyState: NavbarState = {
-    isLoggedIn: UserService.Instance.user !== undefined,
+    isLoggedIn: false,
     unreadCount: 0,
     replies: [],
     mentions: [],
@@ -62,22 +65,13 @@ export class Navbar extends Component<any, NavbarState> {
     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<any, NavbarState> {
         () => 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<any, NavbarState> {
   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<any, NavbarState> {
     return (
       <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
         <div class="container">
-          <Link title={this.state.version} class="navbar-brand" to="/">
-            {this.state.siteName}
-          </Link>
+          {!this.state.siteLoading ? (
+            <Link title={this.state.version} class="navbar-brand" to="/">
+              {this.state.siteName}
+            </Link>
+          ) : (
+            <div class="navbar-item">
+              <svg class="icon icon-spinner spin">
+                <use xlinkHref="#icon-spinner"></use>
+              </svg>
+            </div>
+          )}
           {this.state.isLoggedIn && (
             <Link
               class="ml-auto p-0 navbar-toggler nav-link border-0"
@@ -180,151 +196,160 @@ export class Navbar extends Component<any, NavbarState> {
           >
             <span class="navbar-toggler-icon"></span>
           </button>
-          <div
-            className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
-          >
-            <ul class="navbar-nav my-2 mr-auto">
-              <li class="nav-item">
-                <Link
-                  class="nav-link"
-                  to="/communities"
-                  title={i18n.t('communities')}
-                >
-                  {i18n.t('communities')}
-                </Link>
-              </li>
-              <li class="nav-item">
-                <Link
-                  class="nav-link"
-                  to={{
-                    pathname: '/create_post',
-                    state: { prevPath: this.currentLocation },
-                  }}
-                  title={i18n.t('create_post')}
-                >
-                  {i18n.t('create_post')}
-                </Link>
-              </li>
-              <li class="nav-item">
-                <Link
-                  class="nav-link"
-                  to="/create_community"
-                  title={i18n.t('create_community')}
-                >
-                  {i18n.t('create_community')}
-                </Link>
-              </li>
-              <li className="nav-item">
-                <Link
-                  class="nav-link"
-                  to="/sponsors"
-                  title={i18n.t('donate_to_lemmy')}
-                >
-                  <svg class="icon">
-                    <use xlinkHref="#icon-coffee"></use>
-                  </svg>
-                </Link>
-              </li>
-            </ul>
-            {!this.context.router.history.location.pathname.match(
-              /^\/search/
-            ) && (
-              <form
-                class="form-inline"
-                onSubmit={linkEvent(this, this.handleSearchSubmit)}
-              >
-                <input
-                  class={`form-control mr-0 search-input ${
-                    this.state.toggleSearch ? 'show-input' : 'hide-input'
-                  }`}
-                  onInput={linkEvent(this, this.handleSearchParam)}
-                  value={this.state.searchParam}
-                  ref={this.searchTextField}
-                  type="text"
-                  placeholder={i18n.t('search')}
-                  onBlur={linkEvent(this, this.handleSearchBlur)}
-                ></input>
-                <button
-                  name="search-btn"
-                  onClick={linkEvent(this, this.handleSearchBtn)}
-                  class="btn btn-link"
-                  style="color: var(--gray)"
-                >
-                  <svg class="icon">
-                    <use xlinkHref="#icon-search"></use>
-                  </svg>
-                </button>
-              </form>
-            )}
-            <ul class="navbar-nav my-2">
-              {this.canAdmin && (
+          {!this.state.siteLoading && (
+            <div
+              className={`${
+                !this.state.expanded && 'collapse'
+              } navbar-collapse`}
+            >
+              <ul class="navbar-nav my-2 mr-auto">
+                <li class="nav-item">
+                  <Link
+                    class="nav-link"
+                    to="/communities"
+                    title={i18n.t('communities')}
+                  >
+                    {i18n.t('communities')}
+                  </Link>
+                </li>
+                <li class="nav-item">
+                  <Link
+                    class="nav-link"
+                    to={{
+                      pathname: '/create_post',
+                      state: { prevPath: this.currentLocation },
+                    }}
+                    title={i18n.t('create_post')}
+                  >
+                    {i18n.t('create_post')}
+                  </Link>
+                </li>
+                <li class="nav-item">
+                  <Link
+                    class="nav-link"
+                    to="/create_community"
+                    title={i18n.t('create_community')}
+                  >
+                    {i18n.t('create_community')}
+                  </Link>
+                </li>
                 <li className="nav-item">
                   <Link
                     class="nav-link"
-                    to={`/admin`}
-                    title={i18n.t('admin_settings')}
+                    to="/sponsors"
+                    title={i18n.t('donate_to_lemmy')}
                   >
                     <svg class="icon">
-                      <use xlinkHref="#icon-settings"></use>
+                      <use xlinkHref="#icon-coffee"></use>
                     </svg>
                   </Link>
                 </li>
+              </ul>
+              {!this.context.router.history.location.pathname.match(
+                /^\/search/
+              ) && (
+                <form
+                  class="form-inline"
+                  onSubmit={linkEvent(this, this.handleSearchSubmit)}
+                >
+                  <input
+                    class={`form-control mr-0 search-input ${
+                      this.state.toggleSearch ? 'show-input' : 'hide-input'
+                    }`}
+                    onInput={linkEvent(this, this.handleSearchParam)}
+                    value={this.state.searchParam}
+                    ref={this.searchTextField}
+                    type="text"
+                    placeholder={i18n.t('search')}
+                    onBlur={linkEvent(this, this.handleSearchBlur)}
+                  ></input>
+                  <button
+                    name="search-btn"
+                    onClick={linkEvent(this, this.handleSearchBtn)}
+                    class="btn btn-link"
+                    style="color: var(--gray)"
+                  >
+                    <svg class="icon">
+                      <use xlinkHref="#icon-search"></use>
+                    </svg>
+                  </button>
+                </form>
               )}
-            </ul>
-            {this.state.isLoggedIn ? (
-              <>
-                <ul class="navbar-nav my-2">
+              <ul class="navbar-nav my-2">
+                {this.canAdmin && (
                   <li className="nav-item">
-                    <Link class="nav-link" to="/inbox" title={i18n.t('inbox')}>
+                    <Link
+                      class="nav-link"
+                      to={`/admin`}
+                      title={i18n.t('admin_settings')}
+                    >
                       <svg class="icon">
-                        <use xlinkHref="#icon-bell"></use>
+                        <use xlinkHref="#icon-settings"></use>
                       </svg>
-                      {this.state.unreadCount > 0 && (
-                        <span class="ml-1 badge badge-light">
-                          {this.state.unreadCount}
-                        </span>
-                      )}
                     </Link>
                   </li>
-                </ul>
-                <ul class="navbar-nav">
+                )}
+              </ul>
+              {this.state.isLoggedIn ? (
+                <>
+                  <ul class="navbar-nav my-2">
+                    <li className="nav-item">
+                      <Link
+                        class="nav-link"
+                        to="/inbox"
+                        title={i18n.t('inbox')}
+                      >
+                        <svg class="icon">
+                          <use xlinkHref="#icon-bell"></use>
+                        </svg>
+                        {this.state.unreadCount > 0 && (
+                          <span class="ml-1 badge badge-light">
+                            {this.state.unreadCount}
+                          </span>
+                        )}
+                      </Link>
+                    </li>
+                  </ul>
+                  <ul class="navbar-nav">
+                    <li className="nav-item">
+                      <Link
+                        class="nav-link"
+                        to={`/u/${UserService.Instance.user.name}`}
+                        title={i18n.t('settings')}
+                      >
+                        <span>
+                          {UserService.Instance.user.avatar &&
+                            showAvatars() && (
+                              <img
+                                src={pictrsAvatarThumbnail(
+                                  UserService.Instance.user.avatar
+                                )}
+                                height="32"
+                                width="32"
+                                class="rounded-circle mr-2"
+                              />
+                            )}
+                          {UserService.Instance.user.name}
+                        </span>
+                      </Link>
+                    </li>
+                  </ul>
+                </>
+              ) : (
+                <ul class="navbar-nav my-2">
                   <li className="nav-item">
                     <Link
-                      class="nav-link"
-                      to={`/u/${UserService.Instance.user.username}`}
-                      title={i18n.t('settings')}
+                      class="btn btn-success"
+                      to="/login"
+                      title={i18n.t('login_sign_up')}
                     >
-                      <span>
-                        {UserService.Instance.user.avatar && showAvatars() && (
-                          <img
-                            src={pictrsAvatarThumbnail(
-                              UserService.Instance.user.avatar
-                            )}
-                            height="32"
-                            width="32"
-                            class="rounded-circle mr-2"
-                          />
-                        )}
-                        {UserService.Instance.user.username}
-                      </span>
+                      {i18n.t('login_sign_up')}
                     </Link>
                   </li>
                 </ul>
-              </>
-            ) : (
-              <ul class="navbar-nav my-2">
-                <li className="nav-item">
-                  <Link
-                    class="btn btn-success"
-                    to="/login"
-                    title={i18n.t('login_sign_up')}
-                  >
-                    {i18n.t('login_sign_up')}
-                  </Link>
-                </li>
-              </ul>
-            )}
-          </div>
+              )}
+            </div>
+          )}
         </div>
       </nav>
     );
@@ -400,38 +425,53 @@ export class Navbar extends Component<any, NavbarState> {
         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<any, NavbarState> {
   }
 
   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<any, PostState> {
         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<UserView>;
   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<string> = new Subject<string>();
+  public unreadCountSub: BehaviorSubject<number> = new BehaviorSubject<number>(
+    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() {
