merge branch 'main' of https://github.com/Privoce/rustchat-web
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
|
/.idea
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
|
|||||||
+148
-153
@@ -1,165 +1,160 @@
|
|||||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||||
import { nanoid } from "@reduxjs/toolkit";
|
import { nanoid } from "@reduxjs/toolkit";
|
||||||
import baseQuery from "./base.query";
|
import baseQuery from "./base.query";
|
||||||
import {
|
import { setAuthData, updateToken, resetAuthData, updateInitialized } from "../slices/auth.data";
|
||||||
setAuthData,
|
|
||||||
updateToken,
|
|
||||||
resetAuthData,
|
|
||||||
updateInitilize,
|
|
||||||
} from "../slices/auth.data";
|
|
||||||
import BASE_URL, { KEY_DEVICE_KEY } from "../config";
|
import BASE_URL, { KEY_DEVICE_KEY } from "../config";
|
||||||
const getDeviceId = () => {
|
const getDeviceId = () => {
|
||||||
let d = localStorage.getItem(KEY_DEVICE_KEY);
|
let d = localStorage.getItem(KEY_DEVICE_KEY);
|
||||||
if (!d) {
|
if (!d) {
|
||||||
d = `web:${nanoid()}`;
|
d = `web:${nanoid()}`;
|
||||||
localStorage.setItem(KEY_DEVICE_KEY, d);
|
localStorage.setItem(KEY_DEVICE_KEY, d);
|
||||||
}
|
}
|
||||||
return d;
|
return d;
|
||||||
};
|
};
|
||||||
export const authApi = createApi({
|
export const authApi = createApi({
|
||||||
reducerPath: "authApi",
|
reducerPath: "authApi",
|
||||||
baseQuery,
|
baseQuery,
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
login: builder.mutation({
|
login: builder.mutation({
|
||||||
query: (credentials) => ({
|
query: (credentials) => ({
|
||||||
url: "token/login",
|
url: "token/login",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
credential: credentials,
|
credential: credentials,
|
||||||
device: getDeviceId(),
|
device: getDeviceId(),
|
||||||
device_token: "test",
|
device_token: "test"
|
||||||
},
|
}
|
||||||
}),
|
}),
|
||||||
transformResponse: (data) => {
|
transformResponse: (data) => {
|
||||||
const { avatar_updated_at } = data.user;
|
const { avatar_updated_at } = data.user;
|
||||||
data.user.avatar =
|
data.user.avatar =
|
||||||
avatar_updated_at == 0
|
avatar_updated_at == 0
|
||||||
? ""
|
? ""
|
||||||
: `${BASE_URL}/resource/avatar?uid=${data.user.uid}&t=${avatar_updated_at}`;
|
: `${BASE_URL}/resource/avatar?uid=${data.user.uid}&t=${avatar_updated_at}`;
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
async onQueryStarted(params, { dispatch, queryFulfilled }) {
|
async onQueryStarted(params, { dispatch, queryFulfilled }) {
|
||||||
try {
|
try {
|
||||||
const { data } = await queryFulfilled;
|
const { data } = await queryFulfilled;
|
||||||
if (data) {
|
if (data) {
|
||||||
console.log("login resp", data);
|
console.log("login resp", data);
|
||||||
dispatch(setAuthData(data));
|
dispatch(setAuthData(data));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
console.log("login error");
|
console.log("login error");
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}),
|
|
||||||
// 更新token
|
|
||||||
renew: builder.mutation({
|
|
||||||
query: ({ token, refreshToken }) => ({
|
|
||||||
url: "/token/renew",
|
|
||||||
method: "POST",
|
|
||||||
body: {
|
|
||||||
token,
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
async onQueryStarted(params, { dispatch, queryFulfilled }) {
|
|
||||||
try {
|
|
||||||
const { data } = await queryFulfilled;
|
|
||||||
dispatch(updateToken(data));
|
|
||||||
} catch {
|
|
||||||
dispatch(resetAuthData());
|
|
||||||
console.log("renew token error");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
// 更新 device token
|
|
||||||
updateDeviceToken: builder.mutation({
|
|
||||||
query: (device_token) => ({
|
|
||||||
url: "/token/device_token",
|
|
||||||
method: "PUT",
|
|
||||||
body: {
|
|
||||||
device_token,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
// 获取openid
|
|
||||||
getOpenid: builder.mutation({
|
|
||||||
query: ({ issuer, redirect_uri }) => ({
|
|
||||||
url: "/token/openid/authorize",
|
|
||||||
method: "POST",
|
|
||||||
body: {
|
|
||||||
issuer,
|
|
||||||
redirect_uri,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
checkInviteTokenValid: builder.mutation({
|
|
||||||
query: (token) => ({
|
|
||||||
url: "user/check_invite_magic_token",
|
|
||||||
method: "POST",
|
|
||||||
body: { magic_token: token },
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
updatePassword: builder.mutation({
|
|
||||||
query: ({ old_password, new_password }) => ({
|
|
||||||
url: "user/change_password",
|
|
||||||
method: "POST",
|
|
||||||
body: { old_password, new_password },
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
sendMagicLink: builder.mutation({
|
|
||||||
query: (email) => ({
|
|
||||||
url: "token/send_magic_link",
|
|
||||||
method: "POST",
|
|
||||||
body: { email },
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
getMetamaskNonce: builder.query({
|
|
||||||
query: (address) => ({
|
|
||||||
url: `/token/metamask/nonce?public_address=${address}`,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
getCredentials: builder.query({
|
|
||||||
query: () => ({
|
|
||||||
url: `/token/credentials`,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
logout: builder.query({
|
|
||||||
query: () => ({ url: `token/logout` }),
|
|
||||||
async onQueryStarted(params, { dispatch, queryFulfilled }) {
|
|
||||||
try {
|
|
||||||
await queryFulfilled;
|
|
||||||
dispatch(resetAuthData());
|
|
||||||
} catch {
|
|
||||||
console.log("logout error");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
getInitialized: builder.query({
|
|
||||||
query: () => ({
|
|
||||||
url: `/admin/system/initialized`,
|
|
||||||
}),
|
|
||||||
async onQueryStarted(params, { dispatch, queryFulfilled }) {
|
|
||||||
try {
|
|
||||||
const { data: isInitized } = await queryFulfilled;
|
|
||||||
dispatch(updateInitilize(isInitized));
|
|
||||||
} catch {
|
|
||||||
console.log("api initialized error");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
|
// 更新token
|
||||||
|
renew: builder.mutation({
|
||||||
|
query: ({ token, refreshToken }) => ({
|
||||||
|
url: "/token/renew",
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
token,
|
||||||
|
refresh_token: refreshToken
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
async onQueryStarted(params, { dispatch, queryFulfilled }) {
|
||||||
|
try {
|
||||||
|
const { data } = await queryFulfilled;
|
||||||
|
dispatch(updateToken(data));
|
||||||
|
} catch {
|
||||||
|
dispatch(resetAuthData());
|
||||||
|
console.log("renew token error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// 更新 device token
|
||||||
|
updateDeviceToken: builder.mutation({
|
||||||
|
query: (device_token) => ({
|
||||||
|
url: "/token/device_token",
|
||||||
|
method: "PUT",
|
||||||
|
body: {
|
||||||
|
device_token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
// 获取openid
|
||||||
|
getOpenid: builder.mutation({
|
||||||
|
query: ({ issuer, redirect_uri }) => ({
|
||||||
|
url: "/token/openid/authorize",
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
issuer,
|
||||||
|
redirect_uri
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
checkInviteTokenValid: builder.mutation({
|
||||||
|
query: (token) => ({
|
||||||
|
url: "user/check_invite_magic_token",
|
||||||
|
method: "POST",
|
||||||
|
body: { magic_token: token }
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
updatePassword: builder.mutation({
|
||||||
|
query: ({ old_password, new_password }) => ({
|
||||||
|
url: "user/change_password",
|
||||||
|
method: "POST",
|
||||||
|
body: { old_password, new_password }
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
sendMagicLink: builder.mutation({
|
||||||
|
query: (email) => ({
|
||||||
|
url: "token/send_magic_link",
|
||||||
|
method: "POST",
|
||||||
|
body: { email }
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
getMetamaskNonce: builder.query({
|
||||||
|
query: (address) => ({
|
||||||
|
url: `/token/metamask/nonce?public_address=${address}`
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
getCredentials: builder.query({
|
||||||
|
query: () => ({
|
||||||
|
url: `/token/credentials`
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
logout: builder.query({
|
||||||
|
query: () => ({ url: `token/logout` }),
|
||||||
|
async onQueryStarted(params, { dispatch, queryFulfilled }) {
|
||||||
|
try {
|
||||||
|
await queryFulfilled;
|
||||||
|
dispatch(resetAuthData());
|
||||||
|
} catch {
|
||||||
|
console.log("logout error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
getInitialized: builder.query({
|
||||||
|
query: () => ({
|
||||||
|
url: `/admin/system/initialized`
|
||||||
|
}),
|
||||||
|
async onQueryStarted(params, { dispatch, queryFulfilled }) {
|
||||||
|
try {
|
||||||
|
const { data: isInitialized } = await queryFulfilled;
|
||||||
|
dispatch(updateInitialized(isInitialized));
|
||||||
|
} catch {
|
||||||
|
console.log("api initialized error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
useGetInitializedQuery,
|
useGetInitializedQuery,
|
||||||
useSendMagicLinkMutation,
|
useSendMagicLinkMutation,
|
||||||
useGetCredentialsQuery,
|
useGetCredentialsQuery,
|
||||||
useUpdateDeviceTokenMutation,
|
useUpdateDeviceTokenMutation,
|
||||||
useGetOpenidMutation,
|
useGetOpenidMutation,
|
||||||
useRenewMutation,
|
useRenewMutation,
|
||||||
useLazyGetMetamaskNonceQuery,
|
useLazyGetMetamaskNonceQuery,
|
||||||
useLoginMutation,
|
useLoginMutation,
|
||||||
useLazyLogoutQuery,
|
useLazyLogoutQuery,
|
||||||
useCheckInviteTokenValidMutation,
|
useCheckInviteTokenValidMutation,
|
||||||
useUpdatePasswordMutation,
|
useUpdatePasswordMutation
|
||||||
} = authApi;
|
} = authApi;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const whiteList = [
|
|||||||
"getMetamaskNonce",
|
"getMetamaskNonce",
|
||||||
"renew",
|
"renew",
|
||||||
"getInitialized",
|
"getInitialized",
|
||||||
|
"createAdmin",
|
||||||
];
|
];
|
||||||
const baseQuery = fetchBaseQuery({
|
const baseQuery = fetchBaseQuery({
|
||||||
baseUrl: BASE_URL,
|
baseUrl: BASE_URL,
|
||||||
|
|||||||
+210
-196
@@ -4,204 +4,218 @@ import { updateInfo } from "../slices/server";
|
|||||||
import baseQuery from "./base.query";
|
import baseQuery from "./base.query";
|
||||||
const defaultExpireDuration = 7 * 24 * 60 * 60;
|
const defaultExpireDuration = 7 * 24 * 60 * 60;
|
||||||
export const serverApi = createApi({
|
export const serverApi = createApi({
|
||||||
reducerPath: "serverApi",
|
reducerPath: "serverApi",
|
||||||
baseQuery,
|
baseQuery,
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
getServer: builder.query({
|
getServer: builder.query({
|
||||||
query: () => ({ url: `admin/system/organization` }),
|
query: () => ({ url: `admin/system/organization` }),
|
||||||
transformResponse: (data) => {
|
transformResponse: (data) => {
|
||||||
data.logo = `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`;
|
data.logo = `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`;
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
async onQueryStarted(data, { dispatch, queryFulfilled }) {
|
async onQueryStarted(data, { dispatch, queryFulfilled }) {
|
||||||
try {
|
try {
|
||||||
const { data: server } = await queryFulfilled;
|
const { data: server } = await queryFulfilled;
|
||||||
dispatch(updateInfo(server));
|
dispatch(updateInfo(server));
|
||||||
} catch {
|
} catch {
|
||||||
console.log("get server info error");
|
console.log("get server info error");
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}),
|
|
||||||
getThirdPartySecret: builder.query({
|
|
||||||
query: () => ({
|
|
||||||
// headers: {
|
|
||||||
// "content-type": "text/plain",
|
|
||||||
// accept: "text/plain",
|
|
||||||
// },
|
|
||||||
url: `/admin/system/third_party_secret`,
|
|
||||||
responseHandler: (response) => response.text(),
|
|
||||||
}),
|
|
||||||
keepUnusedDataFor: 0,
|
|
||||||
}),
|
|
||||||
updateThirdPartySecret: builder.mutation({
|
|
||||||
query: () => ({
|
|
||||||
url: `/admin/system/third_party_secret`,
|
|
||||||
method: "POST",
|
|
||||||
responseHandler: (response) => response.text(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
getMetrics: builder.query({
|
|
||||||
query: () => ({ url: `/admin/system/metrics` }),
|
|
||||||
}),
|
|
||||||
getServerVersion: builder.query({
|
|
||||||
query: () => ({
|
|
||||||
headers: {
|
|
||||||
// "content-type": "text/plain",
|
|
||||||
accept: "text/plain",
|
|
||||||
},
|
|
||||||
url: `/admin/system/version`,
|
|
||||||
responseHandler: (response) => response.text(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
getFirebaseConfig: builder.query({
|
|
||||||
query: () => ({ url: `admin/fcm/config` }),
|
|
||||||
}),
|
|
||||||
getGoogleAuthConfig: builder.query({
|
|
||||||
query: () => ({ url: `admin/google_auth/config` }),
|
|
||||||
}),
|
|
||||||
updateGoogleAuthConfig: builder.mutation({
|
|
||||||
query: (data) => ({
|
|
||||||
url: `admin/google_auth/config`,
|
|
||||||
method: "POST",
|
|
||||||
body: data,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
getGithubAuthConfig: builder.query({
|
|
||||||
query: () => ({ url: `admin/github_auth/config` }),
|
|
||||||
}),
|
|
||||||
updateGithubAuthConfig: builder.mutation({
|
|
||||||
query: (data) => ({
|
|
||||||
url: `admin/github_auth/config`,
|
|
||||||
method: "POST",
|
|
||||||
body: data,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
sendTestEmail: builder.mutation({
|
|
||||||
query: (data) => ({
|
|
||||||
url: `/admin/system/send_mail`,
|
|
||||||
method: "POST",
|
|
||||||
body: data,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
updateFirebaseConfig: builder.mutation({
|
|
||||||
query: (data) => ({
|
|
||||||
url: `admin/fcm/config`,
|
|
||||||
method: "POST",
|
|
||||||
body: data,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
getAgoraConfig: builder.query({
|
|
||||||
query: () => ({ url: `admin/agora/config` }),
|
|
||||||
}),
|
|
||||||
updateAgoraConfig: builder.mutation({
|
|
||||||
query: (data) => ({
|
|
||||||
url: `admin/agora/config`,
|
|
||||||
method: "POST",
|
|
||||||
body: data,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
getSMTPConfig: builder.query({
|
|
||||||
query: () => ({ url: `admin/smtp/config` }),
|
|
||||||
}),
|
|
||||||
getSMTPStatus: builder.query({
|
|
||||||
query: () => ({ url: `/admin/smtp/enabled` }),
|
|
||||||
}),
|
|
||||||
updateSMTPConfig: builder.mutation({
|
|
||||||
query: (data) => ({
|
|
||||||
url: `admin/smtp/config`,
|
|
||||||
method: "POST",
|
|
||||||
body: data,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
getLoginConfig: builder.query({
|
|
||||||
query: () => ({ url: `admin/login/config` }),
|
|
||||||
}),
|
|
||||||
updateLoginConfig: builder.mutation({
|
|
||||||
query: (data) => ({
|
|
||||||
url: `admin/login/config`,
|
|
||||||
method: "POST",
|
|
||||||
body: data,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
updateLogo: builder.mutation({
|
|
||||||
query: (data) => ({
|
|
||||||
headers: {
|
|
||||||
"content-type": "image/png",
|
|
||||||
},
|
|
||||||
url: `admin/system/organization/logo`,
|
|
||||||
method: "POST",
|
|
||||||
body: data,
|
|
||||||
}),
|
|
||||||
async onQueryStarted(data, { dispatch, queryFulfilled }) {
|
|
||||||
try {
|
|
||||||
await queryFulfilled;
|
|
||||||
dispatch(
|
|
||||||
updateInfo({
|
|
||||||
logo: `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
console.log("update server logo error");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
createInviteLink: builder.query({
|
|
||||||
query: (expired_in = defaultExpireDuration) => ({
|
|
||||||
headers: {
|
|
||||||
"content-type": "text/plain",
|
|
||||||
accept: "text/plain",
|
|
||||||
},
|
|
||||||
url: `/admin/system/create_invite_link?expired_in=${expired_in}`,
|
|
||||||
responseHandler: (response) => response.text(),
|
|
||||||
}),
|
|
||||||
transformResponse: (link) => {
|
|
||||||
// 替换掉域名
|
|
||||||
const invite = new URL(link);
|
|
||||||
return `${location.origin}${invite.pathname}${invite.search}${invite.hash}`;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
updateServer: builder.mutation({
|
|
||||||
query: (data) => ({
|
|
||||||
url: `admin/system/organization`,
|
|
||||||
method: "POST",
|
|
||||||
body: data,
|
|
||||||
}),
|
|
||||||
async onQueryStarted(data, { dispatch, queryFulfilled, getState }) {
|
|
||||||
const { name: prevName, description: prevDesc } = getState().server;
|
|
||||||
dispatch(updateInfo(data));
|
|
||||||
try {
|
|
||||||
await queryFulfilled;
|
|
||||||
} catch {
|
|
||||||
dispatch(updateInfo({ name: prevName, description: prevDesc }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
|
getThirdPartySecret: builder.query({
|
||||||
|
query: () => ({
|
||||||
|
// headers: {
|
||||||
|
// "content-type": "text/plain",
|
||||||
|
// accept: "text/plain",
|
||||||
|
// },
|
||||||
|
url: `/admin/system/third_party_secret`,
|
||||||
|
responseHandler: (response) => response.text()
|
||||||
|
}),
|
||||||
|
keepUnusedDataFor: 0
|
||||||
|
}),
|
||||||
|
updateThirdPartySecret: builder.mutation({
|
||||||
|
query: () => ({
|
||||||
|
url: `/admin/system/third_party_secret`,
|
||||||
|
method: "POST",
|
||||||
|
responseHandler: (response) => response.text()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
getMetrics: builder.query({
|
||||||
|
query: () => ({ url: `/admin/system/metrics` })
|
||||||
|
}),
|
||||||
|
getServerVersion: builder.query({
|
||||||
|
query: () => ({
|
||||||
|
headers: {
|
||||||
|
// "content-type": "text/plain",
|
||||||
|
accept: "text/plain"
|
||||||
|
},
|
||||||
|
url: `/admin/system/version`,
|
||||||
|
responseHandler: (response) => response.text()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
getFirebaseConfig: builder.query({
|
||||||
|
query: () => ({ url: `admin/fcm/config` })
|
||||||
|
}),
|
||||||
|
getGoogleAuthConfig: builder.query({
|
||||||
|
query: () => ({ url: `admin/google_auth/config` })
|
||||||
|
}),
|
||||||
|
updateGoogleAuthConfig: builder.mutation({
|
||||||
|
query: (data) => ({
|
||||||
|
url: `admin/google_auth/config`,
|
||||||
|
method: "POST",
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
getGithubAuthConfig: builder.query({
|
||||||
|
query: () => ({ url: `admin/github_auth/config` })
|
||||||
|
}),
|
||||||
|
updateGithubAuthConfig: builder.mutation({
|
||||||
|
query: (data) => ({
|
||||||
|
url: `admin/github_auth/config`,
|
||||||
|
method: "POST",
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
sendTestEmail: builder.mutation({
|
||||||
|
query: (data) => ({
|
||||||
|
url: `/admin/system/send_mail`,
|
||||||
|
method: "POST",
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
updateFirebaseConfig: builder.mutation({
|
||||||
|
query: (data) => ({
|
||||||
|
url: `admin/fcm/config`,
|
||||||
|
method: "POST",
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
getAgoraConfig: builder.query({
|
||||||
|
query: () => ({ url: `admin/agora/config` })
|
||||||
|
}),
|
||||||
|
updateAgoraConfig: builder.mutation({
|
||||||
|
query: (data) => ({
|
||||||
|
url: `admin/agora/config`,
|
||||||
|
method: "POST",
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
getSMTPConfig: builder.query({
|
||||||
|
query: () => ({ url: `admin/smtp/config` })
|
||||||
|
}),
|
||||||
|
getSMTPStatus: builder.query({
|
||||||
|
query: () => ({ url: `/admin/smtp/enabled` })
|
||||||
|
}),
|
||||||
|
updateSMTPConfig: builder.mutation({
|
||||||
|
query: (data) => ({
|
||||||
|
url: `admin/smtp/config`,
|
||||||
|
method: "POST",
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
getLoginConfig: builder.query({
|
||||||
|
query: () => ({ url: `admin/login/config` })
|
||||||
|
}),
|
||||||
|
updateLoginConfig: builder.mutation({
|
||||||
|
query: (data) => ({
|
||||||
|
url: `admin/login/config`,
|
||||||
|
method: "POST",
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
updateLogo: builder.mutation({
|
||||||
|
query: (data) => ({
|
||||||
|
headers: {
|
||||||
|
"content-type": "image/png"
|
||||||
|
},
|
||||||
|
url: `admin/system/organization/logo`,
|
||||||
|
method: "POST",
|
||||||
|
body: data
|
||||||
|
}),
|
||||||
|
async onQueryStarted(data, { dispatch, queryFulfilled }) {
|
||||||
|
try {
|
||||||
|
await queryFulfilled;
|
||||||
|
dispatch(
|
||||||
|
updateInfo({
|
||||||
|
logo: `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
console.log("update server logo error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
createInviteLink: builder.query({
|
||||||
|
query: (expired_in = defaultExpireDuration) => ({
|
||||||
|
headers: {
|
||||||
|
"content-type": "text/plain",
|
||||||
|
accept: "text/plain"
|
||||||
|
},
|
||||||
|
url: `/admin/system/create_invite_link?expired_in=${expired_in}`,
|
||||||
|
responseHandler: (response) => response.text()
|
||||||
|
}),
|
||||||
|
transformResponse: (link) => {
|
||||||
|
// 替换掉域名
|
||||||
|
const invite = new URL(link);
|
||||||
|
return `${location.origin}${invite.pathname}${invite.search}${invite.hash}`;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
updateServer: builder.mutation({
|
||||||
|
query: (data) => ({
|
||||||
|
url: `admin/system/organization`,
|
||||||
|
method: "POST",
|
||||||
|
body: data
|
||||||
|
}),
|
||||||
|
async onQueryStarted(data, { dispatch, queryFulfilled, getState }) {
|
||||||
|
const { name: prevName, description: prevDesc } = getState().server;
|
||||||
|
dispatch(updateInfo(data));
|
||||||
|
try {
|
||||||
|
await queryFulfilled;
|
||||||
|
} catch {
|
||||||
|
dispatch(updateInfo({ name: prevName, description: prevDesc }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
createAdmin: builder.mutation({
|
||||||
|
query: (data) => ({
|
||||||
|
url: `/admin/system/create_admin`,
|
||||||
|
method: "POST",
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
getInitialized: builder.query({
|
||||||
|
query: () => ({
|
||||||
|
url: `/admin/system/initialized`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
useGetServerVersionQuery,
|
useGetServerVersionQuery,
|
||||||
useGetGithubAuthConfigQuery,
|
useGetGithubAuthConfigQuery,
|
||||||
useUpdateGithubAuthConfigMutation,
|
useUpdateGithubAuthConfigMutation,
|
||||||
useGetGoogleAuthConfigQuery,
|
useGetGoogleAuthConfigQuery,
|
||||||
useUpdateGoogleAuthConfigMutation,
|
useUpdateGoogleAuthConfigMutation,
|
||||||
useGetSMTPStatusQuery,
|
useGetSMTPStatusQuery,
|
||||||
useSendTestEmailMutation,
|
useSendTestEmailMutation,
|
||||||
useUpdateFirebaseConfigMutation,
|
useUpdateFirebaseConfigMutation,
|
||||||
useGetFirebaseConfigQuery,
|
useGetFirebaseConfigQuery,
|
||||||
useGetLoginConfigQuery,
|
useGetLoginConfigQuery,
|
||||||
useUpdateLoginConfigMutation,
|
useUpdateLoginConfigMutation,
|
||||||
useGetSMTPConfigQuery,
|
useGetSMTPConfigQuery,
|
||||||
useUpdateSMTPConfigMutation,
|
useUpdateSMTPConfigMutation,
|
||||||
useGetAgoraConfigQuery,
|
useGetAgoraConfigQuery,
|
||||||
useUpdateAgoraConfigMutation,
|
useUpdateAgoraConfigMutation,
|
||||||
useGetServerQuery,
|
useGetServerQuery,
|
||||||
useGetMetricsQuery,
|
useGetMetricsQuery,
|
||||||
useLazyGetServerQuery,
|
useLazyGetServerQuery,
|
||||||
useUpdateServerMutation,
|
useUpdateServerMutation,
|
||||||
useUpdateLogoMutation,
|
useUpdateLogoMutation,
|
||||||
useCreateInviteLinkQuery,
|
useCreateInviteLinkQuery,
|
||||||
useLazyCreateInviteLinkQuery,
|
useLazyCreateInviteLinkQuery,
|
||||||
useGetThirdPartySecretQuery,
|
useGetThirdPartySecretQuery,
|
||||||
useUpdateThirdPartySecretMutation,
|
useUpdateThirdPartySecretMutation,
|
||||||
|
useCreateAdminMutation,
|
||||||
|
useGetInitializedQuery
|
||||||
} = serverApi;
|
} = serverApi;
|
||||||
|
|||||||
+69
-80
@@ -1,89 +1,78 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
import {
|
import { KEY_PWA_INSTALLED, KEY_REFRESH_TOKEN, KEY_TOKEN, KEY_UID, KEY_EXPIRE } from "../config";
|
||||||
KEY_PWA_INSTALLED,
|
|
||||||
KEY_REFRESH_TOKEN,
|
|
||||||
KEY_TOKEN,
|
|
||||||
KEY_UID,
|
|
||||||
KEY_EXPIRE,
|
|
||||||
} from "../config";
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
initialized: true,
|
initialized: true,
|
||||||
uid: null,
|
uid: null,
|
||||||
token: localStorage.getItem(KEY_TOKEN),
|
token: localStorage.getItem(KEY_TOKEN),
|
||||||
expireTime: localStorage.getItem(KEY_EXPIRE) || new Date().getTime(),
|
expireTime: localStorage.getItem(KEY_EXPIRE) || new Date().getTime(),
|
||||||
refreshToken: localStorage.getItem(KEY_REFRESH_TOKEN),
|
refreshToken: localStorage.getItem(KEY_REFRESH_TOKEN)
|
||||||
};
|
};
|
||||||
const emptyState = {
|
const emptyState = {
|
||||||
initialized: true,
|
initialized: true,
|
||||||
uid: null,
|
uid: null,
|
||||||
token: null,
|
token: null,
|
||||||
expireTime: new Date().getTime(),
|
expireTime: new Date().getTime(),
|
||||||
refreshToken: null,
|
refreshToken: null
|
||||||
};
|
};
|
||||||
const authDataSlice = createSlice({
|
const authDataSlice = createSlice({
|
||||||
name: "authData",
|
name: "authData",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setAuthData(state, action) {
|
setAuthData(state, action) {
|
||||||
const {
|
const {
|
||||||
initialized = true,
|
initialized = true,
|
||||||
user: { uid },
|
user: { uid },
|
||||||
token,
|
token,
|
||||||
refresh_token,
|
refresh_token,
|
||||||
expired_in = 0,
|
expired_in = 0
|
||||||
} = action.payload;
|
} = action.payload;
|
||||||
state.initialized = initialized;
|
state.initialized = initialized;
|
||||||
state.uid = uid;
|
state.uid = uid;
|
||||||
state.token = token;
|
state.token = token;
|
||||||
state.refreshToken = refresh_token;
|
state.refreshToken = refresh_token;
|
||||||
// 当前时间往后推expire时长
|
// 当前时间往后推expire时长
|
||||||
console.log("expire", expired_in);
|
console.log("expire", expired_in);
|
||||||
const expireTime = new Date().getTime() + Number(expired_in) * 1000;
|
const expireTime = new Date().getTime() + Number(expired_in) * 1000;
|
||||||
state.expireTime = expireTime;
|
state.expireTime = expireTime;
|
||||||
// set local data
|
// set local data
|
||||||
localStorage.setItem(KEY_EXPIRE, expireTime);
|
localStorage.setItem(KEY_EXPIRE, expireTime);
|
||||||
localStorage.setItem(KEY_TOKEN, token);
|
localStorage.setItem(KEY_TOKEN, token);
|
||||||
localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token);
|
localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token);
|
||||||
localStorage.setItem(KEY_UID, uid);
|
localStorage.setItem(KEY_UID, uid);
|
||||||
},
|
|
||||||
resetAuthData() {
|
|
||||||
console.log("clear auth data");
|
|
||||||
// remove local data
|
|
||||||
localStorage.removeItem(KEY_EXPIRE);
|
|
||||||
localStorage.removeItem(KEY_TOKEN);
|
|
||||||
localStorage.removeItem(KEY_REFRESH_TOKEN);
|
|
||||||
localStorage.removeItem(KEY_UID);
|
|
||||||
localStorage.removeItem(KEY_PWA_INSTALLED);
|
|
||||||
|
|
||||||
return emptyState;
|
|
||||||
},
|
|
||||||
setUid(state, action) {
|
|
||||||
const uid = action.payload;
|
|
||||||
state.uid = uid;
|
|
||||||
console.log("set uid orginal");
|
|
||||||
},
|
|
||||||
updateInitilize(state, action) {
|
|
||||||
const isInitized = action.payload;
|
|
||||||
state.initialized = isInitized;
|
|
||||||
},
|
|
||||||
updateToken(state, action) {
|
|
||||||
const { token, refresh_token, expired_in } = action.payload;
|
|
||||||
console.log("refresh token");
|
|
||||||
state.token = token;
|
|
||||||
const et = new Date().getTime() + Number(expired_in) * 1000;
|
|
||||||
state.expireTime = et;
|
|
||||||
state.refreshToken = refresh_token;
|
|
||||||
localStorage.setItem(KEY_EXPIRE, et);
|
|
||||||
localStorage.setItem(KEY_TOKEN, token);
|
|
||||||
localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
resetAuthData() {
|
||||||
|
console.log("clear auth data");
|
||||||
|
// remove local data
|
||||||
|
localStorage.removeItem(KEY_EXPIRE);
|
||||||
|
localStorage.removeItem(KEY_TOKEN);
|
||||||
|
localStorage.removeItem(KEY_REFRESH_TOKEN);
|
||||||
|
localStorage.removeItem(KEY_UID);
|
||||||
|
localStorage.removeItem(KEY_PWA_INSTALLED);
|
||||||
|
|
||||||
|
return emptyState;
|
||||||
|
},
|
||||||
|
setUid(state, action) {
|
||||||
|
const uid = action.payload;
|
||||||
|
state.uid = uid;
|
||||||
|
console.log("set uid orginal");
|
||||||
|
},
|
||||||
|
updateInitialized(state, action) {
|
||||||
|
const isInitialized = action.payload;
|
||||||
|
state.initialized = isInitialized;
|
||||||
|
},
|
||||||
|
updateToken(state, action) {
|
||||||
|
const { token, refresh_token, expired_in } = action.payload;
|
||||||
|
console.log("refresh token");
|
||||||
|
state.token = token;
|
||||||
|
const et = new Date().getTime() + Number(expired_in) * 1000;
|
||||||
|
state.expireTime = et;
|
||||||
|
state.refreshToken = refresh_token;
|
||||||
|
localStorage.setItem(KEY_EXPIRE, et);
|
||||||
|
localStorage.setItem(KEY_TOKEN, token);
|
||||||
|
localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
export const {
|
export const { updateInitialized, setAuthData, resetAuthData, setUid, updateToken } =
|
||||||
updateInitilize,
|
authDataSlice.actions;
|
||||||
setAuthData,
|
|
||||||
resetAuthData,
|
|
||||||
setUid,
|
|
||||||
updateToken,
|
|
||||||
} = authDataSlice.actions;
|
|
||||||
export default authDataSlice.reducer;
|
export default authDataSlice.reducer;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="17" height="18" viewBox="0 0 17 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.60846 1.61513C2.1087 1.3432 1.5 1.70497 1.5 2.27392V15.7262C1.5 16.2952 2.1087 16.6569 2.60846 16.385L14.97 9.65887C15.4921 9.37481 15.4921 8.62534 14.97 8.34128L2.60846 1.61513ZM0 2.27392C0 0.567069 1.82609 -0.518242 3.32538 0.297548L15.687 7.0237C17.2531 7.87588 17.2531 10.1243 15.687 10.9764L3.32538 17.7026C1.82609 18.5184 0 17.4331 0 15.7262V2.27392Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 489 B |
@@ -0,0 +1,91 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { nanoid } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
const StyledForm = styled.form`
|
||||||
|
> .option {
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> input[type="radio"] {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
& + .box {
|
||||||
|
width: 512px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #d0d5dd;
|
||||||
|
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all ease-in-out 250ms;
|
||||||
|
|
||||||
|
& > label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #667085;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all ease-in-out 250ms;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: inset 0 0 0 4px #ffffff;
|
||||||
|
border: 1px solid #d0d5dd;
|
||||||
|
margin: 14px 8px 14px 14px;
|
||||||
|
transition: all ease-in-out 500ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked + .box {
|
||||||
|
background: #22ccee;
|
||||||
|
border: 1px solid #d0d5dd;
|
||||||
|
|
||||||
|
& > label {
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: inset 0 0 0 4px #22ccee;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function Radio({ options, value = undefined, onChange = undefined }) {
|
||||||
|
const [innerValue, setInnerValue] = useState(0);
|
||||||
|
const id = useRef(nanoid());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledForm>
|
||||||
|
{options.map((item, index) => (
|
||||||
|
<div className="option" key={index}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={(value !== undefined ? value : innerValue) === index}
|
||||||
|
onChange={() => {
|
||||||
|
value === undefined && setInnerValue(index);
|
||||||
|
onChange !== null && onChange(index);
|
||||||
|
}}
|
||||||
|
id={`${id.current}-${index}`}
|
||||||
|
/>
|
||||||
|
<div className="box">
|
||||||
|
<label htmlFor={`${id.current}-${index}`}>{item}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</StyledForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
+102
-103
@@ -21,115 +21,114 @@ import store from "../app/store";
|
|||||||
import InvitePage from "./invite";
|
import InvitePage from "./invite";
|
||||||
import SettingPage from "./setting";
|
import SettingPage from "./setting";
|
||||||
import SettingChannelPage from "./settingChannel";
|
import SettingChannelPage from "./settingChannel";
|
||||||
|
import OnboardingPage from "./onboarding";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import ResourceManagement from "./resources";
|
import ResourceManagement from "./resources";
|
||||||
|
|
||||||
const PageRoutes = () => {
|
const PageRoutes = () => {
|
||||||
const {
|
const {
|
||||||
ui: { online },
|
ui: { online },
|
||||||
fileMessages,
|
fileMessages
|
||||||
} = useSelector((store) => {
|
} = useSelector((store) => {
|
||||||
return { ui: store.ui, fileMessages: store.fileMessage };
|
return { ui: store.ui, fileMessages: store.fileMessage };
|
||||||
});
|
});
|
||||||
// 掉线检测
|
// 掉线检测
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let toastId = 0;
|
let toastId = 0;
|
||||||
if (!online) {
|
if (!online) {
|
||||||
toast.error("Network Offline!", { duration: Infinity });
|
toast.error("Network Offline!", { duration: Infinity });
|
||||||
} else {
|
} else {
|
||||||
toast.dismiss(toastId);
|
toast.dismiss(toastId);
|
||||||
}
|
}
|
||||||
}, [online]);
|
}, [online]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/oauth/:token" element={<OAuthPage />} />
|
<Route path="/oauth/:token" element={<OAuthPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="/login"
|
path="/login"
|
||||||
element={
|
element={
|
||||||
<RequireNoAuth>
|
<RequireNoAuth>
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
</RequireNoAuth>
|
</RequireNoAuth>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/send_magic_link"
|
path="/send_magic_link"
|
||||||
element={
|
element={
|
||||||
<RequireNoAuth>
|
<RequireNoAuth>
|
||||||
<SendMagicLinkPage />
|
<SendMagicLinkPage />
|
||||||
</RequireNoAuth>
|
</RequireNoAuth>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/reg"
|
path="/reg"
|
||||||
element={
|
element={
|
||||||
<RequireNoAuth>
|
<RequireNoAuth>
|
||||||
<RegBasePage />
|
<RegBasePage />
|
||||||
</RequireNoAuth>
|
</RequireNoAuth>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route index element={<RegPage />} />
|
<Route index element={<RegPage />} />
|
||||||
<Route path="magiclink">
|
<Route path="magiclink">
|
||||||
<Route index element={<RegWithUsernamePage />} />
|
<Route index element={<RegWithUsernamePage />} />
|
||||||
<Route path=":token" element={<RegWithUsernamePage />} />
|
<Route path=":token" element={<RegWithUsernamePage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
path="/email_login"
|
path="/email_login"
|
||||||
element={
|
element={
|
||||||
<RequireNoAuth>
|
<RequireNoAuth>
|
||||||
<SendMagicLinkPage />
|
<SendMagicLinkPage />
|
||||||
</RequireNoAuth>
|
</RequireNoAuth>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="/invite" element={<InvitePage />} />
|
<Route path="/invite" element={<InvitePage />} />
|
||||||
<Route
|
<Route path="/onboarding" element={<OnboardingPage />} />
|
||||||
path="/"
|
<Route
|
||||||
element={
|
path="/"
|
||||||
<RequireAuth>
|
element={
|
||||||
<HomePage />
|
<RequireAuth>
|
||||||
</RequireAuth>
|
<HomePage />
|
||||||
}
|
</RequireAuth>
|
||||||
>
|
}
|
||||||
<Route path="setting">
|
>
|
||||||
<Route index element={<SettingPage />} />
|
<Route path="setting">
|
||||||
<Route path="channel/:cid" element={<SettingChannelPage />} />
|
<Route index element={<SettingPage />} />
|
||||||
</Route>
|
<Route path="channel/:cid" element={<SettingChannelPage />} />
|
||||||
<Route index element={<ChatPage />} />
|
</Route>
|
||||||
<Route path="chat">
|
<Route index element={<ChatPage />} />
|
||||||
<Route index element={<ChatPage />} />
|
<Route path="chat">
|
||||||
<Route path="channel/:channel_id" element={<ChatPage />} />
|
<Route index element={<ChatPage />} />
|
||||||
<Route path="dm/:user_id" element={<ChatPage />} />
|
<Route path="channel/:channel_id" element={<ChatPage />} />
|
||||||
</Route>
|
<Route path="dm/:user_id" element={<ChatPage />} />
|
||||||
<Route path="contacts">
|
</Route>
|
||||||
<Route index element={<ContactsPage />} />
|
<Route path="contacts">
|
||||||
<Route path=":user_id" element={<ContactsPage />} />
|
<Route index element={<ContactsPage />} />
|
||||||
</Route>
|
<Route path=":user_id" element={<ContactsPage />} />
|
||||||
<Route path="favs" element={<FavoritesPage />}></Route>
|
</Route>
|
||||||
<Route
|
<Route path="favs" element={<FavoritesPage />}></Route>
|
||||||
path="files"
|
<Route path="files" element={<ResourceManagement fileMessages={fileMessages} />}></Route>
|
||||||
element={<ResourceManagement fileMessages={fileMessages} />}
|
</Route>
|
||||||
></Route>
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Route>
|
</Routes>
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
</HashRouter>
|
||||||
</Routes>
|
);
|
||||||
</HashRouter>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
// const local_key = "AUTH_DATA";
|
// const local_key = "AUTH_DATA";
|
||||||
export default function ReduxRoutes() {
|
export default function ReduxRoutes() {
|
||||||
// const [authData, setAuthData] = useState(
|
// const [authData, setAuthData] = useState(
|
||||||
// JSON.parse(localStorage.getItem(local_key))
|
// JSON.parse(localStorage.getItem(local_key))
|
||||||
// );
|
// );
|
||||||
// const updateAuthData = (data) => {
|
// const updateAuthData = (data) => {
|
||||||
// localStorage.setItem(local_key, JSON.stringify(data));
|
// localStorage.setItem(local_key, JSON.stringify(data));
|
||||||
// setAuthData(data);
|
// setAuthData(data);
|
||||||
// };
|
// };
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Meta />
|
<Meta />
|
||||||
<PageRoutes />
|
<PageRoutes />
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import WelcomeStep from "./steps/1-welcome";
|
||||||
|
import ServerNameStep from "./steps/2-serverName";
|
||||||
|
import AdminCredentialsStep from "./steps/3-adminCredentials";
|
||||||
|
import InviteRuleStep from "./steps/4-inviteRule";
|
||||||
|
import InviteLinkStep from "./steps/5-inviteLink";
|
||||||
|
import CompletedStep from "./steps/6-completed";
|
||||||
|
import StyledOnboardingPage from "./styled";
|
||||||
|
|
||||||
|
export default function OnboardingPage() {
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
const [data, setData] = useState({
|
||||||
|
serverName: ""
|
||||||
|
});
|
||||||
|
const props = { setStep, data, setData };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>Rustchat Setup</title>
|
||||||
|
</Helmet>
|
||||||
|
<StyledOnboardingPage>
|
||||||
|
{step === 0 && <WelcomeStep {...props} />}
|
||||||
|
{step === 1 && <ServerNameStep {...props} />}
|
||||||
|
{step === 2 && <AdminCredentialsStep {...props} />}
|
||||||
|
{step === 3 && <InviteRuleStep {...props} />}
|
||||||
|
{step === 4 && <InviteLinkStep {...props} />}
|
||||||
|
{step === 5 && <CompletedStep {...props} />}
|
||||||
|
{step === 6 && <Navigate replace to="/" />}
|
||||||
|
</StyledOnboardingPage>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
import StyledButton from "../../../common/component/styled/Button";
|
||||||
|
import PlayIcon from "../../../assets/icons/play.svg?url";
|
||||||
|
|
||||||
|
const StyledFirstStep = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function WelcomeStep({ setStep }) {
|
||||||
|
return (
|
||||||
|
<StyledFirstStep>
|
||||||
|
<span className="primaryText">Welcome to your Rustchat!</span>
|
||||||
|
<span className="secondaryText">
|
||||||
|
Everything in this space is owned by you. Let’s set up your space!
|
||||||
|
</span>
|
||||||
|
<StyledButton className="startButton" onClick={() => setStep((prev) => prev + 1)}>
|
||||||
|
<img src={PlayIcon} alt="play icon" />
|
||||||
|
<span>Start</span>
|
||||||
|
</StyledButton>
|
||||||
|
</StyledFirstStep>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import StyledInput from "../../../common/component/styled/Input";
|
||||||
|
import StyledButton from "../../../common/component/styled/Button";
|
||||||
|
|
||||||
|
const StyledSpaceNameStep = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .secondaryText {
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function ServerNameStep({ setStep, data, setData }) {
|
||||||
|
return (
|
||||||
|
<StyledSpaceNameStep>
|
||||||
|
<span className="primaryText">Create a new server</span>
|
||||||
|
<span className="secondaryText">
|
||||||
|
Servers are shared environments where teams can work on projects and chat.
|
||||||
|
</span>
|
||||||
|
<StyledInput
|
||||||
|
className="input"
|
||||||
|
placeholder="Enter server name"
|
||||||
|
value={data.serverName}
|
||||||
|
onChange={(e) =>
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
serverName: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StyledButton
|
||||||
|
className="button"
|
||||||
|
onClick={() => {
|
||||||
|
// Verification for space name
|
||||||
|
if (data.serverName === "") {
|
||||||
|
toast.error("Please enter server name!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStep((prev) => prev + 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create Server
|
||||||
|
</StyledButton>
|
||||||
|
</StyledSpaceNameStep>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import StyledInput from "../../../common/component/styled/Input";
|
||||||
|
import StyledButton from "../../../common/component/styled/Button";
|
||||||
|
import {
|
||||||
|
useCreateAdminMutation,
|
||||||
|
useGetServerQuery,
|
||||||
|
useUpdateServerMutation
|
||||||
|
} from "../../../app/services/server";
|
||||||
|
import { useLoginMutation } from "../../../app/services/auth";
|
||||||
|
import { updateInitialized } from "../../../app/slices/auth.data";
|
||||||
|
|
||||||
|
const StyledAdminCredentialsStep = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .input:not(:nth-last-child(2)) {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function AdminCredentialsStep({ data, setStep }) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [createAdmin, { isLoading: isSigningUp, error: signUpError }] = useCreateAdminMutation();
|
||||||
|
const [login, { isLoading: isLoggingIn, isSuccess: isLoggedIn, error: loginError }] =
|
||||||
|
useLoginMutation();
|
||||||
|
const { data: serverData } = useGetServerQuery();
|
||||||
|
const [updateServer, { isLoading: isUpdatingServer, isSuccess: isUpdatedServer }] =
|
||||||
|
useUpdateServerMutation();
|
||||||
|
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirm, setConfirm] = useState("");
|
||||||
|
|
||||||
|
// Display error
|
||||||
|
useEffect(() => {
|
||||||
|
if (signUpError === undefined) return;
|
||||||
|
toast.error(`Failed to sign up: ${signUpError.data}`);
|
||||||
|
}, [signUpError]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (loginError === undefined) return;
|
||||||
|
toast.error(`Login failed: ${loginError.data}`);
|
||||||
|
}, [loginError]);
|
||||||
|
|
||||||
|
// After logged in
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedIn) {
|
||||||
|
dispatch(updateInitialized(true));
|
||||||
|
setTimeout(() => {
|
||||||
|
// Set server name
|
||||||
|
updateServer({
|
||||||
|
...serverData,
|
||||||
|
name: data.serverName
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
|
// After updated server
|
||||||
|
useEffect(() => {
|
||||||
|
if (isUpdatedServer) {
|
||||||
|
setStep((prev) => prev + 1);
|
||||||
|
}
|
||||||
|
}, [isUpdatedServer]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledAdminCredentialsStep>
|
||||||
|
<span className="primaryText">Now let’s set up your admin account</span>
|
||||||
|
<span className="secondaryText">You are the 1st user and admin of your space!</span>
|
||||||
|
<StyledInput
|
||||||
|
className="input"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<StyledInput
|
||||||
|
className="input"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<StyledInput
|
||||||
|
className="input"
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm your password"
|
||||||
|
value={confirm}
|
||||||
|
onChange={(e) => setConfirm(e.target.value)}
|
||||||
|
/>
|
||||||
|
<StyledButton
|
||||||
|
className="button"
|
||||||
|
onClick={async () => {
|
||||||
|
// Verification for admin credentials
|
||||||
|
if (email === "") {
|
||||||
|
toast.error("Please enter admin email!");
|
||||||
|
return;
|
||||||
|
} else if (password === "") {
|
||||||
|
toast.error("Please enter admin password!");
|
||||||
|
return;
|
||||||
|
} else if (password !== confirm) {
|
||||||
|
toast.error("Two passwords do not match!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await createAdmin({
|
||||||
|
email,
|
||||||
|
name: "Admin",
|
||||||
|
password,
|
||||||
|
gender: 0
|
||||||
|
});
|
||||||
|
await login({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
type: "password"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!(isSigningUp || isLoggingIn || isUpdatingServer) ? "Sign Up" : "..."}
|
||||||
|
</StyledButton>
|
||||||
|
</StyledAdminCredentialsStep>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import StyledRadio from "../../../common/component/styled/Radio";
|
||||||
|
import StyledButton from "../../../common/component/styled/Button";
|
||||||
|
import { useGetLoginConfigQuery, useUpdateLoginConfigMutation } from "../../../app/services/server";
|
||||||
|
|
||||||
|
const StyledInviteRuleStep = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .input:not(:nth-last-child(2)) {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function InviteRuleStep({ setStep }) {
|
||||||
|
const { data: loginConfig } = useGetLoginConfigQuery();
|
||||||
|
const [updateLoginConfig, { isSuccess, error }] = useUpdateLoginConfigMutation();
|
||||||
|
|
||||||
|
const [value, setValue] = useState(0);
|
||||||
|
|
||||||
|
// Display error
|
||||||
|
useEffect(() => {
|
||||||
|
if (error === undefined) return;
|
||||||
|
toast.error(`Failed to update invitation rule: ${error.data}`);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
// Increment `step` when updating has completed
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSuccess) setStep((prev) => prev + 1);
|
||||||
|
}, [isSuccess]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledInviteRuleStep>
|
||||||
|
<span className="primaryText">Last step: invite others!</span>
|
||||||
|
<span className="secondaryText">Firstly, who can sign up to this server?</span>
|
||||||
|
<StyledRadio
|
||||||
|
options={["Everyone", "Invitation link only"]}
|
||||||
|
value={value}
|
||||||
|
onChange={async (v) => {
|
||||||
|
setValue(v);
|
||||||
|
if (loginConfig !== undefined) {
|
||||||
|
const whoCanSignUp = ["EveryOne", "InvitationOnly"][v];
|
||||||
|
await updateLoginConfig({
|
||||||
|
...loginConfig,
|
||||||
|
who_can_sign_up: whoCanSignUp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<StyledButton className="button border_less ghost" onClick={() => setStep((prev) => prev + 1)}>
|
||||||
|
Skip
|
||||||
|
</StyledButton>
|
||||||
|
</StyledInviteRuleStep>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
import StyledInput from "../../../common/component/styled/Input";
|
||||||
|
import StyledButton from "../../../common/component/styled/Button";
|
||||||
|
import useInviteLink from "../../../common/hook/useInviteLink";
|
||||||
|
|
||||||
|
const StyledInviteLinkStep = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .secondaryText {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tip {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #475467;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .link {
|
||||||
|
position: relative;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #f4f4f5;
|
||||||
|
box-shadow: 0 1px 2px rgba(31, 41, 55, 0.08);
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 374px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> input {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 11px 0 11px 8px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #78787c;
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
padding: 0 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #22ccee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function InviteLinkStep({ setStep }) {
|
||||||
|
const { link, linkCopied, copyLink } = useInviteLink();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledInviteLinkStep>
|
||||||
|
<span className="primaryText">Last step: invite others!</span>
|
||||||
|
<span className="secondaryText">Now let’s invite others!</span>
|
||||||
|
<span className="tip">Send invitation link to your future community members:</span>
|
||||||
|
<div className="link">
|
||||||
|
<StyledInput className="large" readOnly placeholder="Generating" value={link} />
|
||||||
|
<StyledButton onClick={copyLink} className="ghost small border_less">
|
||||||
|
{linkCopied ? "Copied" : `Copy`}
|
||||||
|
</StyledButton>
|
||||||
|
</div>
|
||||||
|
<StyledButton className="button border_less ghost" onClick={() => setStep((prev) => prev + 1)}>
|
||||||
|
Skip
|
||||||
|
</StyledButton>
|
||||||
|
</StyledInviteLinkStep>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
import StyledButton from "../../../common/component/styled/Button";
|
||||||
|
import PlayIcon from "../../../assets/icons/play.svg?url";
|
||||||
|
|
||||||
|
const StyledLastStep = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .secondaryText {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tip {
|
||||||
|
width: 560px;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 24px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 96px;
|
||||||
|
|
||||||
|
> .strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function CompletedStep({ data, setStep }) {
|
||||||
|
return (
|
||||||
|
<StyledLastStep>
|
||||||
|
<span className="primaryText">Welcome to {data.serverName}</span>
|
||||||
|
<span className="secondaryText">Proudly presented by Rustchat</span>
|
||||||
|
<span className="tip">
|
||||||
|
More settings, including domain resolution, privileges, securities, and invites are available in{" "}
|
||||||
|
<span className="strong">Settings</span>
|
||||||
|
</span>
|
||||||
|
<StyledButton className="startButton" onClick={() => setStep((prev) => prev + 1)}>
|
||||||
|
<img src={PlayIcon} alt="play icon" />
|
||||||
|
<span>Enter</span>
|
||||||
|
</StyledButton>
|
||||||
|
</StyledLastStep>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const StyledOnboardingPage = styled.div`
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
// shared with child components
|
||||||
|
.primaryText,
|
||||||
|
.secondaryText {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primaryText {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 30px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryText {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startButton {
|
||||||
|
width: 128px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 0 12px;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
margin-bottom: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 360px;
|
||||||
|
height: 44px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.input {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #d0d5dd;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 360px;
|
||||||
|
|
||||||
|
&.ghost.border_less {
|
||||||
|
width: fit-content;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #98a2b3;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default StyledOnboardingPage;
|
||||||
@@ -9298,7 +9298,7 @@ utils-merge@1.0.1:
|
|||||||
|
|
||||||
uuid@^8.3.2:
|
uuid@^8.3.2:
|
||||||
version "8.3.2"
|
version "8.3.2"
|
||||||
resolved "https://mirrors.tencent.com/npm/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||||
|
|
||||||
v8-compile-cache-lib@^3.0.0:
|
v8-compile-cache-lib@^3.0.0:
|
||||||
|
|||||||
Reference in New Issue
Block a user