This commit is contained in:
zerosoul
2022-06-06 22:22:06 +08:00
17 changed files with 1125 additions and 533 deletions
+1
View File
@@ -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
View File
@@ -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;
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+3
View File
@@ -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

+91
View File
@@ -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
View File
@@ -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>
); );
} }
+35
View File
@@ -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>
</>
);
}
+26
View File
@@ -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. Lets 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 lets 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 lets 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>
);
}
+75
View File
@@ -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;
+1 -1
View File
@@ -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: