forked from github/plane
fix: auth redirection issue fixes when user is logged in (#2499)
* fix: auth redirection issues * fix: redirect flickering fix * chore: sign in page ui improvement and redirection fix (#2501) * style: sign in page ui improvement * chore: sign up redirection added and ui improvement * chore: redirection validation and create workspace form fix (#2504) * chore: sign in redirection validation * fix: create workspace form input fix * chore: code refactor --------- Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
This commit is contained in:
parent
d78b4dccf3
commit
9f1fd2327a
@ -14,7 +14,7 @@ export const LayersIcon: React.FC<ISvgIcons> = ({
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<g clip-path="url(#clip0_7258_81938)">
|
<g clipPath="url(#clip0_7258_81938)">
|
||||||
<path
|
<path
|
||||||
d="M16.5953 6.69606L16.6072 5.17376L6.85812 8.92381L6.85812 19.4238L9.00319 18.6961"
|
d="M16.5953 6.69606L16.6072 5.17376L6.85812 8.92381L6.85812 19.4238L9.00319 18.6961"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
|
@ -203,7 +203,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
size="md"
|
size="xl"
|
||||||
onClick={handleSubmit(handleSignin)}
|
onClick={handleSubmit(handleSignin)}
|
||||||
disabled={!isValid && isDirty}
|
disabled={!isValid && isDirty}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
|
@ -51,7 +51,7 @@ export const EmailForgotPasswordForm: FC<IEmailForgotPasswordForm> = (props) =>
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
hasError={Boolean(errors.email)}
|
hasError={Boolean(errors.email)}
|
||||||
placeholder="Enter registered email address.."
|
placeholder="Enter registered email address.."
|
||||||
className="border-custom-border-300 h-[46px]"
|
className="border-custom-border-300 h-[46px] w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -56,7 +56,7 @@ export const EmailPasswordForm: React.FC<IEmailPasswordForm> = (props) => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
hasError={Boolean(errors.email)}
|
hasError={Boolean(errors.email)}
|
||||||
placeholder="Enter your email address..."
|
placeholder="Enter your email address..."
|
||||||
className="border-custom-border-300 h-[46px]"
|
className="border-custom-border-300 h-[46px] w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -77,7 +77,7 @@ export const EmailPasswordForm: React.FC<IEmailPasswordForm> = (props) => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
hasError={Boolean(errors.password)}
|
hasError={Boolean(errors.password)}
|
||||||
placeholder="Enter your password..."
|
placeholder="Enter your password..."
|
||||||
className="border-custom-border-300 h-[46px]"
|
className="border-custom-border-300 h-[46px] w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -93,8 +93,10 @@ export const EmailPasswordForm: React.FC<IEmailPasswordForm> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full text-center h-[46px]"
|
className="w-full"
|
||||||
|
size="xl"
|
||||||
disabled={!isValid && isDirty}
|
disabled={!isValid && isDirty}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
|
@ -122,6 +122,7 @@ export const EmailSignUpForm: React.FC<Props> = (props) => {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
size="xl"
|
||||||
disabled={!isValid && isDirty}
|
disabled={!isValid && isDirty}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
@ -20,14 +21,15 @@ import {
|
|||||||
import { Loader, Spinner } from "@plane/ui";
|
import { Loader, Spinner } from "@plane/ui";
|
||||||
// images
|
// images
|
||||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||||
|
// types
|
||||||
import { IUserSettings } from "types";
|
import { IUserSettings } from "types";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const appConfigService = new AppConfigService();
|
const appConfigService = new AppConfigService();
|
||||||
const authService = new AuthService();
|
const authService = new AuthService();
|
||||||
|
|
||||||
export const SignInView = observer(() => {
|
export const SignInView = observer(() => {
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
|
const { fetchCurrentUserSettings } = userStore;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// states
|
// states
|
||||||
@ -35,25 +37,46 @@ export const SignInView = observer(() => {
|
|||||||
// toast
|
// toast
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// fetch app config
|
// fetch app config
|
||||||
const { data, error } = useSWR("APP_CONFIG", () => appConfigService.envConfig());
|
const { data, error: appConfigError } = useSWR("APP_CONFIG", () => appConfigService.envConfig());
|
||||||
// fetch user info
|
|
||||||
useSWR("USER_INFO", () => userStore.fetchCurrentUser());
|
|
||||||
// computed
|
// computed
|
||||||
const enableEmailPassword =
|
const enableEmailPassword =
|
||||||
data &&
|
data &&
|
||||||
(data?.email_password_login || !(data?.email_password_login || data?.magic_login || data?.google || data?.github));
|
(data?.email_password_login || !(data?.email_password_login || data?.magic_login || data?.google || data?.github));
|
||||||
|
|
||||||
const handleLoginRedirection = () =>
|
useEffect(() => {
|
||||||
userStore
|
fetchCurrentUserSettings().then((settings) => {
|
||||||
.fetchCurrentUserSettings()
|
setLoading(true);
|
||||||
.then((userSettings: IUserSettings) => {
|
router.push(
|
||||||
const workspaceSlug =
|
`/${
|
||||||
userSettings?.workspace?.last_workspace_slug || userSettings?.workspace?.fallback_workspace_slug;
|
settings.workspace.last_workspace_slug
|
||||||
router.push(`/${workspaceSlug}`);
|
? settings.workspace.last_workspace_slug
|
||||||
})
|
: settings.workspace.fallback_workspace_slug
|
||||||
.catch(() => {
|
}`
|
||||||
setLoading(false);
|
);
|
||||||
});
|
});
|
||||||
|
}, [fetchCurrentUserSettings, router]);
|
||||||
|
|
||||||
|
const handleLoginRedirection = () => {
|
||||||
|
userStore.fetchCurrentUser().then((user) => {
|
||||||
|
const isOnboard = user.onboarding_step.profile_complete;
|
||||||
|
if (isOnboard) {
|
||||||
|
userStore
|
||||||
|
.fetchCurrentUserSettings()
|
||||||
|
.then((userSettings: IUserSettings) => {
|
||||||
|
const workspaceSlug =
|
||||||
|
userSettings?.workspace?.last_workspace_slug || userSettings?.workspace?.fallback_workspace_slug;
|
||||||
|
if (workspaceSlug) router.push(`/${workspaceSlug}`);
|
||||||
|
else if (userSettings.workspace.invites > 0) router.push("/invitations");
|
||||||
|
else router.push("/create-workspace");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
router.push("/onboarding");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
||||||
try {
|
try {
|
||||||
@ -114,7 +137,13 @@ export const SignInView = observer(() => {
|
|||||||
return authService
|
return authService
|
||||||
.emailLogin(formData)
|
.emailLogin(formData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
handleLoginRedirection();
|
userStore.fetchCurrentUser().then((user) => {
|
||||||
|
const isOnboard = user.onboarding_step.profile_complete;
|
||||||
|
if (isOnboard) handleLoginRedirection();
|
||||||
|
else {
|
||||||
|
router.push("/onboarding");
|
||||||
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -166,7 +195,7 @@ export const SignInView = observer(() => {
|
|||||||
Sign in to Plane
|
Sign in to Plane
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{!data && !error ? (
|
{!data && !appConfigError ? (
|
||||||
<div className="pt-10 w-ful">
|
<div className="pt-10 w-ful">
|
||||||
<Loader className="space-y-4 w-full pb-4">
|
<Loader className="space-y-4 w-full pb-4">
|
||||||
<Loader.Item height="46px" width="360px" />
|
<Loader.Item height="46px" width="360px" />
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, Fragment, FC, ChangeEvent } from "react";
|
import { useState, useEffect, Fragment, FC, ChangeEvent } from "react";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
// icons
|
// icons
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
@ -8,9 +9,9 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import { useWorkspaceMyMembership } from "contexts/workspace-member.context";
|
import { useWorkspaceMyMembership } from "contexts/workspace-member.context";
|
||||||
// ui
|
// ui
|
||||||
import { Button, CustomSelect, CustomSearchSelect, Input, TextArea } from "@plane/ui";
|
import { Button, CustomSelect, Input, TextArea } from "@plane/ui";
|
||||||
import { Avatar } from "components/ui";
|
|
||||||
// components
|
// components
|
||||||
|
import { WorkspaceMemberSelect } from "components/workspace";
|
||||||
import { ImagePickerPopover } from "components/core";
|
import { ImagePickerPopover } from "components/core";
|
||||||
import EmojiIconPicker from "components/emoji-icon-picker";
|
import EmojiIconPicker from "components/emoji-icon-picker";
|
||||||
// helpers
|
// helpers
|
||||||
@ -19,8 +20,6 @@ import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
|
|||||||
import { IWorkspaceMember } from "types";
|
import { IWorkspaceMember } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project";
|
import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project";
|
||||||
import { WorkspaceMemberSelect } from "components/workspace";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
@ -136,13 +136,17 @@ export const CreateWorkspaceForm: FC<Props> = ({
|
|||||||
message: "Workspace name should not exceed 80 characters",
|
message: "Workspace name should not exceed 80 characters",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
render={({ field: { value, ref } }) => (
|
render={({ field: { value, ref, onChange } }) => (
|
||||||
<Input
|
<Input
|
||||||
id="workspaceName"
|
id="workspaceName"
|
||||||
name="name"
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"))}
|
onChange={(e) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
setValue("name", e.target.value);
|
||||||
|
setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"));
|
||||||
|
}}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.name)}
|
hasError={Boolean(errors.name)}
|
||||||
placeholder="Enter workspace name..."
|
placeholder="Enter workspace name..."
|
||||||
@ -166,7 +170,7 @@ export const CreateWorkspaceForm: FC<Props> = ({
|
|||||||
id="workspaceUrl"
|
id="workspaceUrl"
|
||||||
name="slug"
|
name="slug"
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value.toLocaleLowerCase().trim().replace(/ /g, "-")}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
/^[a-zA-Z0-9_-]+$/.test(e.target.value) ? setInvalidSlug(false) : setInvalidSlug(true)
|
/^[a-zA-Z0-9_-]+$/.test(e.target.value) ? setInvalidSlug(false) : setInvalidSlug(true)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import useUser from "hooks/use-user";
|
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// hooks
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -8,16 +10,14 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Crisp = () => {
|
const Crisp = observer(() => {
|
||||||
const { user } = useUser();
|
const { user: userStore } = useMobxStore();
|
||||||
|
const { currentUser } = userStore;
|
||||||
|
|
||||||
const validateCurrentUser = useCallback(() => {
|
const validateCurrentUser = useCallback(() => {
|
||||||
const currentUser = user ? user : null;
|
if (currentUser) return currentUser.email;
|
||||||
|
|
||||||
if (currentUser && currentUser.email) return currentUser.email;
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, [user]);
|
}, [currentUser]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window && validateCurrentUser()) {
|
if (typeof window && validateCurrentUser()) {
|
||||||
@ -40,5 +40,5 @@ const Crisp = () => {
|
|||||||
}, [validateCurrentUser]);
|
}, [validateCurrentUser]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
});
|
||||||
export default Crisp;
|
export default Crisp;
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
|
import { FC, ReactNode } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: ReactNode;
|
||||||
gradient?: boolean;
|
gradient?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DefaultLayout: React.FC<Props> = ({ children, gradient = false }) => (
|
const DefaultLayout: FC<Props> = ({ children, gradient = false }) => (
|
||||||
<div className={`h-screen w-full overflow-hidden ${gradient ? "" : "bg-custom-background-100"}`}>
|
<div className={`h-screen w-full overflow-hidden ${gradient ? "" : "bg-custom-background-100"}`}>{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default DefaultLayout;
|
export default DefaultLayout;
|
||||||
|
@ -8,6 +8,10 @@ import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
const MobxStoreInit = observer(() => {
|
const MobxStoreInit = observer(() => {
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, cycleId, moduleId, globalViewId, viewId, inboxId } = router.query;
|
||||||
|
// store
|
||||||
const {
|
const {
|
||||||
theme: themeStore,
|
theme: themeStore,
|
||||||
user: userStore,
|
user: userStore,
|
||||||
@ -23,14 +27,11 @@ const MobxStoreInit = observer(() => {
|
|||||||
const [dom, setDom] = useState<any>();
|
const [dom, setDom] = useState<any>();
|
||||||
// theme
|
// theme
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, globalViewId, viewId, inboxId } = router.query;
|
|
||||||
|
|
||||||
// const dom = useMemo(() => window && window.document?.querySelector<HTMLElement>("[data-theme='custom']"), [document]);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sidebar collapsed fetching from local storage
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// sidebar collapsed toggle
|
|
||||||
const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed");
|
const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed");
|
||||||
const localBoolValue = localValue ? (localValue === "true" ? true : false) : false;
|
const localBoolValue = localValue ? (localValue === "true" ? true : false) : false;
|
||||||
if (localValue && themeStore?.sidebarCollapsed === undefined) {
|
if (localValue && themeStore?.sidebarCollapsed === undefined) {
|
||||||
@ -38,6 +39,9 @@ const MobxStoreInit = observer(() => {
|
|||||||
}
|
}
|
||||||
}, [themeStore, userStore, setTheme]);
|
}, [themeStore, userStore, setTheme]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setting up the theme of the user by fetching it from local storage
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userStore.currentUser) return;
|
if (!userStore.currentUser) return;
|
||||||
if (window) {
|
if (window) {
|
||||||
@ -45,11 +49,13 @@ const MobxStoreInit = observer(() => {
|
|||||||
}
|
}
|
||||||
setTheme(userStore.currentUser?.theme?.theme || "system");
|
setTheme(userStore.currentUser?.theme?.theme || "system");
|
||||||
if (userStore.currentUser?.theme?.theme === "custom" && dom) {
|
if (userStore.currentUser?.theme?.theme === "custom" && dom) {
|
||||||
console.log("userStore.currentUser?.theme?.theme", userStore.currentUser?.theme);
|
|
||||||
applyTheme(userStore.currentUser?.theme?.palette, false);
|
applyTheme(userStore.currentUser?.theme?.palette, false);
|
||||||
} else unsetCustomCssVariables();
|
} else unsetCustomCssVariables();
|
||||||
}, [userStore.currentUser, setTheme, dom]);
|
}, [userStore.currentUser, setTheme, dom]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setting router info to the respective stores.
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workspaceSlug) workspaceStore.setWorkspaceSlug(workspaceSlug.toString());
|
if (workspaceSlug) workspaceStore.setWorkspaceSlug(workspaceSlug.toString());
|
||||||
if (projectId) projectStore.setProjectId(projectId.toString());
|
if (projectId) projectStore.setProjectId(projectId.toString());
|
||||||
|
@ -50,7 +50,7 @@ const SignUp: NextPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response) await mutateUser();
|
if (response) await mutateUser();
|
||||||
router.push("/");
|
router.push("/onboarding");
|
||||||
})
|
})
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
|
@ -11,6 +11,7 @@ import { IWorkspaceMember, IProjectMember } from "types";
|
|||||||
export interface IUserStore {
|
export interface IUserStore {
|
||||||
loader: boolean;
|
loader: boolean;
|
||||||
|
|
||||||
|
isUserLoggedIn: boolean | null;
|
||||||
currentUser: IUser | null;
|
currentUser: IUser | null;
|
||||||
currentUserSettings: IUserSettings | null;
|
currentUserSettings: IUserSettings | null;
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ export interface IUserStore {
|
|||||||
class UserStore implements IUserStore {
|
class UserStore implements IUserStore {
|
||||||
loader: boolean = false;
|
loader: boolean = false;
|
||||||
|
|
||||||
|
isUserLoggedIn: boolean | null = null;
|
||||||
currentUser: IUser | null = null;
|
currentUser: IUser | null = null;
|
||||||
currentUserSettings: IUserSettings | null = null;
|
currentUserSettings: IUserSettings | null = null;
|
||||||
|
|
||||||
@ -85,10 +87,14 @@ class UserStore implements IUserStore {
|
|||||||
if (response) {
|
if (response) {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.currentUser = response;
|
this.currentUser = response;
|
||||||
|
this.isUserLoggedIn = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.isUserLoggedIn = false;
|
||||||
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user