feat: ability to link existing accounts to oauth
This commit is contained in:
parent
0847802ce4
commit
561849ae5b
9 changed files with 171 additions and 19 deletions
|
@ -22,12 +22,15 @@ import {
|
|||
CheckIcon,
|
||||
CrossIcon,
|
||||
DeleteIcon,
|
||||
DiscordIcon,
|
||||
FlameshotIcon,
|
||||
GitHubIcon,
|
||||
RefreshIcon,
|
||||
SettingsIcon,
|
||||
ShareXIcon,
|
||||
} from 'components/icons';
|
||||
import DownloadIcon from 'components/icons/DownloadIcon';
|
||||
import TrashIcon from 'components/icons/TrashIcon';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
import { SmallTable } from 'components/SmallTable';
|
||||
|
@ -51,7 +54,17 @@ function ExportDataTooltip({ children }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default function Manage() {
|
||||
export default function Manage({ oauth_registration, oauth_providers: raw_oauth_providers }) {
|
||||
const oauth_providers = JSON.parse(raw_oauth_providers);
|
||||
const icons = {
|
||||
GitHub: GitHubIcon,
|
||||
Discord: DiscordIcon,
|
||||
};
|
||||
|
||||
for (const provider of oauth_providers) {
|
||||
provider.Icon = icons[provider.name];
|
||||
}
|
||||
|
||||
const [user, setUser] = useRecoilState(userSelector);
|
||||
const modals = useModals();
|
||||
|
||||
|
@ -290,6 +303,26 @@ export default function Manage() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleOauthUnlink = async () => {
|
||||
const res = await useFetch('/api/auth/oauth', 'DELETE');
|
||||
if (res.error) {
|
||||
showNotification({
|
||||
title: 'Error while unlinking from OAuth',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
setUser(res);
|
||||
showNotification({
|
||||
title: 'Unlinked from OAuth',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <CheckIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const interval = useInterval(() => getExports(), 30000);
|
||||
useEffect(() => {
|
||||
getExports();
|
||||
|
@ -334,7 +367,31 @@ export default function Manage() {
|
|||
</Group>
|
||||
</form>
|
||||
|
||||
<Box mb='md'>
|
||||
{oauth_registration && (
|
||||
<Box my='md'>
|
||||
<Title>OAuth</Title>
|
||||
<MutedText size='md'>Link your account with an OAuth provider.</MutedText>
|
||||
|
||||
<Group>
|
||||
{oauth_providers
|
||||
.filter((x) => x.name.toLowerCase() !== user.oauthProvider)
|
||||
.map(({ link_url, name, Icon }, i) => (
|
||||
<Link key={i} href={link_url} passHref legacyBehavior>
|
||||
<Button size='lg' leftIcon={<Icon />} component='a' my='sm'>
|
||||
Link account with {name}
|
||||
</Button>
|
||||
</Link>
|
||||
))}
|
||||
{user.oauth && user.oauthProvider && (
|
||||
<Button onClick={handleOauthUnlink} size='lg' leftIcon={<TrashIcon />} my='sm' color='red'>
|
||||
Unlink account with {user.oauthProvider[0].toUpperCase() + user.oauthProvider.slice(1)}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box my='md'>
|
||||
<Title>Avatar</Title>
|
||||
<FileInput
|
||||
placeholder='Click to upload a file'
|
||||
|
|
|
@ -13,11 +13,13 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||
oauth_providers.push({
|
||||
name: 'GitHub',
|
||||
url: '/api/auth/oauth/github',
|
||||
link_url: '/api/auth/oauth/github?state=link',
|
||||
});
|
||||
if (discEnabled)
|
||||
oauth_providers.push({
|
||||
name: 'Discord',
|
||||
url: '/api/auth/oauth/discord',
|
||||
link_url: '/api/auth/oauth/discord?state=link',
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
export const github_auth = {
|
||||
oauth_url: (clientId: string) =>
|
||||
`https://github.com/login/oauth/authorize?client_id=${clientId}&scope=user`,
|
||||
oauth_url: (clientId: string, state?: string) =>
|
||||
`https://github.com/login/oauth/authorize?client_id=${clientId}&scope=read:user${
|
||||
state ? `&state=${state}` : ''
|
||||
}`,
|
||||
oauth_user: async (access_token: string) => {
|
||||
const res = await fetch('https://api.github.com/user', {
|
||||
headers: {
|
||||
|
@ -14,10 +16,10 @@ export const github_auth = {
|
|||
};
|
||||
|
||||
export const discord_auth = {
|
||||
oauth_url: (clientId: string, origin: string) =>
|
||||
oauth_url: (clientId: string, origin: string, state?: string) =>
|
||||
`https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
||||
`${origin}/api/auth/oauth/discord`
|
||||
)}&response_type=code&scope=identify`,
|
||||
)}&response_type=code&scope=identify${state ? `&state=${state}` : ''}`,
|
||||
oauth_user: async (access_token: string) => {
|
||||
const res = await fetch('https://discord.com/api/users/@me', {
|
||||
headers: {
|
||||
|
|
|
@ -36,7 +36,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
if (!valid) return res.forbid('Wrong password');
|
||||
|
||||
res.setCookie('user', user.id, {
|
||||
sameSite: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 6.048e8 * 2),
|
||||
path: '/',
|
||||
});
|
||||
|
|
|
@ -13,12 +13,13 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
return res.bad('Discord OAuth is not configured');
|
||||
}
|
||||
|
||||
const { code } = req.query as { code: string };
|
||||
const { code, state } = req.query as { code: string; state?: string };
|
||||
if (!code)
|
||||
return res.redirect(
|
||||
discord_auth.oauth_url(
|
||||
config.oauth.discord_client_id,
|
||||
`${config.core.https ? 'https' : 'http'}://${req.headers.host}`
|
||||
`${config.core.https ? 'https' : 'http'}://${req.headers.host}`,
|
||||
state
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -55,7 +56,34 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
},
|
||||
});
|
||||
|
||||
if (existing && existing.oauth && existing.oauthProvider === 'discord') {
|
||||
if (state && state === 'link') {
|
||||
const user = await req.user();
|
||||
if (!user) return res.error('not logged in, unable to link account');
|
||||
|
||||
if (user.oauth && user.oauthProvider === 'discord')
|
||||
return res.error('account already linked with discord');
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
oauth: true,
|
||||
oauthProvider: 'discord',
|
||||
oauthAccessToken: json.access_token,
|
||||
avatar: avatarBase64,
|
||||
},
|
||||
});
|
||||
req.cleanCookie('user');
|
||||
res.setCookie('user', user.id, {
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 6.048e8 * 2),
|
||||
path: '/',
|
||||
});
|
||||
Logger.get('user').info(`User ${user.username} (${user.id}) linked account via oauth(discord)`);
|
||||
|
||||
return res.redirect('/');
|
||||
} else if (existing && existing.oauth && existing.oauthProvider === 'discord') {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: existing.id,
|
||||
|
@ -67,7 +95,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
|
||||
req.cleanCookie('user');
|
||||
res.setCookie('user', existing.id, {
|
||||
sameSite: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 6.048e8 * 2),
|
||||
path: '/',
|
||||
});
|
||||
|
@ -92,7 +120,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
|
||||
req.cleanCookie('user');
|
||||
res.setCookie('user', user.id, {
|
||||
sameSite: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 6.048e8 * 2),
|
||||
path: '/',
|
||||
});
|
||||
|
|
|
@ -13,9 +13,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
return res.bad('GitHub OAuth is not configured');
|
||||
}
|
||||
|
||||
const { code } = req.query as { code: string };
|
||||
const { code, state } = req.query as { code: string; state: string };
|
||||
|
||||
if (!code) return res.redirect(github_auth.oauth_url(config.oauth.github_client_id));
|
||||
if (!code) return res.redirect(github_auth.oauth_url(config.oauth.github_client_id, state));
|
||||
|
||||
const resp = await fetch('https://github.com/login/oauth/access_token', {
|
||||
method: 'POST',
|
||||
|
@ -47,7 +47,33 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
},
|
||||
});
|
||||
|
||||
if (existing && existing.oauth && existing.oauthProvider === 'github') {
|
||||
if (state && state === 'link') {
|
||||
const user = await req.user();
|
||||
if (!user) return res.error('not logged in, unable to link account');
|
||||
|
||||
if (user.oauth && user.oauthProvider === 'github') return res.error('account already linked with github');
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
oauth: true,
|
||||
oauthProvider: 'github',
|
||||
oauthAccessToken: json.access_token,
|
||||
avatar: avatarBase64,
|
||||
},
|
||||
});
|
||||
req.cleanCookie('user');
|
||||
res.setCookie('user', user.id, {
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 6.048e8 * 2),
|
||||
path: '/',
|
||||
});
|
||||
Logger.get('user').info(`User ${user.username} (${user.id}) linked account via oauth(github)`);
|
||||
|
||||
return res.redirect('/');
|
||||
} else if (existing && existing.oauth && existing.oauthProvider === 'github') {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: existing.id,
|
||||
|
@ -59,7 +85,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
|
||||
req.cleanCookie('user');
|
||||
res.setCookie('user', existing.id, {
|
||||
sameSite: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 6.048e8 * 2),
|
||||
path: '/',
|
||||
});
|
||||
|
@ -84,7 +110,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
|
||||
req.cleanCookie('user');
|
||||
res.setCookie('user', user.id, {
|
||||
sameSite: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 6.048e8 * 2),
|
||||
path: '/',
|
||||
});
|
||||
|
|
35
src/pages/api/auth/oauth/index.ts
Normal file
35
src/pages/api/auth/oauth/index.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import prisma from 'lib/prisma';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
const user = await req.user();
|
||||
if (!user) return res.error('not logged in');
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
if (!user.password)
|
||||
return res.forbid("can't unlink account without a password, please set one then unlink.");
|
||||
|
||||
const nuser = await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
oauth: false,
|
||||
oauthProvider: null,
|
||||
oauthAccessToken: null,
|
||||
},
|
||||
});
|
||||
|
||||
delete nuser.password;
|
||||
|
||||
return res.json(nuser);
|
||||
} else {
|
||||
return res.json({
|
||||
enabled: user.oauth,
|
||||
provider: user.oauthProvider,
|
||||
access_token: user.oauthAccessToken,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default withZipline(handler);
|
|
@ -41,7 +41,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
});
|
||||
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
await datasource.delete(files[i].file);
|
||||
try {
|
||||
await datasource.delete(files[i].file);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const { count } = await prisma.image.deleteMany({
|
||||
|
|
|
@ -18,7 +18,7 @@ export default function ManagePage(props) {
|
|||
<title>{title}</title>
|
||||
</Head>
|
||||
<Layout props={props}>
|
||||
<Manage />
|
||||
<Manage oauth_providers={props.oauth_providers} oauth_registration={props.oauth_registration} />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue