Skip to content

Add login flow #530

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 42 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
92c123f
Add endpoint for getting current user info
dadmobile Jun 6, 2025
f814c4c
Added user modal
aahaanmaini Jun 6, 2025
30f5410
Add API function to login
dadmobile Jun 9, 2025
72aebdd
Merge branch 'add/users_api' of https://github.com/transformerlab/tra…
dadmobile Jun 9, 2025
f9c105e
Merge conflict
dadmobile Jun 9, 2025
06303e4
Remove incorrectly added code from merge conflict.
dadmobile Jun 9, 2025
046faa6
Add a check for errors when fetching inside useAPI
dadmobile Jun 9, 2025
1e0edb6
Add a function to get current user access token
dadmobile Jun 9, 2025
00626a2
Add getAccessToken to API SDK
dadmobile Jun 9, 2025
e79294e
Remove bad line from merge conflict.
dadmobile Jun 9, 2025
37f9091
Pass test access token and check for authorization in useAPI
dadmobile Jun 9, 2025
219d5e0
Change react imports to standard style.
dadmobile Jun 10, 2025
8be72c3
Rename UserModal to UserLoginModal
dadmobile Jun 10, 2025
62998b3
Rename UserModal to UserLoginModal
dadmobile Jun 10, 2025
86b99d4
Temporarily only bring up login when username clicked.
dadmobile Jun 10, 2025
79cbe00
Remove user console logging
dadmobile Jun 10, 2025
526e126
Use getFullPath to get login URL
dadmobile Jun 10, 2025
e0b325b
Fix getAccessToken to await on storage
dadmobile Jun 10, 2025
7260b23
Get access token from storage and add to API calls.
dadmobile Jun 10, 2025
6de3a41
Return an empty string for access token if none is set
dadmobile Jun 10, 2025
8655961
Add a function for logging out
dadmobile Jun 10, 2025
16793dc
Add logout to API SDK
dadmobile Jun 10, 2025
b018e8c
Remove debug output
dadmobile Jun 10, 2025
0eb529f
Make logout button actually work. Temp make user icon test login.
dadmobile Jun 10, 2025
6d0d6ec
Alternate between login and UserDetails depending on login state on s…
dadmobile Jun 10, 2025
9270031
Make sure mutates happen when login state changes.
dadmobile Jun 10, 2025
92e740a
Only the logout icon should be clickable on user details
dadmobile Jun 10, 2025
9023229
Move login code to login modal from sidebar.
dadmobile Jun 10, 2025
2c68960
Make user modal close on successful login
dadmobile Jun 10, 2025
3cb2613
Properly close user modal so escape key works.
dadmobile Jun 11, 2025
3ef82f7
Properly read the formdata on login
dadmobile Jun 11, 2025
8930f34
Reset user modal on incorrect login information.
dadmobile Jun 11, 2025
d9715e3
Make login button look like other sidebar items
dadmobile Jun 11, 2025
53bec10
Add a message to login model for errors and failed login.
dadmobile Jun 12, 2025
ab6298b
Add auth register endpoint
dadmobile Jun 12, 2025
03690a4
Add auth endpoint to SDK
dadmobile Jun 12, 2025
cd3dc53
Create a shared function for registering a user
dadmobile Jun 12, 2025
76ace00
Update login modal to use register user API
dadmobile Jun 12, 2025
4aa4c55
Merge branch 'main' into add/users_api
dadmobile Jun 24, 2025
a4dd49e
Fix broken merge conflict.
dadmobile Jun 25, 2025
6eb3ae3
Clean up import list on sidebar.
dadmobile Jun 25, 2025
bacd667
Remove unneeded state variable storing user details.
dadmobile Jun 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 114 additions & 35 deletions src/renderer/components/Nav/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { useNavigate } from 'react-router-dom';
import { useState, useEffect } from 'react';

import List from '@mui/joy/List';
import Divider from '@mui/joy/Divider';

import {
CodeIcon,
GraduationCapIcon,
Expand All @@ -24,14 +21,21 @@ import {
WorkflowIcon,
UserIcon,
LogOutIcon,
LogInIcon,
} from 'lucide-react';

import { RiImageAiLine } from 'react-icons/ri';

import {
Box,
ButtonGroup,
Divider,
IconButton,
List,
ListItem,
ListItemButton,
ListItemContent,
ListItemDecorator,
Sheet,
Tooltip,
Typography,
Expand All @@ -40,10 +44,13 @@ import {
import {
useModelStatus,
usePluginStatus,
useAPI,
logout,
getFullPath,
} from 'renderer/lib/transformerlab-api-sdk';

import SelectExperimentMenu from '../Experiment/SelectExperimentMenu';
import UserLoginModal from '../User/UserLoginModal';

import SubNavItem from './SubNavItem';
import ColorSchemeToggle from './ColorSchemeToggle';
Expand Down Expand Up @@ -218,10 +225,70 @@ function GlobalMenuItems({ DEV_MODE, experimentInfo, outdatedPluginsCount }) {
);
}

function UserDetailsPanel({userDetails, mutate}) {
return (
<>
<UserIcon />
<Box
sx={{ minWidth: 0, flex: 1 }}
>
<Typography
level="title-sm"
sx={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{userDetails?.name}
</Typography>
<Typography
level="body-xs"
sx={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{userDetails?.email}
</Typography>
</Box>

<IconButton
size="sm"
variant="plain"
color="neutral"
sx={{
cursor: 'pointer',
'&:hover': {
backgroundColor: 'var(--joy-palette-neutral-100)',
borderRadius: 'sm',
},
}}
>
<LogOutIcon
size="18px"
onClick={async() => {
await logout();
mutate();
alert("User logged out.");
}}
/>
</IconButton>
</>
);
}

function BottomMenuItems({ DEV_MODE, navigate, themeSetter }) {
const [userLoginModalOpen, setUserLoginModalOpen] = useState(false);
const { data: userInfo, error: userError, isLoading: userLoading, mutate: userMutate } = useAPI('users', ['me'], {});

if (userError) {
console.log(userError);
}

return (
<>
{' '}
<Divider sx={{ my: 2 }} />
<Box
sx={{
Expand All @@ -232,37 +299,42 @@ function BottomMenuItems({ DEV_MODE, navigate, themeSetter }) {
maxWidth: '180px',
}}
>
{/* <Avatar
variant="outlined"
size="sm"
src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=286"
/> */}
<UserIcon />
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography
level="title-sm"
sx={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
User Name
</Typography>
<Typography
level="body-xs"
sx={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
user@test.com
</Typography>
</Box>
<IconButton size="sm" variant="plain" color="neutral">
<LogOutIcon size="18px" />
</IconButton>

{(userInfo && userInfo.id) ? (
<UserDetailsPanel
userDetails={userInfo}
mutate={userMutate}
/>
) : (
<List
sx={{
'--ListItem-radius': '6px',
'--ListItem-minHeight': '32px',
overflowY: 'auto',
flex: 1,
}}
>
<ListItem className="FirstSidebar_Content">
<ListItemButton
variant='plain'
onClick={() => setUserLoginModalOpen(true)}
>
<ListItemDecorator sx={{ minInlineSize: '30px' }}>
<LogInIcon />
</ListItemDecorator>
<ListItemContent
sx={{
display: 'flex',
justifyContent: 'flex-start',
alignContent: 'center',
}}
>
<Typography level="body-sm">Login</Typography>
</ListItemContent>
</ListItemButton>
</ListItem>
</List>
)}
</Box>
<ButtonGroup
sx={{
Expand Down Expand Up @@ -300,6 +372,13 @@ src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fi
</IconButton>
</Tooltip>
</ButtonGroup>
<UserLoginModal
open={userLoginModalOpen}
onClose={() => {
setUserLoginModalOpen(false);
userMutate();
}}
/>
</>
);
}
Expand Down
184 changes: 184 additions & 0 deletions src/renderer/components/User/UserLoginModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { useState } from 'react';

import {
Modal,
ModalDialog,
Stack,
Typography,
Tab,
TabList,
TabPanel,
Tabs,
FormControl,
FormLabel,
Input,
Button,
Alert,
} from '@mui/joy';

import {
login,
registerUser
} from 'renderer/lib/transformerlab-api-sdk';

export default function UserLoginModal({ open, onClose }) {
const [loginErrorMessage, setLoginErrorMessage] = useState(null);

const commonTabPanelSx = {
p: 1,
pt: 4,
width: '100%',
maxWidth: '400px',
mx: 'auto'
};

const descriptionAlertSx = {
display: 'flex',
justifyContent: 'center',
mb: 2
};

return (
<Modal
open={open}
onClose={() => {
setLoginErrorMessage(null);
onClose();
}}
>
<ModalDialog
sx={{
top: '5vh',
margin: 'auto',
transform: 'translateX(-50%)',
width: '80vw',
maxWidth: '600px',
height: '90vh',
}}
>
<Tabs defaultValue="login">
<TabList tabFlex={1}>
<Tab value="login">Login</Tab>
<Tab value="register">Register New User</Tab>
</TabList>

<TabPanel value="login" sx={commonTabPanelSx}>
<Alert variant="plain" sx={descriptionAlertSx}>
<Typography level="body-sm" textColor="text.tertiary">
{loginErrorMessage ?
loginErrorMessage :
"Login with your existing account credentials."
}
</Typography>
</Alert>
<form
onSubmit={async (event) => {
event.preventDefault();

// Read login data from the form and submit
const formData = new FormData(event.currentTarget);
const username = formData.get('email') as string;
const password = formData.get('password') as string;
const result = await login(username, password);

// Check if login was successful. If not, stay on screen
if (result?.status == "success") {
console.log(`Login attempt successful for user ${username}`);
setLoginErrorMessage(null);
onClose();
} else if (result?.status == "error") {
setLoginErrorMessage(result?.message);
} else { // unauthorized - reset fields
(event.target as HTMLFormElement).reset();
setLoginErrorMessage(result?.message);
}
}}
>
<Stack spacing={2}>
<FormControl>
<FormLabel>Email</FormLabel>
<Input
name="email"
type="email"
autoFocus
required
placeholder="user@example.com"
/>
</FormControl>
<FormControl>
<FormLabel>Password</FormLabel>
<Input
name="password"
type="password"
required
placeholder="Enter your password"
/>
</FormControl>
<Button type="submit">Login</Button>
</Stack>
</form>
</TabPanel>

<TabPanel value="register" sx={commonTabPanelSx}>
<Alert variant="plain" sx={descriptionAlertSx}>
<Typography level="body-sm" textColor="text.tertiary">
Create a new account to get started.
</Typography>
</Alert>
<form
onSubmit={async (event) => {
event.preventDefault();

// Read user data from the form and submit
const formData = new FormData(event.currentTarget);
const name = formData.get('name') as string;
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const result = await registerUser(name, email, password);

// Check if login was successful. If not, stay on screen
if (result?.status == "success") {
alert(`Registered new user ${email}`);
onClose();
} else {
alert(result?.message);
}
}}
>
<Stack spacing={2}>
<FormControl>
<FormLabel>Name</FormLabel>
<Input
name="name"
autoFocus
required
placeholder="Enter your name"
/>
</FormControl>
<FormControl>
<FormLabel>Email</FormLabel>
<Input
name="email"
type="email"
required
placeholder="user@example.com"
/>
</FormControl>
<FormControl>
<FormLabel>Password</FormLabel>
<Input
name="password"
type="password"
required
placeholder="Create a password"
/>
</FormControl>
<Button type="submit">Register</Button>
</Stack>
</form>
</TabPanel>
</Tabs>
</ModalDialog>
</Modal>
);
}
Loading