diff --git a/frontend/desktop/prisma/global/schema.prisma b/frontend/desktop/prisma/global/schema.prisma index 579aff516ea..6cb383e6109 100644 --- a/frontend/desktop/prisma/global/schema.prisma +++ b/frontend/desktop/prisma/global/schema.prisma @@ -107,4 +107,5 @@ enum ProviderType { WECHAT GOOGLE PASSWORD + OAUTH2 } diff --git a/frontend/desktop/src/__tests__/api/e2e/namespace/invite.test.ts b/frontend/desktop/src/__tests__/api/e2e/namespace/invite.test.ts index 8a2800cfdc0..a62487cec74 100644 --- a/frontend/desktop/src/__tests__/api/e2e/namespace/invite.test.ts +++ b/frontend/desktop/src/__tests__/api/e2e/namespace/invite.test.ts @@ -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; @@ -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); diff --git a/frontend/desktop/src/components/signin/auth/AuthList.tsx b/frontend/desktop/src/components/signin/auth/AuthList.tsx index b80453ce951..d38da4df1a8 100644 --- a/frontend/desktop/src/components/signin/auth/AuthList.tsx +++ b/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'; @@ -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); @@ -100,6 +103,29 @@ const AuthList = () => { }); }, need: needGoogle + }, + { + icon: () => ( +
+ logo +
+ ), + 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 } ]; diff --git a/frontend/desktop/src/pages/api/auth/namespace/invite.ts b/frontend/desktop/src/pages/api/auth/namespace/invite.ts index 68df428dca0..920663e2b19 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/invite.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/invite.ts @@ -1,6 +1,5 @@ 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'; @@ -8,6 +7,8 @@ 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 { @@ -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); diff --git a/frontend/desktop/src/pages/api/auth/oauth/oauth2/index.ts b/frontend/desktop/src/pages/api/auth/oauth/oauth2/index.ts new file mode 100644 index 00000000000..8c321fbaa9d --- /dev/null +++ b/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 + }); + } +} diff --git a/frontend/desktop/src/pages/api/platform/getEnv.ts b/frontend/desktop/src/pages/api/platform/getEnv.ts index 0d19f15b9f9..e6f008f9666 100644 --- a/frontend/desktop/src/pages/api/platform/getEnv.ts +++ b/frontend/desktop/src/pages/api/platform/getEnv.ts @@ -9,7 +9,8 @@ import { enableWechatRecharge, enableLicense, enableRecharge, - enableOpenWechat + enableOpenWechat, + enableOAuth2 } from '@/services/enable'; import { SystemEnv } from '@/types'; @@ -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(res, { data: { SEALOS_CLOUD_DOMAIN: process.env.SEALOS_CLOUD_DOMAIN || 'cloud.sealos.io', @@ -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 } }); } diff --git a/frontend/desktop/src/pages/callback.tsx b/frontend/desktop/src/pages/callback.tsx index 58c45fde658..827f204794c 100644 --- a/frontend/desktop/src/pages/callback.tsx +++ b/frontend/desktop/src/pages/callback.tsx @@ -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'); diff --git a/frontend/desktop/src/services/backend/globalAuth.ts b/frontend/desktop/src/services/backend/globalAuth.ts index cf1f9ef003c..985a34e722b 100644 --- a/frontend/desktop/src/services/backend/globalAuth.ts +++ b/frontend/desktop/src/services/backend/globalAuth.ts @@ -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; @@ -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: { diff --git a/frontend/desktop/src/services/enable.ts b/frontend/desktop/src/services/enable.ts index f9d74772547..d0ea9dd4607 100644 --- a/frontend/desktop/src/services/enable.ts +++ b/frontend/desktop/src/services/enable.ts @@ -36,8 +36,13 @@ 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 && @@ -45,3 +50,10 @@ export const enablePersistImage = () => !!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; diff --git a/frontend/desktop/src/stores/global.ts b/frontend/desktop/src/stores/global.ts index 509852f1edd..f66cb2b9085 100644 --- a/frontend/desktop/src/stores/global.ts +++ b/frontend/desktop/src/stores/global.ts @@ -34,7 +34,10 @@ export const useGlobalStore = create()( 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(); diff --git a/frontend/desktop/src/types/api.ts b/frontend/desktop/src/types/api.ts index 4f849afec10..f53ba5bf39e 100644 --- a/frontend/desktop/src/types/api.ts +++ b/frontend/desktop/src/types/api.ts @@ -3,5 +3,3 @@ export type ApiResp = { message?: string; data?: T; }; -export const INVITE_LIMIT = 5; -export const TEAM_LIMIT = 5; diff --git a/frontend/desktop/src/types/system.ts b/frontend/desktop/src/types/system.ts index e6febf7cf65..9e0b1691469 100644 --- a/frontend/desktop/src/types/system.ts +++ b/frontend/desktop/src/types/system.ts @@ -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 = { diff --git a/frontend/desktop/src/types/user.ts b/frontend/desktop/src/types/user.ts index 13e91fb8a2e..229cbc5d9f1 100644 --- a/frontend/desktop/src/types/user.ts +++ b/frontend/desktop/src/types/user.ts @@ -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; @@ -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; +}; diff --git a/frontend/providers/applaunchpad/README.md b/frontend/providers/applaunchpad/README.md index ba2e19ad670..771484afa45 100644 --- a/frontend/providers/applaunchpad/README.md +++ b/frontend/providers/applaunchpad/README.md @@ -1,76 +1,62 @@ # sealos app launchpad +## Preparation, refer to the README.md in the frontend directory + ## project tree + ```bash . -├── Dockerfile -├── Makefile -├── README.md +├── data +│ ├── form_slider_config.json // Optional configuration files ├── deploy -│ └── manifests -│ └── frontend.yaml -├── next-env.d.ts -├── next.config.js -├── package.json -├── pnpm-lock.yaml ├── public -│ └── favicon.ico +│ ├── locales +│ ├── favicon.ico +│ └── logo.svg ├── src -│ ├── api # FE api -│ ├── components # global components -│ │ ├── AppStatusTag -│ │ ├── ButtonGroup -│ │ ├── FormControl -│ │ ├── Icon -│ │ │ ├── icons # svg icon -│ │ │ └── index.tsx -│ │ ├── PodLineChart -│ │ ├── RangeInput -│ │ ├── RangeSlider -│ │ ├── Slider -│ │ └── YamlCode -│ ├── constants # global constant data -│ │ ├── app.ts -│ │ ├── editApp.ts -│ │ └── theme.ts -│ ├── hooks # global hooks -│ │ ├── useConfirm.tsx -│ │ ├── useLoading.tsx -│ │ ├── useScreen.ts -│ │ └── useToast.ts -│ ├── mock -│ ├── pages -│ │ ├── 404.tsx -│ │ ├── _app.tsx -│ │ ├── _document.tsx -│ │ ├── api # server api +│ ├── api +│ ├── components // Components +│ ├── constants +│ ├── hooks +│ ├── mock +│ ├── pages // Pages, the path is the route +│ │ ├── api // Server-side API │ │ ├── app │ │ │ ├── detail +│ │ │ │ ├── components +│ │ │ │ ├── index.module.scss +│ │ │ │ └── index.tsx // Detail page │ │ │ └── edit -│ │ └── apps -│ │ └── index.tsx -│ ├── services # server function +│ │ │ ├── components +│ │ │ ├── index.module.scss +│ │ │ └── index.tsx // Create and edit page +│ │ ├── apps +│ │ │ ├── components +│ │ │ ├── index.module.scss +│ │ │ └── index.tsx // App list page +│ │ ├── 404.tsx +│ │ ├── _app.tsx +│ │ └── _document.tsx +│ ├── services │ │ ├── backend │ │ │ ├── auth.ts │ │ │ ├── kubernetes.ts │ │ │ └── response.ts │ │ ├── error.ts │ │ ├── kubernet.ts -│ │ └── request.ts -│ ├── store # FE store -│ │ ├── app.ts -│ │ ├── global.ts -│ │ └── static.ts +│ │ ├── monitorFetch.ts +│ │ ├── request.ts +│ │ └── streamFetch.ts +│ ├── store │ ├── styles -│ │ └── reset.scss │ ├── types -│ │ ├── app.d.ts -│ │ ├── index.d.ts -│ │ └── user.d.ts │ └── utils -│ ├── adapt.ts # format api data -│ ├── deployYaml2Json.ts # form data to yaml -│ ├── tools.ts -│ └── user.ts +├── Dockerfile +├── Makefile +├── README.md +├── next-env.d.ts +├── next-i18next.config.js +├── next.config.js +├── package.json └── tsconfig.json -``` \ No newline at end of file +``` diff --git a/frontend/providers/costcenter/src/components/billing/TypeMenu.tsx b/frontend/providers/costcenter/src/components/billing/TypeMenu.tsx index de5a3dc60c1..6034b4d7143 100644 --- a/frontend/providers/costcenter/src/components/billing/TypeMenu.tsx +++ b/frontend/providers/costcenter/src/components/billing/TypeMenu.tsx @@ -21,6 +21,7 @@ export default function TypeMenu({