From 882ad08f0be7e4c6404ac092d5b0564442d27706 Mon Sep 17 00:00:00 2001 From: Camden Mecklem Date: Tue, 14 Jul 2026 13:49:08 -0400 Subject: [PATCH 01/11] WIP --- .env.example | 9 + Gemfile | 6 +- Gemfile.lock | 33 ++-- app/controllers/api/base_controller.rb | 54 +++++- app/controllers/api/users_controller.rb | 22 +++ app/models/user.rb | 5 +- client/package.json | 1 + client/src/App.js | 161 +++++++++--------- client/src/components/AdminPage.js | 8 +- client/src/components/AuthenticatedRoute.js | 8 +- client/src/components/ClerkLoginModal.js | 34 ++++ .../src/components/ClerkLoginModal.module.css | 26 +++ client/src/components/OrganizationModal.js | 41 ----- client/src/components/Sidebar.js | 11 +- client/src/config/Api.js | 15 -- client/src/contexts/AuthenticationContext.js | 53 ++++++ client/src/i18n/en.json | 29 +--- client/src/index.js | 1 - client/src/pages/Login.js | 54 ++---- client/src/pages/Login.module.css | 10 +- client/src/pages/Organization.js | 18 +- client/src/pages/Project.js | 7 +- client/src/pages/Resource.js | 9 +- client/src/pages/User.js | 94 ++-------- client/src/services/Authentication.js | 109 ------------ client/src/services/Users.js | 5 + client/src/transforms/Authentication.js | 35 ---- client/src/transforms/Organization.js | 20 --- client/src/transforms/User.js | 27 +-- client/src/types/User.js | 3 +- client/yarn.lock | 37 +++- config/initializers/jwt_auth.rb | 5 - config/routes.rb | 8 +- db/migrate/20260713155706_add_sso_ids.rb | 6 + ...60713181018_remove_passwords_from_users.rb | 5 + db/schema.rb | 7 +- db/seeds.rb | 2 +- 37 files changed, 432 insertions(+), 546 deletions(-) create mode 100644 client/src/components/ClerkLoginModal.js create mode 100644 client/src/components/ClerkLoginModal.module.css delete mode 100644 client/src/components/OrganizationModal.js delete mode 100644 client/src/config/Api.js create mode 100644 client/src/contexts/AuthenticationContext.js delete mode 100644 client/src/services/Authentication.js delete mode 100644 client/src/transforms/Authentication.js delete mode 100644 config/initializers/jwt_auth.rb create mode 100644 db/migrate/20260713155706_add_sso_ids.rb create mode 100644 db/migrate/20260713181018_remove_passwords_from_users.rb diff --git a/.env.example b/.env.example index 2e66fc3..6d510c6 100644 --- a/.env.example +++ b/.env.example @@ -41,6 +41,12 @@ CANTALOUPE_API_USERNAME=admin # API password for the Cantaloupe instance CANTALOUPE_API_PASSWORD=mySecret +# Clerk config +VITE_CLERK_SIGN_IN_URL=https://staging.performant.studio/sso +VITE_CLERK_PUBLISHABLE_KEY=my_key +CLERK_SECRET_KEY=my_secret_key +VITE_CLERK_SIGN_IN_FORCE_REDIRECT_URL=http://localhost:6780/dashboard + # DB Host name. In docker-compose setup this should be db. # Otherwise it should be the host of the DB (i.e. localhost, NOT http://localhost) DATABASE_HOST=db @@ -100,6 +106,9 @@ SECRET_KEY_BASE=ac2f3e6030bbd6f234267dc400996a1ec109e50b6317b8de34cc76cd9136dd1e # Recommended: 5 SIDEKIQ_CONCURRENCY=5 +# Vite environment +VITE_ENVIRONMENT=development + # Port to use for the Vite dev server. VITE_PORT=6780 diff --git a/Gemfile b/Gemfile index 2a35666..13df87d 100644 --- a/Gemfile +++ b/Gemfile @@ -27,9 +27,6 @@ gem 'resource_api', git: 'https://github.com/performant-software/resource-api.gi # Use Json Web Token (JWT) for token based authentication gem 'jwt', '~> 3.1.2' -# JWT authentication -gem 'jwt_auth', git: 'https://github.com/performant-software/jwt-auth.git', tag: 'v0.1.3' - # Use ActiveModel has_secure_password gem 'bcrypt', '~> 3.1.20' @@ -51,6 +48,9 @@ gem 'data_migrate', '~> 11.3' # Background jobs gem 'sidekiq', '~> 8.0.5', group: :production +# Clerk +gem 'clerk-sdk-ruby', '>= 7.0' + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem 'debug', platforms: %i[ mri mingw x64_mingw ] diff --git a/Gemfile.lock b/Gemfile.lock index 2825dc0..72068f7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,12 +1,3 @@ -GIT - remote: https://github.com/performant-software/jwt-auth.git - revision: 993b47f395d7349c499a0ffda6ef286871ef89dc - tag: v0.1.3 - specs: - jwt_auth (0.1.0) - jwt (~> 3.1.2) - rails (>= 7.1, < 9) - GIT remote: https://github.com/performant-software/resource-api.git revision: ab5afbdfedfd4e89309d748b46167652562a22bf @@ -125,7 +116,14 @@ GEM bootsnap (1.18.4) msgpack (~> 1.2) builder (3.3.0) - concurrent-ruby (1.3.4) + clerk-sdk-ruby (7.0.0) + base64 (>= 0.2.0, < 1.0) + concurrent-ruby (~> 1.3.5) + faraday (>= 2.14.1) + faraday-multipart (~> 1.2.0) + faraday-retry (~> 2.4.0) + jwt (~> 3.0) + concurrent-ruby (1.3.7) connection_pool (2.5.3) crass (1.0.6) csv (3.3.5) @@ -142,6 +140,16 @@ GEM railties (>= 6.1) drb (2.2.3) erubi (1.13.1) + faraday (2.14.3) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-multipart (1.2.0) + multipart-post (~> 2.0) + faraday-net_http (3.4.4) + net-http (~> 0.5) + faraday-retry (2.4.0) + faraday (~> 2.0) globalid (1.2.1) activesupport (>= 6.1) httparty (0.23.1) @@ -176,6 +184,9 @@ GEM msgpack (1.7.5) multi_xml (0.7.2) bigdecimal (~> 3.1) + multipart-post (2.4.1) + net-http (0.9.1) + uri (>= 0.11.1) net-imap (0.5.9) date net-protocol @@ -280,12 +291,12 @@ DEPENDENCIES aws-sdk-s3 (~> 1.193) bcrypt (~> 3.1.20) bootsnap + clerk-sdk-ruby (>= 7.0) data_migrate (~> 11.3) debug dotenv-rails httparty (~> 0.23.1) jwt (~> 3.1.2) - jwt_auth! mini_magick (~> 5.3.0) pg (~> 1.5.9) puma (~> 6.6.0) diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 4f618c1..8cd05cb 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -1,11 +1,57 @@ +require 'clerk' + class Api::BaseController < Api::ResourceController - # Includes - include JwtAuth::Authenticateable + protected - # Actions before_action :authenticate_request - protected + attr_reader :current_user + + def authenticate_request + token = request.cookies["__session"] + return deny_access unless token.present? + + clerk_session = clerk_client.verify_token(token) + clerk_id = clerk_session["sub"] + + @current_user = User.find_by(sso_id: clerk_id) + + # If the user exists in Clerk but not in FairImage, + # create a local account for them automatically. + if @current_user.nil? + clerk_user = get_clerk_data(clerk_id) + @current_user = create_user_from_clerk(clerk_user) + end + + return deny_access unless @current_user + + @current_user + end + + def clerk_client + @clerk_client ||= Clerk::SDK.new(secret_key: ENV.fetch("CLERK_SECRET_KEY")) + end + + def create_user_from_clerk(clerk_user) + user = User.new( + sso_id: clerk_user.id, + email: clerk_user.email_addresses.first.email_address, + name: [clerk_user.first_name, clerk_user.last_name].join(" "), + role: 'member', + ) + + user.save! + + user + end + + def get_clerk_data(clerk_id) + clerk_client.users.get(user_id: clerk_id).user + end + + def get_clerk_role(clerk_user) + clerk_user.public_metadata["role"] + end def check_authorization(organization_id) deny_access unless current_user.has_access?(organization_id) diff --git a/app/controllers/api/users_controller.rb b/app/controllers/api/users_controller.rb index aa69804..c879c3c 100644 --- a/app/controllers/api/users_controller.rb +++ b/app/controllers/api/users_controller.rb @@ -10,6 +10,20 @@ class Api::UsersController < Api::BaseController before_action :validate_new_user, unless: -> { current_user.admin? }, only: :create before_action :validate_user, unless: -> { current_user.admin? }, only: [:update, :destroy] + def me + return unless current_user + + puts "Attempting to authenticate user #{current_user.id}" + + clerk_user = get_clerk_data(current_user.sso_id) + + update_user_from_sso(current_user, clerk_user) + + serializer = UsersSerializer.new + + render json: serializer.render_show(current_user), status: :ok + end + protected def base_query @@ -26,6 +40,14 @@ def base_query private + def update_user_from_sso(local_user, sso_user) + if sso_user.private_metadata['is_global_admin'] == true + local_user[:admin] = true + else + local_user[:admin] = false + end + end + def validate_new_user organization_ids = params[:user][:user_organizations].map{ |uo| uo[:organization_id] } check_authorization organization_ids diff --git a/app/models/user.rb b/app/models/user.rb index bc9420d..71cc5e5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,9 +2,6 @@ class User < ApplicationRecord # Includes include Attachable - # JWT - has_secure_password - # Relationships has_many :user_organizations, dependent: :destroy @@ -12,7 +9,7 @@ class User < ApplicationRecord accepts_nested_attributes_for :user_organizations, allow_destroy: true # Resourceable parameters - allow_params :name, :email, :admin, :api_key, :avatar, :password, :password_confirmation, + allow_params :name, :email, :admin, :api_key, :avatar, user_organizations_attributes: [:id, :organization_id, :_destroy] # ActiveStorage diff --git a/client/package.json b/client/package.json index dd23b7a..b46a467 100644 --- a/client/package.json +++ b/client/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@bunchtogether/vite-plugin-flow": "^1.0.2", + "@clerk/react": "^6.12.2", "@performant-software/semantic-components": "^3.1.0", "@performant-software/shared-components": "^3.1.0", "@performant-software/user-defined-fields": "^3.1.0", diff --git a/client/src/App.js b/client/src/App.js index edbc95a..cf91d7d 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -21,103 +21,112 @@ import Resource from './pages/Resource'; import Resources from './pages/Resources'; import User from './pages/User'; import Users from './pages/Users'; +import { ClerkProvider } from '@clerk/react'; +import { AuthenticationContextProvider } from './contexts/AuthenticationContext'; const App: ComponentType = () => ( - - - - } - exact - path='/' - /> - - - - )} - path='/' - > - } - path='/dashboard' - /> - } - path='/organizations' - /> - } - path='/organizations/new' - /> - } - path='/organizations/:organizationId' - /> - + + + + } - index + element={} + exact + path='/' /> } - path='new' - /> - + + + )} + path='/' > } - index + element={} + path='/dashboard' /> } + path='/organizations' + /> + } + path='/organizations/new' + /> + } + path='/organizations/:organizationId' + /> + } + element={} index /> } + element={} path='new' /> } - path=':resourceId' - /> + path=':projectId' + > + } + index + /> + + } + index + /> + } + path='new' + /> + } + path=':resourceId' + /> + + + } + path='/users' + /> + } + path='/users/new' + /> + } + path='/users/:userId' + /> - - } - path='/users' - /> - } - path='/users/new' - /> - } - path='/users/:userId' - /> - - + )} + path='*' /> - )} - path='*' - /> - - - + + + + + ); export default App; diff --git a/client/src/components/AdminPage.js b/client/src/components/AdminPage.js index 7d99748..a56f3f3 100644 --- a/client/src/components/AdminPage.js +++ b/client/src/components/AdminPage.js @@ -1,11 +1,13 @@ // @flow -import React, { type ComponentType } from 'react'; +import React, { type ComponentType, useContext } from 'react'; import { Navigate } from 'react-router'; -import AuthenticationService from '../services/Authentication'; +import { AuthenticationContext } from '../contexts/AuthenticationContext'; const AdminPage: ComponentType = (props) => { - if (!AuthenticationService.isAdmin()) { + const { user } = useContext(AuthenticationContext); + + if (!user.admin) { return ; } diff --git a/client/src/components/AuthenticatedRoute.js b/client/src/components/AuthenticatedRoute.js index 30e95c9..46e30a0 100644 --- a/client/src/components/AuthenticatedRoute.js +++ b/client/src/components/AuthenticatedRoute.js @@ -1,15 +1,17 @@ // @flow -import React, { type ComponentType } from 'react'; +import React, { type ComponentType, useContext } from 'react'; import { Navigate } from 'react-router'; -import AuthenticationService from '../services/Authentication'; +import { AuthenticationContext } from '../contexts/AuthenticationContext'; type Props = { children: any }; const AuthenticatedRoute: ComponentType = ({ children }: Props) => { - if (!AuthenticationService.isAuthenticated()) { + const { user } = useContext(AuthenticationContext); + + if (!user) { return ; } diff --git a/client/src/components/ClerkLoginModal.js b/client/src/components/ClerkLoginModal.js new file mode 100644 index 0000000..b2cbb1d --- /dev/null +++ b/client/src/components/ClerkLoginModal.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { Button } from 'semantic-ui-react'; +import { useClerk } from '@clerk/react'; +import { useTranslation } from 'react-i18next'; +import styles from './ClerkLoginModal.module.css'; + +const ClerkLoginModal = () => { + const { t } = useTranslation(); + + const { buildSignInUrl } = useClerk(); + + return ( +
+
+

+ {t('ClerkLoginModal.header')} +

+ +
+
+ ); +} + +export default ClerkLoginModal; diff --git a/client/src/components/ClerkLoginModal.module.css b/client/src/components/ClerkLoginModal.module.css new file mode 100644 index 0000000..6abf62f --- /dev/null +++ b/client/src/components/ClerkLoginModal.module.css @@ -0,0 +1,26 @@ +.clerkModalContainer { + height: 100vh; + width: 100vw; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; +} + +.clerkModal { + background-color: white; + border-radius: 8px; + padding: 48px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + align-items: center; +} + +.clerkModalHeader { + margin-bottom: 28px; + font-size: 26px; + font-weight: bold; + color: #333; + text-align: center; +} diff --git a/client/src/components/OrganizationModal.js b/client/src/components/OrganizationModal.js deleted file mode 100644 index 910b05f..0000000 --- a/client/src/components/OrganizationModal.js +++ /dev/null @@ -1,41 +0,0 @@ -// @flow - -import { AssociatedDropdown } from '@performant-software/semantic-components'; -import React, { type ComponentType } from 'react'; -import { withTranslation } from 'react-i18next'; -import { Form, Modal } from 'semantic-ui-react'; -import Organization from '../transforms/Organization'; -import OrganizationsService from '../services/Organizations'; - -const OrganizationModal: ComponentType = withTranslation()((props) => ( - - - - - OrganizationsService.fetchAll({ search, sort_by: 'name' })} - onSelection={props.onAssociationInputChange.bind(this, 'organization_id', 'organization')} - renderOption={(organization) => Organization.toDropdown(organization)} - searchQuery={props.item.organization && props.item.organization.name} - value={props.item.organization_id} - /> - - - { props.children } - -)); - -export default OrganizationModal; diff --git a/client/src/components/Sidebar.js b/client/src/components/Sidebar.js index 4279046..47ed65a 100644 --- a/client/src/components/Sidebar.js +++ b/client/src/components/Sidebar.js @@ -1,14 +1,14 @@ // @flow import cx from 'classnames'; -import React, { useCallback, type ComponentType } from 'react'; +import React, { useCallback, type ComponentType, useContext } from 'react'; import { useNavigate, useParams } from 'react-router'; import { useTranslation } from 'react-i18next'; import { Icon, Menu, Popup } from 'semantic-ui-react'; -import AuthenticationService from '../services/Authentication'; import MenuLink from './MenuLink'; import styles from './Sidebar.module.css'; import type { Translateable } from '../types/Translateable'; +import { AuthenticationContext } from '../contexts/AuthenticationContext'; type Props = Translateable & { context: { @@ -20,13 +20,14 @@ const Sidebar: ComponentType = (props: Props) => { const navigate = useNavigate(); const params = useParams(); const { t } = useTranslation(); + const { signOut, user } = useContext(AuthenticationContext); /** * Logs the user out and navigates to the index page. * * @type {function(): Promise|Promise|Promise<*>|*} */ - const onLogout = useCallback(() => AuthenticationService.logout().then(() => navigate('/login')), []); + const onLogout = useCallback(() => signOut().then(() => navigate('/login')), []); return (
= (props: Props) => { }} /> - { AuthenticationService.isAdmin() && ( + { user.admin && ( = (props: Props) => { )} /> )} - { AuthenticationService.isAdmin() && ( + { user.admin && ( { -// Sets the authentication token as a request header - axios.interceptors.request.use((config) => { - // Set the user authentication token - _.extend(config.headers, { Authorization: AuthenticationService.getToken() }); - - return config; - }, (error) => Promise.reject(error)); -}); diff --git a/client/src/contexts/AuthenticationContext.js b/client/src/contexts/AuthenticationContext.js new file mode 100644 index 0000000..b36234d --- /dev/null +++ b/client/src/contexts/AuthenticationContext.js @@ -0,0 +1,53 @@ +// @flow + +import React, { createContext, useEffect, useMemo, useState } from 'react'; +import UsersService from '../services/Users'; +import { useAuth } from '@clerk/react'; +import type { User as UserType } from '../types/User'; + +interface AuthenticationContextType { + setUser: (user: UserType | null) => void; + signOut: () => void; + user: UserType | null; +} + +export const AuthenticationContext = createContext({ + setUser: () => {}, + signOut: () => {}, + user: null, +}); + +interface AuthenticationContextProps { + children?: any; +} + +export const AuthenticationContextProvider = ({ children }: AuthenticationContextProps) => { + const clerkAuth = useAuth(); + const [user, setUser] = useState(null); + const [ready, setReady] = useState(false); + + useEffect(() => { + if (clerkAuth.isSignedIn && !user) { + UsersService.getMe() + .then(res => setUser(res.data)) + .catch(() => clerkAuth.signOut()); + } + }, [clerkAuth.isSignedIn]); + + useEffect(() => { + if (clerkAuth.isLoaded && !!(user || !clerkAuth.isSignedIn)) { + setReady(true); + } + }, [clerkAuth.isSignedIn, user]); + + const value = useMemo(() => { + return { user, setUser, signOut: clerkAuth.signOut }; + }, [user, setUser, clerkAuth.signOut]); + + return ( + + {ready && children} + + ); +}; + diff --git a/client/src/i18n/en.json b/client/src/i18n/en.json index e4cf9f0..6ba3e65 100644 --- a/client/src/i18n/en.json +++ b/client/src/i18n/en.json @@ -18,6 +18,10 @@ "noContent": "No attached content." } }, + "ClerkLoginModal": { + "header": "Welcome to FairImage", + "login": "Sign in with Performant Studio" + }, "Common": { "buttons": { "add": "Add", @@ -85,15 +89,6 @@ } } }, - "OrganizationModal": { - "labels": { - "organization": "Organization" - }, - "title": { - "add": "Add Organization", - "edit": "Edit Organization" - } - }, "Organizations": { "columns": { "location": "Location", @@ -172,19 +167,13 @@ "labels": { "admin": "Administrator", "email": "Email", - "name": "Name", - "password": "Password", - "passwordConfirmation": "Password confirmation" + "name": "Name" }, "organizations": { "columns": { "location": "Location", "name": "Name" } - }, - "password": { - "content": "Passwords must be at least eight (8) characters.", - "header": "Password Policy" } }, "SystemStatus": { @@ -207,14 +196,6 @@ "edit": "Edit User" } }, - "UserOrganizationModal": { - "labels": { - "organization": "Organization" - }, - "title": { - "add": "Add User Organization" - } - }, "Users": { "columns": { "admin": "Administrator", diff --git a/client/src/index.js b/client/src/index.js index beb585a..59ac103 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -5,7 +5,6 @@ import ReactDOM from 'react-dom/client'; import App from './App'; // Configuration -import './config/Api'; import './i18n/i18n'; // CSS diff --git a/client/src/pages/Login.js b/client/src/pages/Login.js index d8e7883..f973dcf 100644 --- a/client/src/pages/Login.js +++ b/client/src/pages/Login.js @@ -1,56 +1,32 @@ // @flow -import React, { useCallback, useState, type ComponentType } from 'react'; -import { LoginModal } from '@performant-software/semantic-components'; +import React, { type ComponentType } from 'react'; +import { SignIn, useAuth } from '@clerk/react'; import { Navigate } from 'react-router'; -import { Image } from 'semantic-ui-react'; -import AuthenticationService from '../services/Authentication'; import styles from './Login.module.css'; +import ClerkLoginModal from '../components/ClerkLoginModal'; -const Login: ComponentType = () => { - const [disabled, setDisabled] = useState(false); - const [email, setEmail] = useState(); - const [error, setError] = useState(false); - const [password, setPassword] = useState(); - - /** - * Attempts to authenticate then navigates to the admin page. - * - * @type {(function(): void)|*} - */ - const onLogin = useCallback(() => { - setDisabled(true); +const isDevelopment = import.meta.env.VITE_ENVIRONMENT === 'development'; - AuthenticationService - .login({ email, password }) - .catch(() => setError(true)) - .finally(() => setDisabled(false)); - }, [email, password]); +const Login: ComponentType = () => { + const { isLoaded, isSignedIn } = useAuth(); - if (AuthenticationService.isAuthenticated()) { - if (AuthenticationService.isAdmin()) { - return ; - } + if (!isLoaded) { + return null; + } - return ; + if (isSignedIn) { + return ; } return (
- - setPassword(value)} - onUsernameChange={(e, { value }) => setEmail(value)} - open - /> + {isDevelopment + ? + : + }
); }; diff --git a/client/src/pages/Login.module.css b/client/src/pages/Login.module.css index cb3e2bb..d22d671 100644 --- a/client/src/pages/Login.module.css +++ b/client/src/pages/Login.module.css @@ -1,5 +1,9 @@ -.login .background { - object-fit: cover; - width: 100vw; +.login { + background: url('/assets/clouds.jpeg') center; + background-size: cover; height: 100vh; + width: 100vw; + display: flex; + align-items: center; + justify-content: center; } diff --git a/client/src/pages/Organization.js b/client/src/pages/Organization.js index 9c2c3af..eb1bf15 100644 --- a/client/src/pages/Organization.js +++ b/client/src/pages/Organization.js @@ -9,7 +9,6 @@ import AdminPage from '../components/AdminPage'; import type { Organization as OrganizationType } from '../types/Organization'; import OrganizationsService from '../services/Organizations'; import SimpleEditPage from '../components/SimpleEditPage'; -import UserModal from '../components/UserModal'; import withEditPage from '../hooks/EditPage'; type Props = EditContainerProps & { @@ -29,10 +28,8 @@ const OrganizationForm = (props: Props) => { name={t('Common.tabs.details')} > { name={t('Common.tabs.users')} > { resolve: (u) => u.user.email, sortable: true }]} - modal={{ - component: UserModal, - props: { - required: ['user_id'] - } - }} items={props.item.user_organizations} - onDelete={props.onDeleteChildAssociation.bind(this, 'user_organizations')} - onSave={props.onSaveChildAssociation.bind(this, 'user_organizations')} /> diff --git a/client/src/pages/Project.js b/client/src/pages/Project.js index 9294767..8223d26 100644 --- a/client/src/pages/Project.js +++ b/client/src/pages/Project.js @@ -2,25 +2,26 @@ import { AssociatedDropdown, FileInputButton, LazyImage } from '@performant-software/semantic-components'; import { UserDefinedFields, UserDefinedFieldsEmbeddedList } from '@performant-software/user-defined-fields'; -import React, { type ComponentType, useEffect } from 'react'; +import React, { type ComponentType, useContext, useEffect } from 'react'; import { withTranslation } from 'react-i18next'; import { Button, Form } from 'semantic-ui-react'; import _ from 'underscore'; -import AuthenticationService from '../services/Authentication'; import Organization from '../transforms/Organization'; import OrganizationsService from '../services/Organizations'; import ProjectsService from '../services/Projects'; import ReadOnlyField from '../components/ReadOnlyField'; import SimpleEditPage from '../components/SimpleEditPage'; import withEditPage from '../hooks/EditPage'; +import { AuthenticationContext } from '../contexts/AuthenticationContext'; const ProjectForm = withTranslation()((props) => { + const { user } = useContext(AuthenticationContext); + /** * Pre-populate the organization dropdown if the user is only a member of one organization. */ useEffect(() => { if (!props.item.id) { - const user = AuthenticationService.getCurrentUser(); if (user.user_organizations && user.user_organizations.length === 1) { const { organization } = _.first(user.user_organizations); props.onSetState({ organization_id: organization.id, organization }); diff --git a/client/src/pages/Resource.js b/client/src/pages/Resource.js index f5406b3..ed88078 100644 --- a/client/src/pages/Resource.js +++ b/client/src/pages/Resource.js @@ -6,6 +6,7 @@ import type { EditContainerProps } from '@performant-software/shared-components/ import { UserDefinedFieldsForm, UserDefinedFields } from '@performant-software/user-defined-fields'; import React, { useCallback, + useContext, useEffect, useMemo, useState, @@ -22,7 +23,6 @@ import { Segment } from 'semantic-ui-react'; import AttachmentDetails from '../components/AttachmentDetails'; -import AuthenticationService from '../services/Authentication'; import ProjectsService from '../services/Projects'; import ReadOnlyField from '../components/ReadOnlyField'; import type { Resource as ResourceType } from '../types/Resource'; @@ -32,6 +32,7 @@ import SimpleEditPage from '../components/SimpleEditPage'; import StatusIcon from '../components/StatusIcon'; import styles from './Resource.module.css'; import withEditPage from '../hooks/EditPage'; +import { AuthenticationContext } from '../contexts/AuthenticationContext'; type Props = EditContainerProps & { item: ResourceType @@ -53,6 +54,8 @@ const ResourceForm = (props: Props) => { const { projectId } = useParams(); const { t } = useTranslation(); + const { user } = useContext(AuthenticationContext); + /** * Memo-izes the current attachment info. * @@ -221,7 +224,7 @@ const ResourceForm = (props: Props) => { status={props.item.content_converted_info ? 'positive' : 'negative'} /> - { AuthenticationService.isAdmin() && ( + { user.admin && ( @@ -237,7 +240,7 @@ const ResourceForm = (props: Props) => { - { AuthenticationService.isAdmin() && attachment && ( + { user.admin && attachment && (
diff --git a/client/src/pages/User.js b/client/src/pages/User.js index 045d2e1..77b7d03 100644 --- a/client/src/pages/User.js +++ b/client/src/pages/User.js @@ -1,27 +1,27 @@ // @flow -import { EmbeddedList, FileInputButton, LazyImage } from '@performant-software/semantic-components'; +import { EmbeddedList } from '@performant-software/semantic-components'; import type { EditContainerProps } from '@performant-software/shared-components/types'; -import React, { type ComponentType, useEffect } from 'react'; +import React, { type ComponentType, useContext, useEffect } from 'react'; import { withTranslation } from 'react-i18next'; import { v4 as uuid } from 'uuid'; -import { Button, Form, Message } from 'semantic-ui-react'; +import { Form } from 'semantic-ui-react'; import _ from 'underscore'; -import AuthenticationService from '../services/Authentication'; import i18n from '../i18n/i18n'; -import OrganizationModal from '../components/OrganizationModal'; import SimpleEditPage from '../components/SimpleEditPage'; import type { Translateable } from '../types/Translateable'; import UsersService from '../services/Users'; import withEditPage from '../hooks/EditPage'; +import { AuthenticationContext } from '../contexts/AuthenticationContext'; const UserForm = withTranslation()((props: EditContainerProps & Translateable) => { + const { user } = useContext(AuthenticationContext); + /** * Pre-populate the organization if the user is only a member of one organization. */ useEffect(() => { if (!props.item.id) { - const user = AuthenticationService.getCurrentUser(); if (user.user_organizations && user.user_organizations.length === 1) { const { organization } = _.first(user.user_organizations); props.onSetState({ user_organizations: [{ organization_id: organization.id, organization }] }); @@ -38,59 +38,20 @@ const UserForm = withTranslation()((props: EditContainerProps & Translateable) = name={props.t('Common.tabs.details')} > - - { !props.item.avatar_url && ( - { - const file = _.first(files); - const url = URL.createObjectURL(file); - props.onSetState({ - avatar: file, - avatar_url: url, - avatar_preview_url: url - }); - }} - /> - )} - { props.item.avatar_url && ( -