feat(auth): add OAuth2 login (#276)

* feat(auth): add OAuth2 login with GitHub and Google

* chore(translations): add files for Japanese

* fix(auth): fix link function for GitHub

* feat(oauth): basic oidc implementation

* feat(oauth): oauth guard

* fix: disable image optimizations for logo to prevent caching issues with custom logos

* fix: memory leak while downloading large files

* chore(translations): update translations via Crowdin (#278)

* New translations en-us.ts (Japanese)

* New translations en-us.ts (Japanese)

* New translations en-us.ts (Japanese)

* release: 0.18.2

* doc(translations): Add Japanese README (#279)

* Added Japanese README.

* Added JAPANESE README link to README.md.

* Updated Japanese README.

* Updated Environment Variable Table.

* updated zh-cn README.

* feat(oauth): unlink account

* refactor(oauth): make providers extensible

* fix(oauth): fix discoveryUri error when toggle google-enabled

* feat(oauth): add microsoft and discord as oauth provider

* docs(oauth): update README.md

* docs(oauth): update oauth2-guide.md

* set password to null for new oauth users

* New translations en-us.ts (Japanese) (#281)

* chore(translations): add Polish files

* fix(oauth): fix random username and password

* feat(oauth): add totp

* fix(oauth): fix totp throttle

* fix(oauth): fix qrcode and remove comment

* feat(oauth): add error page

* fix(oauth): i18n of error page

* feat(auth): add OAuth2 login

* fix(auth): fix link function for GitHub

* feat(oauth): basic oidc implementation

* feat(oauth): oauth guard

* feat(oauth): unlink account

* refactor(oauth): make providers extensible

* fix(oauth): fix discoveryUri error when toggle google-enabled

* feat(oauth): add microsoft and discord as oauth provider

* docs(oauth): update README.md

* docs(oauth): update oauth2-guide.md

* set password to null for new oauth users

* fix(oauth): fix random username and password

* feat(oauth): add totp

* fix(oauth): fix totp throttle

* fix(oauth): fix qrcode and remove comment

* feat(oauth): add error page

* fix(oauth): i18n of error page

* refactor: return null instead of `false` in `getIdOfCurrentUser` functiom

* feat: show original oauth error if available

* refactor: run formatter

* refactor(oauth): error message i18n

* refactor(oauth): make OAuth token available
someone may use it (to revoke token or get other info etc.)
also improved the i18n message

* chore(oauth): remove unused import

* chore: add database migration

* fix: missing python installation for nanoid

---------

Co-authored-by: Elias Schneider <login@eliasschneider.com>
Co-authored-by: ふうせん <10260662+fusengum@users.noreply.github.com>
This commit is contained in:
Qing Fu
2023-10-22 22:09:53 +08:00
committed by GitHub
parent d327bc355c
commit 02cd98fa9c
52 changed files with 1983 additions and 161 deletions

View File

@@ -2,9 +2,11 @@ import {
Anchor,
Button,
Container,
createStyles,
Group,
Paper,
PasswordInput,
Stack,
Text,
TextInput,
Title,
@@ -18,19 +20,47 @@ import { TbInfoCircle } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";
import useConfig from "../../hooks/config.hook";
import useTranslate from "../../hooks/useTranslate.hook";
import useUser from "../../hooks/user.hook";
import useTranslate from "../../hooks/useTranslate.hook";
import authService from "../../services/auth.service";
import { getOAuthIcon, getOAuthUrl } from "../../utils/oauth.util";
import toast from "../../utils/toast.util";
const useStyles = createStyles((theme) => ({
or: {
"&:before": {
content: "''",
flex: 1,
display: "block",
borderTopWidth: 1,
borderTopStyle: "solid",
borderColor:
theme.colorScheme === "dark"
? theme.colors.dark[3]
: theme.colors.gray[4],
},
"&:after": {
content: "''",
flex: 1,
display: "block",
borderTopWidth: 1,
borderTopStyle: "solid",
borderColor:
theme.colorScheme === "dark"
? theme.colors.dark[3]
: theme.colors.gray[4],
},
},
}));
const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
const config = useConfig();
const router = useRouter();
const t = useTranslate();
const { refreshUser } = useUser();
const { classes } = useStyles();
const [showTotp, setShowTotp] = React.useState(false);
const [loginToken, setLoginToken] = React.useState("");
const [oauth, setOAuth] = React.useState<string[]>([]);
const validationSchema = yup.object().shape({
emailOrUsername: yup.string().required(t("common.error.field-required")),
@@ -44,7 +74,6 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
initialValues: {
emailOrUsername: "",
password: "",
totp: "",
},
validate: yupResolver(validationSchema),
});
@@ -55,7 +84,6 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
.then(async (response) => {
if (response.data["loginToken"]) {
// Prompt the user to enter their totp code
setShowTotp(true);
showNotification({
icon: <TbInfoCircle />,
color: "blue",
@@ -63,7 +91,11 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
title: t("signIn.notify.totp-required.title"),
message: t("signIn.notify.totp-required.description"),
});
setLoginToken(response.data["loginToken"]);
router.push(
`/auth/totp/${
response.data["loginToken"]
}?redirect=${encodeURIComponent(redirectPath)}`,
);
} else {
await refreshUser();
router.replace(redirectPath);
@@ -72,25 +104,15 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
.catch(toast.axiosError);
};
const signInTotp = (email: string, password: string, totp: string) => {
authService
.signInTotp(email, password, totp, loginToken)
.then(async () => {
await refreshUser();
router.replace(redirectPath);
})
.catch((error) => {
if (error?.response?.data?.error == "share_password_required") {
toast.axiosError(error);
// Refresh the page to start over
window.location.reload();
}
toast.axiosError(error);
form.setValues({ totp: "" });
});
const getAvailableOAuth = async () => {
const oauth = await authService.getAvailableOAuth();
setOAuth(oauth.data);
};
React.useEffect(() => {
getAvailableOAuth().catch(toast.axiosError);
}, []);
return (
<Container size={420} my={40}>
<Title order={2} align="center" weight={900}>
@@ -107,9 +129,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
<form
onSubmit={form.onSubmit((values) => {
if (showTotp)
signInTotp(values.emailOrUsername, values.password, values.totp);
else signIn(values.emailOrUsername, values.password);
signIn(values.emailOrUsername, values.password);
})}
>
<TextInput
@@ -123,15 +143,6 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
mt="md"
{...form.getInputProps("password")}
/>
{showTotp && (
<TextInput
variant="filled"
label={t("account.modal.totp.code")}
placeholder="******"
mt="md"
{...form.getInputProps("totp")}
/>
)}
{config.get("smtp.enabled") && (
<Group position="right" mt="xs">
<Anchor component={Link} href="/auth/resetPassword" size="xs">
@@ -143,6 +154,27 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
<FormattedMessage id="signin.button.submit" />
</Button>
</form>
{oauth.length > 0 && (
<Stack mt="xl">
<Group align="center" className={classes.or}>
<Text>{t("signIn.oauth.or")}</Text>
</Group>
<Group position="center">
{oauth.map((provider) => (
<Button
key={provider}
component="a"
target="_blank"
title={t(`signIn.oauth.${provider}`)}
href={getOAuthUrl(config.get("general.appUrl"), provider)}
variant="light"
>
{getOAuthIcon(provider)}
</Button>
))}
</Group>
</Stack>
)}
</Paper>
</Container>
);