Skip to content

Commit

Permalink
feat:Desktop add standard OAuth2 login support (#4671)
Browse files Browse the repository at this point in the history
* feat:Desktop add standard OAuth2 login support

Signed-off-by: jingyang <3161362058@qq.com>

* fix bug

Signed-off-by: jingyang <3161362058@qq.com>

---------

Signed-off-by: jingyang <3161362058@qq.com>
  • Loading branch information
zjy365 committed Apr 10, 2024
1 parent ad83b84 commit 3bdcc73
Show file tree
Hide file tree
Showing 15 changed files with 230 additions and 71 deletions.
1 change: 1 addition & 0 deletions frontend/desktop/prisma/global/schema.prisma
Expand Up @@ -107,4 +107,5 @@ enum ProviderType {
WECHAT
GOOGLE
PASSWORD
OAUTH2
}
Expand Up @@ -13,14 +13,16 @@ import { Session } from 'sealos-desktop-sdk/*';
import * as k8s from '@kubernetes/client-node';
import request from '@/__tests__/api/request';
import { _setAuth, cleanDb, cleanK8s } from '@/__tests__/api/tools';
import { INVITE_LIMIT } from '@/types';
import { AccessTokenPayload } from '@/types/token';
import { prisma } from '@/services/backend/db/init';
import { jwtDecode } from 'jwt-decode';
import { getTeamInviteLimit } from '@/services/enable';
const createRequest = _createRequest(request);
const inviteMemberRequest = _inviteMemberRequest(request);
const verifyInviteRequest = _verifyInviteRequest(request);
const listNamespaceRequest = _nsListRequest(request);
const TEAM_INVITE_LIMIT = getTeamInviteLimit();

describe('invite member', () => {
let token1: string;
let payload1: AccessTokenPayload;
Expand Down Expand Up @@ -175,7 +177,7 @@ describe('invite member', () => {
role
});
console.log(i);
if (i < INVITE_LIMIT) {
if (i < TEAM_INVITE_LIMIT) {
expect(inviteRes.code).toBe(200);
} else {
expect(inviteRes.code).toBe(403);
Expand Down
30 changes: 28 additions & 2 deletions frontend/desktop/src/components/signin/auth/AuthList.tsx
@@ -1,7 +1,7 @@
import { useGlobalStore } from '@/stores/global';
import useSessionStore from '@/stores/session';
import { OauthProvider } from '@/types/user';
import { Button, Flex, Icon } from '@chakra-ui/react';
import { Button, Image, Flex, Icon, Center } from '@chakra-ui/react';
import { GithubIcon, GoogleIcon, WechatIcon } from '@sealos/ui';
import { useRouter } from 'next/router';
import { MouseEventHandler } from 'react';
Expand All @@ -16,7 +16,10 @@ const AuthList = () => {
google_client_id = '',
callback_url = '',
// https://sealos.io/siginIn
oauth_proxy = ''
oauth_proxy = '',
oauth2_client_id,
oauth2_auth_url,
needOAuth2 = false
} = systemEnv ?? {};
const oauthLogin = async ({ url, provider }: { url: string; provider?: OauthProvider }) => {
setProvider(provider);
Expand Down Expand Up @@ -100,6 +103,29 @@ const AuthList = () => {
});
},
need: needGoogle
},
{
icon: () => (
<Center>
<Image alt="logo" width={'20px'} src="logo.svg"></Image>
</Center>
),
cb: (e) => {
e.preventDefault();
const state = generateState();
if (oauth_proxy)
oauthProxyLogin({
provider: 'oauth2',
state,
id: oauth2_client_id
});
else
oauthLogin({
provider: 'oauth2',
url: `${oauth2_auth_url}?client_id=${oauth2_client_id}&redirect_uri=${callback_url}&response_type=code&state=${state}`
});
},
need: needOAuth2
}
];

Expand Down
5 changes: 3 additions & 2 deletions frontend/desktop/src/pages/api/auth/namespace/invite.ts
@@ -1,13 +1,14 @@
import { jsonRes } from '@/services/backend/response';
import { bindingRole } from '@/services/backend/team';
import { INVITE_LIMIT } from '@/types/api';
import { UserRole } from '@/types/team';
import { isUserRole, roleToUserRole, vaildManage } from '@/utils/tools';
import { NextApiRequest, NextApiResponse } from 'next';
import { globalPrisma, prisma } from '@/services/backend/db/init';
import { validate } from 'uuid';
import { JoinStatus } from 'prisma/region/generated/client';
import { verifyAccessToken } from '@/services/backend/auth';
import { getTeamInviteLimit } from '@/services/enable';
const TEAM_INVITE_LIMIT = getTeamInviteLimit();

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
Expand Down Expand Up @@ -57,7 +58,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!vaild) return jsonRes(res, { code: 403, message: 'you are not manager' });
if (queryResults.length === 0)
return jsonRes(res, { code: 404, message: 'there are not user in the namespace ' });
if (queryResults.length >= INVITE_LIMIT)
if (queryResults.length >= TEAM_INVITE_LIMIT)
return jsonRes(res, { code: 403, message: 'the invited users are too many' });

const tItem = queryResults.find((item) => item.userCr.uid === targetRegionUser.uid);
Expand Down
91 changes: 91 additions & 0 deletions frontend/desktop/src/pages/api/auth/oauth/oauth2/index.ts
@@ -0,0 +1,91 @@
import { getGlobalToken } from '@/services/backend/globalAuth';
import { jsonRes } from '@/services/backend/response';
import { enableOAuth2 } from '@/services/enable';
import { OAuth2Type, OAuth2UserInfoType } from '@/types/user';
import { customAlphabet } from 'nanoid';
import { NextApiRequest, NextApiResponse } from 'next';
import { ProviderType } from 'prisma/global/generated/client';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12);

const clientId = process.env.OAUTH2_CLIENT_ID!;
const clientSecret = process.env.OAUTH2_CLIENT_SECRET!;
const tokenUrl = process.env.OAUTH2_TOKEN_URL;
const userInfoUrl = process.env.OAUTH2_USERINFO_URL;
const redirectUrl = process.env.CALLBACK_URL;

//OAuth2 Support client_secret_post method to obtain token
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (!enableOAuth2() || !redirectUrl) {
throw new Error('District related env');
}

const { code, inviterId } = req.body;
const url = `${tokenUrl}`;
const oauth2Data = (await (
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
code,
client_id: clientId,
client_secret: clientSecret,
grant_type: 'authorization_code',
redirect_uri: redirectUrl
})
})
).json()) as OAuth2Type;
const access_token = oauth2Data.access_token;

if (!access_token) {
return jsonRes(res, {
message: 'Failed to authenticate',
code: 500,
data: 'access_token is null'
});
}

const userUrl = `${userInfoUrl}?access_token=${access_token}`;
const response = await fetch(userUrl, {
headers: {
Authorization: `Bearer ${access_token}`
}
});
if (!response.ok)
return jsonRes(res, {
code: 401,
message: 'Unauthorized'
});
const result = (await response.json()) as OAuth2UserInfoType;

const id = result.sub;
const name = result?.nickname || result?.name || nanoid(8);
const avatar_url = result?.picture || '';

const data = await getGlobalToken({
provider: ProviderType.OAUTH2,
id: id + '',
avatar_url,
name,
inviterId
});
if (!data)
return jsonRes(res, {
code: 401,
message: 'Unauthorized'
});
return jsonRes(res, {
data,
code: 200,
message: 'Successfully'
});
} catch (err) {
console.log(err);
return jsonRes(res, {
message: 'Failed to authenticate with GitHub',
code: 500
});
}
}
12 changes: 10 additions & 2 deletions frontend/desktop/src/pages/api/platform/getEnv.ts
Expand Up @@ -9,7 +9,8 @@ import {
enableWechatRecharge,
enableLicense,
enableRecharge,
enableOpenWechat
enableOpenWechat,
enableOAuth2
} from '@/services/enable';
import { SystemEnv } from '@/types';

Expand All @@ -34,6 +35,10 @@ export default async function handler(_: NextApiRequest, res: NextApiResponse) {
const rechargeEnabled = enableRecharge();
const guideEnabled = process.env.GUIDE_ENABLED === 'true';
const openWechatEnabled = enableOpenWechat();
const oauth2_client_id = process.env.OAUTH2_CLIENT_ID || '';
const oauth2_auth_url = process.env.OAUTH2_AUTH_URL || '';
const needOAuth2 = enableOAuth2();

return jsonRes<SystemEnv>(res, {
data: {
SEALOS_CLOUD_DOMAIN: process.env.SEALOS_CLOUD_DOMAIN || 'cloud.sealos.io',
Expand All @@ -56,7 +61,10 @@ export default async function handler(_: NextApiRequest, res: NextApiResponse) {
licenseEnabled,
guideEnabled,
openWechatEnabled,
cf_sitekey
cf_sitekey,
oauth2_client_id,
oauth2_auth_url,
needOAuth2
}
});
}
2 changes: 1 addition & 1 deletion frontend/desktop/src/pages/callback.tsx
Expand Up @@ -24,7 +24,7 @@ const Callback: NextPage = () => {
let isProxy: boolean = false;
(async () => {
try {
if (!provider || !['github', 'wechat', 'google'].includes(provider))
if (!provider || !['github', 'wechat', 'google', 'oauth2'].includes(provider))
throw new Error('provider error');
const { code, state } = router.query;
if (!isString(code) || !isString(state)) throw new Error('failed to get code and state');
Expand Down
6 changes: 3 additions & 3 deletions frontend/desktop/src/services/backend/globalAuth.ts
Expand Up @@ -86,7 +86,7 @@ export async function signInByPassword({ id, password }: { id: string; password:
async function signUp({
provider,
id,
name,
name: nickname,
avatar_url
}: {
provider: ProviderType;
Expand All @@ -99,9 +99,9 @@ async function signUp({
const name = nanoid(10);
user = await globalPrisma.user.create({
data: {
name,
name: name,
id: name,
nickname: name,
nickname: nickname,
avatarUri: avatar_url,
oauthProvider: {
create: {
Expand Down
12 changes: 12 additions & 0 deletions frontend/desktop/src/services/enable.ts
Expand Up @@ -36,12 +36,24 @@ export const enableWechatRecharge = () => process.env['WECHAT_ENABLED'] === 'tru
export const enableLicense = () => {
return process.env.LICENSE_ENABLED === 'true';
};

export const getTeamLimit = () => parseInt(process.env['TEAM_LIMIT'] || '') || 50;

export const getTeamInviteLimit = () => parseInt(process.env['TEAM_INVITE_LIMIT'] || '') || 50;

export const getRegionUid = () => process.env['REGION_UID'] || '';

export const enablePersistImage = () =>
!!process.env.OS_URL &&
!!process.env.OS_BUCKET_NAME &&
!!Number(process.env.OS_PORT) &&
!!process.env.OS_ACCESS_KEY &&
!!process.env.OS_SECRET_KEY &&
process.env.PERSIST_AVATAR_ENABLED === 'true';

export const enableOAuth2 = () =>
!!process.env.OAUTH2_CLIENT_ID &&
!!process.env.OAUTH2_CLIENT_SECRET &&
!!process.env.OAUTH2_AUTH_URL &&
!!process.env.OAUTH2_TOKEN_URL &&
!!process.env.OAUTH2_USERINFO_URL;
5 changes: 4 additions & 1 deletion frontend/desktop/src/stores/global.ts
Expand Up @@ -34,7 +34,10 @@ export const useGlobalStore = create<GlobalState>()(
wechatEnabledRecharge: false,
SEALOS_CLOUD_DOMAIN: 'cloud.sealos.io',
rechargeEnabled: false,
openWechatEnabled: false
openWechatEnabled: false,
oauth2_client_id: '',
oauth2_auth_url: '',
needOAuth2: false
},
async initSystemEnv() {
const data = await getSystemEnv();
Expand Down
2 changes: 0 additions & 2 deletions frontend/desktop/src/types/api.ts
Expand Up @@ -3,5 +3,3 @@ export type ApiResp<T = any> = {
message?: string;
data?: T;
};
export const INVITE_LIMIT = 5;
export const TEAM_LIMIT = 5;
3 changes: 3 additions & 0 deletions frontend/desktop/src/types/system.ts
Expand Up @@ -30,6 +30,9 @@ export type LoginProps = {
needGithub: boolean;
needWechat: boolean;
needGoogle: boolean;
oauth2_client_id: string;
oauth2_auth_url: string;
needOAuth2: boolean;
};

export type SystemEnv = {
Expand Down
28 changes: 27 additions & 1 deletion frontend/desktop/src/types/user.ts
Expand Up @@ -71,7 +71,8 @@ export const PROVIDERS = [
'uid',
'password_user',
'google',
'wechat_open'
'wechat_open',
'oauth2'
] as const;
export type Provider = (typeof PROVIDERS)[number];
export type OauthProvider = Exclude<Provider, 'uid' | 'password_user' | 'phone'>;
Expand Down Expand Up @@ -124,3 +125,28 @@ export type AccountCRD = {
encryptDeductionBalance: string;
};
};

export type OAuth2Type = {
access_token: string;
token_type: string;
expires_in: 3599;
refresh_token: string;
scope: string;
};
export type OAuth2UserInfoType = {
sub: string;
birthdate: string | null;
family_name: string | null;
gender: 'M' | 'F' | 'U';
given_name: string | null;
locale: string | null;
middle_name: string | null;
name: string | null;
nickname: string | null;
picture: string;
preferred_username: string | null;
profile: string | null;
updated_at: string;
website: string | null;
zoneinfo: string | null;
};

0 comments on commit 3bdcc73

Please sign in to comment.