Skip to content
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

Feat(auth): Handle resetPassword and Invite user #43

Merged
merged 7 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,9 @@ Create a new migration using the following command:
npx supabase db diff | npx supabase migration new <migration_name>
```

### Password Reset
### Password Reset And Invitations

If users forget their password, they can request for a reset. Atomic CRM handles it for you, using Supabase.
An user can be invited to the CRM by an administrator. The user will receive an email with a link to set their password. The password reset feature is also available. You don't have to worry about these processes, Atomic CRM handles them for you, using Supabase.

Please make sure to read and apply the [Login Callback](./doc/supabase-configuration.md#login-callback) and [Customizing Mail Template](./doc/supabase-configuration.md#customizing-mail-template) sections to properly configure the password reset feature.

Expand Down
34 changes: 17 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"papaparse": "^5.4.1",
"prop-types": "^15.8.1",
"ra-data-fakerest": "^5.1.1",
"ra-supabase": "^3.0.0",
"ra-supabase": "^3.1.0",
"react": "^18.2.0",
"react-admin": "^5.1.0",
"react-cropper": "^2.3.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
function interceptAuthCallback() {
const hash = window.location.hash;
const {access_token, refresh_token, type} = getUrlParams();
window.location.href = `../?access_token=${access_token}&refresh_token=${refresh_token}&type=${type}#/auth-callback`;
window.location.href = `../#/auth-callback?access_token=${access_token}&refresh_token=${refresh_token}&type=${type}`;
}

// Call the function to intercept the auth callback
Expand Down
10 changes: 5 additions & 5 deletions src/providers/fakerest/dataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,12 @@ const dataProviderWithCustomMethod: CrmDataProvider = {
password,
};
},
async salesCreate({
password: _password,
...data
}: SalesFormData): Promise<Sale> {
async salesCreate({ ...data }: SalesFormData): Promise<Sale> {
const user = await dataProvider.create('sales', {
data,
data: {
...data,
password: 'new_password',
},
});

return {
Expand Down
27 changes: 0 additions & 27 deletions src/providers/supabase/authProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,35 +68,8 @@ export const authProvider: AuthProvider = {

return baseAuthProvider.checkAuth(params);
},
// We need to ovveride the getPermissions here to avoid the `ra-supabase` retries
async getPermissions(params) {
const isInitialized = await getIsInitialized();
return isInitialized ? baseAuthProvider.getPermissions(params) : null;
},
handleCallback: async () => {
const { access_token, refresh_token, type } = getUrlParams();
// Users have reset their password or have just been invited and must set a new password
if (type === 'recovery' || type === 'invite') {
if (access_token && refresh_token) {
return {
redirectTo: `/set-password?access_token=${access_token}&refresh_token=${refresh_token}&type=${type}`,
};
}

if (process.env.NODE_ENV === 'development') {
console.error(
'Missing access_token or refresh_token for an invite or recovery'
);
}
}
},
};

const getUrlParams = () => {
const urlSearchParams = new URLSearchParams(window.location.search);
const access_token = urlSearchParams.get('access_token');
const refresh_token = urlSearchParams.get('refresh_token');
const type = urlSearchParams.get('type');

return { access_token, refresh_token, type };
};
10 changes: 1 addition & 9 deletions src/sales/SalesCreate.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Card, Container, Typography } from '@mui/material';
import { useMutation } from '@tanstack/react-query';
import {
PasswordInput,
required,
SimpleForm,
useDataProvider,
useNotify,
Expand Down Expand Up @@ -50,13 +48,7 @@ export function SalesCreate() {
<Card>
<SimpleForm onSubmit={onSubmit as SubmitHandler<any>}>
<Typography variant="h6">Create sale person</Typography>
<SalesInputs>
<PasswordInput
source="password"
validate={required()}
helperText={false}
/>
jonathan-marmelab marked this conversation as resolved.
Show resolved Hide resolved
</SalesInputs>
<SalesInputs />
</SimpleForm>
</Card>
</Container>
Expand Down
4 changes: 1 addition & 3 deletions src/sales/SalesInputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import {
useRecordContext,
} from 'react-admin';
import { Sale } from '../types';
import { ReactNode } from 'react';
import { Stack } from '@mui/material';

export function SalesInputs({ children }: { children?: ReactNode }) {
export function SalesInputs() {
const { identity } = useGetIdentity();
const record = useRecordContext<Sale>();
return (
Expand All @@ -29,7 +28,6 @@ export function SalesInputs({ children }: { children?: ReactNode }) {
validate={required()}
helperText={false}
/>
{children}
<BooleanInput
source="administrator"
readOnly={record?.id === identity?.id}
Expand Down
9 changes: 4 additions & 5 deletions supabase/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ enabled = true
# in emails.
site_url = "http://localhost:5173/"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://localhost:5173/auth-callback/index.html"]
additional_redirect_urls = ["https://localhost:5173/auth-callback.html"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't you need to update the documentation following this change?

# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# If disabled, the refresh token will never expire.
Expand All @@ -91,10 +91,9 @@ double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in.
enable_confirmations = false

# Uncomment to customize email template
# [auth.email.template.invite]
# subject = "You have been invited"
# content_path = "./supabase/templates/invite.html"
[auth.email.template.invite]
subject = "You've been invited to use Atomic CRM"
content_path = "./supabase/templates/invite.html"

[auth.email.template.recovery]
subject = "Reset Password"
Expand Down
6 changes: 4 additions & 2 deletions supabase/functions/users/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ async function inviteUser(req: Request) {
await supabaseAdmin.auth.admin.createUser({
email,
password,
email_confirm: true,
user_metadata: { first_name, last_name },
});

if (!data?.user || userError) {
const { error: emailError } =
await supabaseAdmin.auth.admin.inviteUserByEmail(email);

if (!data?.user || userError || emailError) {
console.error('Error inviting user:', userError);
return createErrorResponse(500, 'Internal Server Error');
}
Expand Down
90 changes: 90 additions & 0 deletions supabase/templates/invite.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!doctype html>
<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
padding: 10px 0;
}
.header h2 {
margin: 0;
color: #333333;
}
.content {
padding: 20px;
line-height: 1.6;
color: #555555;
}
.content p {
margin: 0 0 10px;
}
.button {
display: block;
width: 200px;
margin: 20px auto;
padding: 10px;
text-align: center;
background-color: #007bff;
color: #ffffff;
text-decoration: none;
border-radius: 5px;
}
.footer {
text-align: center;
padding: 10px 0;
font-size: 12px;
color: #777777;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>Confirm your account</h2>
</div>
<div class="content">
<p>Welcome to Atomic CRM.</p>
<p>
You've been invited to join the (Name) organization. Confirm
your address below:
Comment on lines +63 to +64
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This too looks like something that we should document (replacing the organization name in the template)

</p>
<a
href="{{ .ConfirmationURL }}auth-callback.html"
class="button"
>Confirm my account</a
>
<p>
<strong>Warning:</strong> If the password reset request did
not originate from you, please contact us immediately as
this may be a fraudulent attempt.
</p>
<p>See you soon!</p>
<p>The Atomic CRM team</p>
</div>
<div class="footer">
<p>
If the activation link doesn’t work,
<a
href="mailto:support@react-admin-enterprise-edition.zendesk.com"
>contact us</a
>.
</p>
</div>
</div>
</body>
</html>
2 changes: 1 addition & 1 deletion supabase/templates/recovery.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ <h2>Reset Password</h2>
<p>Hello,</p>
<p>To reset your password, follow the link below:</p>
<a
href="{{ .ConfirmationURL }}auth-callback/index.html"
href="{{ .ConfirmationURL }}auth-callback.html"
class="button"
>Reset my password</a
>
Expand Down
Loading