Skip to content

Commit

Permalink
HOSTSD-254 Add preapproved user
Browse files Browse the repository at this point in the history
  • Loading branch information
Fosol committed Mar 8, 2024
1 parent 907a49a commit 084e184
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 75 deletions.
1 change: 1 addition & 0 deletions EMERALD.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Before running these commands you will need to create and populate configuration

There is a folder for each environment, and within each folder it should contain the following files.

- ches.env
- css.env
- dashboard.env
- database.env
Expand Down
7 changes: 5 additions & 2 deletions src/api/Keycloak/CssHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ private Task AddOrUpdateUserAsync(string username, Entities.User? user, HSB.CSS.
if (userRoles.Users.Length > 1) throw new NotAuthorizedException($"Keycloak has multiple users with the same username '{key}'");
if (user == null)
{
_logger.LogDebug("User activation, New User: {username}:{key}", username, key);
// Add the user to the database.
user = new Entities.User(username, email, key)
{
Expand All @@ -170,9 +171,11 @@ private Task AddOrUpdateUserAsync(string username, Entities.User? user, HSB.CSS.
LastLoginOn = DateTime.UtcNow,
};
_userService.Add(user);
_userService.CommitTransaction();
}
else if (user != null)
{
_logger.LogDebug("User activation, Update User: {username}:{key}", username, key);
// The user was created in HSB initially, but now the user has logged in and activated their account.
user.Username = username;
user.DisplayName = principal.GetDisplayName() ?? user.DisplayName;
Expand All @@ -189,15 +192,15 @@ private Task AddOrUpdateUserAsync(string username, Entities.User? user, HSB.CSS.
// Apply the preapproved roles to the user.
var roles = await UpdateUserRolesAsync(key.ToString(), preapprovedRoles);
_userService.Update(user);
return user;
_userService.CommitTransaction();
}
}
else
{
user.LastLoginOn = DateTime.UtcNow;
_userService.Update(user);
_userService.CommitTransaction();
}
_userService.CommitTransaction();

return user;
}
Expand Down
6 changes: 4 additions & 2 deletions src/dashboard/src/app/api/auth/authOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ export const authOptions: AuthOptions = {
const aToken = token as any;

if (trigger === 'update') {
token.roles = session.user.roles;
return token;
const refreshToken = await refreshAccessToken(token);
refreshToken.roles = session.user.roles;
refreshToken.decoded = jwtDecode(`${refreshToken.access_token}`) as any;
return refreshToken;
} else {
if (account && user) {
token.access_token = account.access_token;
Expand Down
6 changes: 5 additions & 1 deletion src/dashboard/src/app/hsb/admin/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import { EditUserRow } from './EditUserRow';
import { defaultUser } from './defaultUser';
import { validateUser } from './validateUser';

/**
* Provides a page component which displays all users so that the admin can modify their permissions.
* @returns Component
*/
export default function Page() {
const state = useAuth();
const { isReady: isReadyUsers, users } = useAdminUsers({ includePermissions: true, init: true });
Expand Down Expand Up @@ -74,7 +78,7 @@ export default function Page() {
}, []);

const handleRemoveUserRow = React.useCallback((userKey: string) => {
setFormUsers((currentUsers) => currentUsers.filter(user => user.key !== userKey));
setFormUsers((currentUsers) => currentUsers.filter((user) => user.key !== userKey));
}, []);

const handleUpdate = React.useCallback(async () => {
Expand Down
10 changes: 9 additions & 1 deletion src/dashboard/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
'use client';

import { Button } from '@/components';
import { useAuth } from '@/hooks';
import { signIn } from 'next-auth/react';
import styles from './Login.module.scss'
import { redirect } from 'next/navigation';
import styles from './Login.module.scss';

export default function Page() {
const { isAuthorized } = useAuth();

// Need this because after activating a pre-authorized user, they get sent to the login page again for some reason.
// One downside to this is it is impossible to go to the login page when already authorized.
if (isAuthorized) redirect('/');

return (
<div className={`dashboardContainer ${styles.container}`}>
<div className={styles.welcome}>
Expand Down
2 changes: 1 addition & 1 deletion src/dashboard/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useAuth } from '@/hooks';
import { redirect } from 'next/navigation';

export default function Page() {
const { isClient, isHSB, isAuthorized } = useAuth();
const { isClient, isHSB, isAuthorized, session } = useAuth();

// Redirect to default page for each type of user.
if (isClient) redirect('/client/dashboard');
Expand Down
23 changes: 21 additions & 2 deletions src/dashboard/src/app/welcome/Welcome.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@
font-weight: bold;
font-style: italic;
color: $bc-black;

&:hover {
text-decoration: underline;
text-decoration: underline;
}
}
}
Expand All @@ -50,4 +50,23 @@
display: flex;
flex-direction: column;
justify-content: flex-end;
}

.activate {
margin-top: 2rem;
padding: 1rem;
font-size: $font-size-18;
border-radius: 1rem;
background: #E8EAEC;
display: flex;
flex-direction: row;

>div:first-child {
flex: 2;
}

>div:nth-child(2) {
position: relative;
flex: 1;
}
}
33 changes: 31 additions & 2 deletions src/dashboard/src/app/welcome/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,44 @@
'use client';

import { LoadingAnimation } from '@/components';
import { useAuth } from '@/hooks';
import { useAppStore } from '@/store';
import { redirect } from 'next/navigation';
import styles from './Welcome.module.scss';

export default function Page() {
const { isAuthorized } = useAuth();
const userinfo = useAppStore((state) => state.userinfo);

// Need this because after activating a pre-authorized user so that it will redirect to their home page.
if (isAuthorized) redirect('/');

return (
<div className={`dashboardContainer ${styles.container}`}>
<div className={styles.welcome}>
<h2>Request Access</h2>
<p>Please email <a href="mailto:[email protected]">[email protected]</a> to request access to your organization&apos;s dashboard</p>
<p>
Please email <a href="mailto:[email protected]">[email protected]</a> to request
access to your organization&apos;s dashboard
</p>
{!userinfo && (
<div className={styles.activate}>
<div>
Activating account. If account has been pre-approved it will automatically apply roles
and redirect to home page.
</div>
<div>
<LoadingAnimation />
</div>
</div>
)}
{userinfo && !userinfo.roles.length && (
<div className={styles.activate}>Your account has not be pre-approved.</div>
)}
</div>
<div className={styles.login}>
<h2>Welcome to the Storage Dashboard</h2>
<p>Sign in to get insights into your organization&apos;s data storage and usage.</p>
<p>Request access to get insights into your organization&apos;s data storage and usage.</p>
</div>
</div>
);
Expand Down
25 changes: 15 additions & 10 deletions src/dashboard/src/components/auth/AuthState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useAppStore } from '@/store';
import { keycloakSessionLogOut } from '@/utils';
import _ from 'lodash';
import { signIn, useSession } from 'next-auth/react';
import { redirect } from 'next/navigation';
import { useRouter } from 'next/navigation';
import React from 'react';
import { FaSignInAlt, FaSignOutAlt } from 'react-icons/fa';
import { toast } from 'react-toastify';
Expand All @@ -19,36 +19,41 @@ export interface IAuthState {
export const AuthState: React.FC<IAuthState> = ({ showName }) => {
const { userinfo: getUserinfo } = useApiUsers();
const { status, session } = useAuth();
const activate = useAppStore((state) => state.activate);
const setActivate = useAppStore((state) => state.setActivate);
const userinfo = useAppStore((state) => state.userinfo);
const setUserinfo = useAppStore((state) => state.setUserinfo);
const { update } = useSession();

// Only ask for userinfo once.
const [init, setInit] = React.useState(true);
const router = useRouter();

React.useEffect(() => {
if (init && !userinfo && status === 'authenticated' && session) {
setInit(false);
if (activate && !userinfo && status === 'authenticated' && session) {
setActivate(false);
if (session.user.roles.length === 0) toast.info('Attempting to preapprove your account');
getUserinfo()
.then(async (res) => {
const userinfo = await res.json();
setUserinfo(userinfo);
// The user activation process can automatically apply roles to a preapproved user.
const roles = _.uniq(userinfo.groups.concat(session.user.roles));
if (
!session.user.roles.length ||
!userinfo.groups.every((group: string) => roles.includes(group))
) {
update({ ...session, user: { ...session.user, roles: roles } });
redirect('/');
// Inform the session of the pre-authorized user role changes.
await update({ ...session, user: { ...session.user, roles: roles } });
setUserinfo(userinfo);
router.push('/');
} else {
setUserinfo(userinfo);
}
})
.catch((error: any) => {
toast.error('Failed to activate user. Try to login again');
console.error('Failed to activate user', error);
setActivate(true);
});
}
}, [setUserinfo, status, getUserinfo, session, update, userinfo, init]);
}, [activate, setUserinfo, status, getUserinfo, session, update, userinfo, setActivate, router]);

if (status === 'loading') return <div>loading...</div>;
else if (status === 'authenticated') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,63 +1,72 @@
@import '@/styles/utils.scss';

.container {
height: 100%;
width: 100%;
background-color: rgba($white, 0.7);
position: absolute;
top: 0;
left: 0;
border-radius: 6px;
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
backdrop-filter: blur(2px);
height: 100%;
width: 100%;
background-color: rgba($white, 0.7);
position: absolute;
top: 0;
left: 0;
border-radius: 6px;
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
backdrop-filter: blur(2px);
}

.animationContainer {
height: 80px;
width: 80px;
display: flex;
justify-content: center;
align-items: flex-end;
gap: 0.2rem;

div {
animation: loading 2s infinite ease-in-out;
height: 5px;
width: 5px;
border-radius: 2px;
background-color: $bc-black;

&.bar1 {

}

&.bar2 {
animation-delay: 0.2s;
}

&.bar3 {
animation-delay: 0.4s;
}

&.bar4 {
animation-delay: 0.6s;
}

&.bar5 {
animation-delay: 0.8s;
}

&.bar6 {
animation-delay: 1s;
}
height: 80px;
width: 80px;
display: flex;
justify-content: center;
align-items: flex-end;
gap: 0.2rem;

div {
animation: loading 2s infinite ease-in-out;
height: 5px;
width: 5px;
border-radius: 2px;
background-color: $bc-black;

&.bar1 {}

&.bar2 {
animation-delay: 0.2s;
}

&.bar3 {
animation-delay: 0.4s;
}

&.bar4 {
animation-delay: 0.6s;
}

&.bar5 {
animation-delay: 0.8s;
}

&.bar6 {
animation-delay: 1s;
}
}
}

@keyframes loading {
0% {height: 10px;}
50% {height: 30px; background-color: rgba($bc-black, 0.5);}
100% {height: 10px};
0% {
height: 10px;
}

50% {
height: 30px;
background-color: rgba($bc-black, 0.5);
}

100% {
height: 10px
}

;
}
6 changes: 5 additions & 1 deletion src/dashboard/src/store/useAppStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { create } from 'zustand';

export interface IAppStoreState {
// User
userinfo?: IUserInfoModel; // TODO: Replace with interface.
activate: boolean;
setActivate: (value: boolean) => void;
userinfo?: IUserInfoModel;
setUserinfo: (value: IUserInfoModel) => void;

// Roles
Expand Down Expand Up @@ -51,6 +53,8 @@ export interface IAppStoreState {

export const useAppStore = create<IAppStoreState>((set) => ({
// User
activate: true,
setActivate: (value) => set((state) => ({ activate: value })),
userinfo: undefined,
setUserinfo: (value) => set((state) => ({ userinfo: value })),

Expand Down
Loading

0 comments on commit 084e184

Please sign in to comment.