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: () => (
+
+
+
+ ),
+ 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({