Merge remote-tracking branch 'upstream/main' into refactor/typescript
# Conflicts: # src/app/services/server.ts # src/common/component/GoogleLoginButton.tsx # src/common/utils.tsx
This commit is contained in:
Vendored
+2
-1
@@ -25,5 +25,6 @@
|
||||
"emmet.triggerExpansionOnTab": true,
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
},
|
||||
"cSpell.words": ["btns", "oidc", "vocechat"]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## Refs
|
||||
|
||||
- preview demo: https://privoce.rustchat.com/
|
||||
- preview demo: https://privoce.voce.chat/
|
||||
- design: https://www.figma.com/file/EHnNr53kNmDWgUT86It6CH/UI
|
||||
- backend APIs: https://dev.rustchat.com/api/swagger
|
||||
- backend APIs: https://dev.voce.chat/api/swagger
|
||||
- text editor: https://plate.udecode.io/docs/installation
|
||||
- markdown editor: https://nhn.github.io/tui.editor/latest/
|
||||
- redux: [@reduxjs/toolkit](https://redux-toolkit.js.org/introduction/getting-started)
|
||||
|
||||
+3
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "rustchat-web",
|
||||
"name": "vocechat-web",
|
||||
"version": "0.2.14",
|
||||
"private": true,
|
||||
"homepage": "https://privoce.rustchat.com",
|
||||
"homepage": "https://privoce.voce.chat",
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.0.2",
|
||||
"@metamask/onboarding": "^1.0.1",
|
||||
@@ -43,6 +43,7 @@
|
||||
"linkify-plugin-mention": "^3.0.4",
|
||||
"linkifyjs": "^3.0.5",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"mini-css-extract-plugin": "^2.6.0",
|
||||
"pwa-badge": "^1.1.1",
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
privoce.rustchat.com
|
||||
privoce.voce.chat
|
||||
File diff suppressed because one or more lines are too long
@@ -15,12 +15,12 @@ self.addEventListener("notificationclick", function (event) {
|
||||
firstClient.focus();
|
||||
return;
|
||||
}
|
||||
const { rustchat_from_uid, rustchat_to_uid, rustchat_to_gid } = customData;
|
||||
const { vocechat_from_uid, vocechat_to_uid, vocechat_to_gid } = customData;
|
||||
let chatClient;
|
||||
let redirectPath = rustchat_to_uid
|
||||
? `/chat/dm/${rustchat_from_uid}`
|
||||
: rustchat_to_gid
|
||||
? `/chat/channel/${rustchat_to_gid}`
|
||||
let redirectPath = vocechat_to_uid
|
||||
? `/chat/dm/${vocechat_from_uid}`
|
||||
: vocechat_to_gid
|
||||
? `/chat/channel/${vocechat_to_gid}`
|
||||
: "";
|
||||
if (!redirectPath) {
|
||||
firstClient.focus();
|
||||
@@ -49,13 +49,13 @@ importScripts(
|
||||
|
||||
// Initialize the Firebase app in the service worker by passing the generated config
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDyJ6B1Ouenoha_gdGkBwIkBNStlwhlbO0",
|
||||
authDomain: "rustchat-develop.firebaseapp.com",
|
||||
projectId: "rustchat-develop",
|
||||
storageBucket: "rustchat-develop.appspot.com",
|
||||
messagingSenderId: "418687074928",
|
||||
appId: "1:418687074928:web:753286adbf239f5af9eab5",
|
||||
measurementId: "G-XV476KEC8P"
|
||||
apiKey: "AIzaSyCc3VuCJZgzQLIH2wrYdQzsUOc1DuZiIOA",
|
||||
authDomain: "vocechatdev.firebaseapp.com",
|
||||
projectId: "vocechatdev",
|
||||
storageBucket: "vocechatdev.appspot.com",
|
||||
messagingSenderId: "526613312184",
|
||||
appId: "1:526613312184:web:d13c92582baf470d487a4d",
|
||||
measurementId: "G-82RQ3YSCP7"
|
||||
};
|
||||
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Rustchat WebAPP</title>
|
||||
<title>VoceChat WebAPP</title>
|
||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.0/styles/github.min.css"> -->
|
||||
<style>
|
||||
html {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "RustChat",
|
||||
"name": "VoceChat",
|
||||
"short_name": "Your private chat APP",
|
||||
"icons": [
|
||||
{
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ checkBrowsers(paths.appPath, isInteractive)
|
||||
// version and md5 files
|
||||
fs.writeFileSync(`${buildFolder}/VERSION`, require("../package.json").version);
|
||||
const hash = md5File.sync(`${buildFolder}/VERSION`);
|
||||
fs.writeFileSync(`${buildFolder}/web.rustchat.md5`, hash);
|
||||
fs.writeFileSync(`${buildFolder}/web..md5`, hash);
|
||||
},
|
||||
(err) => {
|
||||
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === "true";
|
||||
|
||||
+20
-19
@@ -1,22 +1,22 @@
|
||||
// const BASE_URL = `${location.origin}/api`;
|
||||
const BASE_URL = `https://dev.rustchat.com/api`;
|
||||
export const CACHE_VERSION = `0.2.17`;
|
||||
export const CACHE_VERSION = `0.3.0`;
|
||||
export const ContentTypes = {
|
||||
text: "text/plain",
|
||||
markdown: "text/markdown",
|
||||
file: "rustchat/file",
|
||||
file: "vocechat/file",
|
||||
archive: "vocechat/archive",
|
||||
formData: "multipart/form-data",
|
||||
json: "application/json",
|
||||
archive: "rustchat/archive"
|
||||
json: "application/json"
|
||||
};
|
||||
export const firebaseConfig = {
|
||||
apiKey: "AIzaSyDyJ6B1Ouenoha_gdGkBwIkBNStlwhlbO0",
|
||||
authDomain: "rustchat-develop.firebaseapp.com",
|
||||
projectId: "rustchat-develop",
|
||||
storageBucket: "rustchat-develop.appspot.com",
|
||||
messagingSenderId: "418687074928",
|
||||
appId: "1:418687074928:web:753286adbf239f5af9eab5",
|
||||
measurementId: "G-XV476KEC8P"
|
||||
apiKey: "AIzaSyCc3VuCJZgzQLIH2wrYdQzsUOc1DuZiIOA",
|
||||
authDomain: "vocechatdev.firebaseapp.com",
|
||||
projectId: "vocechatdev",
|
||||
storageBucket: "vocechatdev.appspot.com",
|
||||
messagingSenderId: "526613312184",
|
||||
appId: "1:526613312184:web:d13c92582baf470d487a4d",
|
||||
measurementId: "G-82RQ3YSCP7"
|
||||
};
|
||||
export const ChatPrefixs = {
|
||||
channel: "#",
|
||||
@@ -27,14 +27,15 @@ export const vapidKey = `BGXCn-5YRXSFw38Q9lUKJ5bibL212-yIQn1pCvthGhp6_KwA29FO1Ax
|
||||
export const tokenHeader = "X-API-Key";
|
||||
export const FILE_SLICE_SIZE = 1000 * 200 * 8; //200kb
|
||||
export const FILE_IMAGE_SIZE = 1000 * 10000 * 8; //10mb
|
||||
export const KEY_TOKEN = "RUSTCHAT_TOKEN";
|
||||
export const KEY_EXPIRE = "RUSTCHAT_TOKEN_EXPIRE";
|
||||
export const KEY_REFRESH_TOKEN = "RUSTCHAT_REFRESH_TOKEN";
|
||||
export const KEY_UID = "RUSTCHAT_CURR_UID";
|
||||
export const KEY_DEVICE_KEY = "RUSTCHAT_DEVICE_KEY";
|
||||
export const KEY_USERS_VERSION = "RUSTCHAT_USERS_VERSION";
|
||||
export const KEY_AFTER_MID = "RUSTCHAT_AFTER_MID";
|
||||
export const KEY_PWA_INSTALLED = "RUSTCHAT_PWA_INSTALLED";
|
||||
export const KEY_TOKEN = "VOCECHAT_TOKEN";
|
||||
export const KEY_EXPIRE = "VOCECHAT_TOKEN_EXPIRE";
|
||||
export const KEY_REFRESH_TOKEN = "VOCECHAT_REFRESH_TOKEN";
|
||||
export const KEY_UID = "VOCECHAT_CURR_UID";
|
||||
export const KEY_DEVICE_KEY = "VOCECHAT_DEVICE_KEY";
|
||||
export const KEY_USERS_VERSION = "VOCECHAT_USERS_VERSION";
|
||||
export const KEY_AFTER_MID = "VOCECHAT_AFTER_MID";
|
||||
export const KEY_PWA_INSTALLED = "VOCECHAT_PWA_INSTALLED";
|
||||
export const KEY_LOCAL_MAGIC_TOKEN = "VOCECHAT_LOCAL_MAGIC_TOKEN";
|
||||
export const Emojis = ["👍", "❤️", "😄", "👀", "👎", "🎉", "🙁", "🚀"];
|
||||
export const Views = {
|
||||
item: "item",
|
||||
|
||||
+18
-13
@@ -2,8 +2,8 @@ import { createApi } from "@reduxjs/toolkit/query/react";
|
||||
import { nanoid } from "@reduxjs/toolkit";
|
||||
import baseQuery from "./base.query";
|
||||
import { setAuthData, updateToken, resetAuthData, updateInitialized } from "../slices/auth.data";
|
||||
import BASE_URL, { KEY_DEVICE_KEY } from "../config";
|
||||
import { AuthData } from "../../types/auth";
|
||||
import BASE_URL, { KEY_DEVICE_KEY, KEY_LOCAL_MAGIC_TOKEN } from "../config";
|
||||
import { AuthData, LoginCredential } from "../../types/auth";
|
||||
|
||||
const getDeviceId = () => {
|
||||
let d = localStorage.getItem(KEY_DEVICE_KEY);
|
||||
@@ -18,12 +18,12 @@ export const authApi = createApi({
|
||||
reducerPath: "authApi",
|
||||
baseQuery,
|
||||
endpoints: (builder) => ({
|
||||
login: builder.mutation<AuthData, string>({
|
||||
query: (credentials) => ({
|
||||
login: builder.mutation<AuthData, LoginCredential>({
|
||||
query: (credential) => ({
|
||||
url: "token/login",
|
||||
method: "POST",
|
||||
body: {
|
||||
credential: credentials,
|
||||
credential,
|
||||
device: getDeviceId(),
|
||||
device_token: "test"
|
||||
}
|
||||
@@ -32,13 +32,10 @@ export const authApi = createApi({
|
||||
const { avatar_updated_at } = data.user;
|
||||
return {
|
||||
...data,
|
||||
user: {
|
||||
...data.user,
|
||||
avatar:
|
||||
avatar_updated_at == 0
|
||||
? ""
|
||||
: `${BASE_URL}/resource/avatar?uid=${data.user.uid}&t=${avatar_updated_at}`
|
||||
}
|
||||
avatar:
|
||||
avatar_updated_at == 0
|
||||
? ""
|
||||
: `${BASE_URL}/resource/avatar?uid=${data.user.uid}&t=${avatar_updated_at}`
|
||||
};
|
||||
},
|
||||
async onQueryStarted(params, { dispatch, queryFulfilled }) {
|
||||
@@ -47,6 +44,8 @@ export const authApi = createApi({
|
||||
if (data) {
|
||||
dispatch(setAuthData(data));
|
||||
}
|
||||
// 从localstorage 去掉 magic token
|
||||
localStorage.removeItem(KEY_LOCAL_MAGIC_TOKEN);
|
||||
} catch {
|
||||
console.log("login error");
|
||||
}
|
||||
@@ -139,6 +138,11 @@ export const authApi = createApi({
|
||||
url: `/token/metamask/nonce?public_address=${address}`
|
||||
})
|
||||
}),
|
||||
checkEmail: builder.query({
|
||||
query: (email) => ({
|
||||
url: `/user/check_email?email=${encodeURIComponent(email)}`
|
||||
})
|
||||
}),
|
||||
getCredentials: builder.query({
|
||||
query: () => ({ url: "/token/credentials" })
|
||||
}),
|
||||
@@ -153,7 +157,7 @@ export const authApi = createApi({
|
||||
}
|
||||
}
|
||||
}),
|
||||
getInitialized: builder.query({
|
||||
getInitialized: builder.query<boolean, void>({
|
||||
query: () => ({ url: "/admin/system/initialized" }),
|
||||
async onQueryStarted(params, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
@@ -168,6 +172,7 @@ export const authApi = createApi({
|
||||
});
|
||||
|
||||
export const {
|
||||
useLazyCheckEmailQuery,
|
||||
useGetInitializedQuery,
|
||||
useSendLoginMagicLinkMutation,
|
||||
useSendRegMagicLinkMutation,
|
||||
|
||||
@@ -9,6 +9,7 @@ const whiteList = [
|
||||
"register",
|
||||
"sendLoginMagicLink",
|
||||
"sendRegMagicLink",
|
||||
"checkEmail",
|
||||
"checkMagicTokenValid",
|
||||
"getGoogleAuthConfig",
|
||||
"getGithubAuthConfig",
|
||||
@@ -76,16 +77,6 @@ const baseQueryWithTokenCheck = async (args, api, extraOptions) => {
|
||||
toast.error(`${api.endpoint}: Failed to fetch`);
|
||||
}
|
||||
break;
|
||||
case 404:
|
||||
{
|
||||
toast.error("Request Not Found");
|
||||
}
|
||||
break;
|
||||
case 500:
|
||||
{
|
||||
toast.error(result.error.data || "server error");
|
||||
}
|
||||
break;
|
||||
case 401:
|
||||
{
|
||||
if (api.endpoint !== "login") {
|
||||
@@ -93,15 +84,22 @@ const baseQueryWithTokenCheck = async (args, api, extraOptions) => {
|
||||
location.href = "/#/login";
|
||||
toast.error("API Not Authenticated");
|
||||
}
|
||||
// toast.error("token expired, please login again");
|
||||
// } else {
|
||||
// return;
|
||||
// }
|
||||
}
|
||||
break;
|
||||
case 403:
|
||||
toast.error("Request Not Allowed");
|
||||
break;
|
||||
case 404:
|
||||
{
|
||||
toast.error("Request Not Found");
|
||||
}
|
||||
break;
|
||||
case 500:
|
||||
{
|
||||
toast.error(result.error.data || "Server Error");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
+48
-49
@@ -1,26 +1,31 @@
|
||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||
import BASE_URL from "../config";
|
||||
import { updateInfo } from "../slices/server";
|
||||
import { updateInfo, StoredServer } from "../slices/server";
|
||||
import baseQuery from "./base.query";
|
||||
|
||||
import { RootState } from "../store";
|
||||
import { User } from "../../types/auth";
|
||||
import {
|
||||
FirebaseConfig,
|
||||
GoogleAuthConfig,
|
||||
LoginConfig,
|
||||
Server,
|
||||
TestEmailDTO,
|
||||
NewAdminDTO,
|
||||
SMTPConfig,
|
||||
AgoraConfig,
|
||||
GithubAuthConfig
|
||||
} from "../../types/server";
|
||||
const defaultExpireDuration = 7 * 24 * 60 * 60;
|
||||
|
||||
// todo: doc
|
||||
interface GetServerResponse {}
|
||||
interface GetThirdPartySecretResponse {}
|
||||
interface UpdateThirdPartySecretResponse {}
|
||||
// interface GetServerVersionResponse {}
|
||||
interface GetLoginConfigResponse {}
|
||||
|
||||
export const serverApi = createApi({
|
||||
reducerPath: "serverApi",
|
||||
baseQuery,
|
||||
endpoints: (builder) => ({
|
||||
getServer: builder.query<GetServerResponse, void>({
|
||||
getServer: builder.query<StoredServer, void>({
|
||||
query: () => ({ url: `admin/system/organization` }),
|
||||
transformResponse: (data) => {
|
||||
data.logo = `${BASE_URL}/resource/organization/logo?t=${+new Date()}`;
|
||||
return data;
|
||||
transformResponse: (data: Server) => {
|
||||
const logo = `${BASE_URL}/resource/organization/logo?t=${+new Date()}`;
|
||||
return { ...data, logo };
|
||||
},
|
||||
async onQueryStarted(data, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
@@ -31,98 +36,94 @@ export const serverApi = createApi({
|
||||
}
|
||||
}
|
||||
}),
|
||||
getThirdPartySecret: builder.query<GetThirdPartySecretResponse, void>({
|
||||
getThirdPartySecret: builder.query<string, void>({
|
||||
query: () => ({
|
||||
// headers: {
|
||||
// "content-type": "text/plain",
|
||||
// accept: "text/plain",
|
||||
// },
|
||||
url: `/admin/system/third_party_secret`,
|
||||
responseHandler: (response) => response.text()
|
||||
responseHandler: (response: Response) => response.text()
|
||||
}),
|
||||
keepUnusedDataFor: 0
|
||||
}),
|
||||
updateThirdPartySecret: builder.mutation<UpdateThirdPartySecretResponse, void>({
|
||||
updateThirdPartySecret: builder.mutation<string, void>({
|
||||
query: () => ({
|
||||
url: `/admin/system/third_party_secret`,
|
||||
method: "POST",
|
||||
responseHandler: (response) => response.text()
|
||||
responseHandler: (response: Response) => response.text()
|
||||
})
|
||||
}),
|
||||
getMetrics: builder.query({
|
||||
query: () => ({ url: `/admin/system/metrics` })
|
||||
}),
|
||||
getServerVersion: builder.query<string, void>({
|
||||
query: () => ({
|
||||
headers: { accept: "text/plain" },
|
||||
headers: {
|
||||
// "content-type": "text/plain",
|
||||
accept: "text/plain"
|
||||
},
|
||||
url: `/admin/system/version`,
|
||||
responseHandler: (response: Response) => response.text()
|
||||
})
|
||||
}),
|
||||
getFirebaseConfig: builder.query({
|
||||
getFirebaseConfig: builder.query<FirebaseConfig, void>({
|
||||
query: () => ({ url: `admin/fcm/config` })
|
||||
}),
|
||||
getGoogleAuthConfig: builder.query({
|
||||
getGoogleAuthConfig: builder.query<GoogleAuthConfig, void>({
|
||||
query: () => ({ url: `admin/google_auth/config` })
|
||||
}),
|
||||
updateGoogleAuthConfig: builder.mutation({
|
||||
updateGoogleAuthConfig: builder.mutation<void, GoogleAuthConfig>({
|
||||
query: (data) => ({
|
||||
url: `admin/google_auth/config`,
|
||||
method: "POST",
|
||||
body: data
|
||||
})
|
||||
}),
|
||||
getGithubAuthConfig: builder.query({
|
||||
getGithubAuthConfig: builder.query<GithubAuthConfig, void>({
|
||||
query: () => ({ url: `admin/github_auth/config` })
|
||||
}),
|
||||
updateGithubAuthConfig: builder.mutation({
|
||||
updateGithubAuthConfig: builder.mutation<void, GithubAuthConfig>({
|
||||
query: (data) => ({
|
||||
url: `admin/github_auth/config`,
|
||||
method: "POST",
|
||||
body: data
|
||||
})
|
||||
}),
|
||||
sendTestEmail: builder.mutation({
|
||||
sendTestEmail: builder.mutation<void, TestEmailDTO>({
|
||||
query: (data) => ({
|
||||
url: `/admin/system/send_mail`,
|
||||
method: "POST",
|
||||
body: data
|
||||
})
|
||||
}),
|
||||
updateFirebaseConfig: builder.mutation({
|
||||
updateFirebaseConfig: builder.mutation<void, FirebaseConfig>({
|
||||
query: (data) => ({
|
||||
url: `admin/fcm/config`,
|
||||
method: "POST",
|
||||
body: data
|
||||
})
|
||||
}),
|
||||
getAgoraConfig: builder.query({
|
||||
getAgoraConfig: builder.query<AgoraConfig, void>({
|
||||
query: () => ({ url: `admin/agora/config` })
|
||||
}),
|
||||
updateAgoraConfig: builder.mutation({
|
||||
updateAgoraConfig: builder.mutation<void, AgoraConfig>({
|
||||
query: (data) => ({
|
||||
url: `admin/agora/config`,
|
||||
method: "POST",
|
||||
body: data
|
||||
})
|
||||
}),
|
||||
getSMTPConfig: builder.query({
|
||||
getSMTPConfig: builder.query<SMTPConfig, void>({
|
||||
query: () => ({ url: `admin/smtp/config` })
|
||||
}),
|
||||
getSMTPStatus: builder.query({
|
||||
getSMTPStatus: builder.query<boolean, void>({
|
||||
query: () => ({ url: `/admin/smtp/enabled` })
|
||||
}),
|
||||
updateSMTPConfig: builder.mutation({
|
||||
updateSMTPConfig: builder.mutation<void, SMTPConfig>({
|
||||
query: (data) => ({
|
||||
url: `admin/smtp/config`,
|
||||
method: "POST",
|
||||
body: data
|
||||
})
|
||||
}),
|
||||
getLoginConfig: builder.query<GetLoginConfigResponse, void>({
|
||||
getLoginConfig: builder.query<LoginConfig, void>({
|
||||
query: () => ({ url: `admin/login/config` })
|
||||
}),
|
||||
updateLoginConfig: builder.mutation({
|
||||
updateLoginConfig: builder.mutation<void, LoginConfig>({
|
||||
query: (data) => ({
|
||||
url: `admin/login/config`,
|
||||
method: "POST",
|
||||
@@ -151,29 +152,30 @@ export const serverApi = createApi({
|
||||
}
|
||||
}
|
||||
}),
|
||||
createInviteLink: builder.query({
|
||||
createInviteLink: builder.query<string, number>({
|
||||
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()
|
||||
responseHandler: (response: Response) => response.text()
|
||||
}),
|
||||
transformResponse: (link) => {
|
||||
transformResponse: (link: string) => {
|
||||
// 替换掉域名
|
||||
const invite = new URL(link);
|
||||
return `${location.origin}${invite.pathname}${invite.search}${invite.hash}`;
|
||||
}
|
||||
}),
|
||||
updateServer: builder.mutation({
|
||||
updateServer: builder.mutation<void, Server>({
|
||||
query: (data) => ({
|
||||
url: `admin/system/organization`,
|
||||
method: "POST",
|
||||
body: data
|
||||
}),
|
||||
async onQueryStarted(data, { dispatch, queryFulfilled, getState }) {
|
||||
const { name: prevName, description: prevDesc } = getState().server;
|
||||
const rootStore = getState() as RootState;
|
||||
const { name: prevName, description: prevDesc } = rootStore.server;
|
||||
dispatch(updateInfo(data));
|
||||
try {
|
||||
await queryFulfilled;
|
||||
@@ -182,14 +184,14 @@ export const serverApi = createApi({
|
||||
}
|
||||
}
|
||||
}),
|
||||
createAdmin: builder.mutation({
|
||||
createAdmin: builder.mutation<User, NewAdminDTO>({
|
||||
query: (data) => ({
|
||||
url: `/admin/system/create_admin`,
|
||||
method: "POST",
|
||||
body: data
|
||||
})
|
||||
}),
|
||||
getInitialized: builder.query({
|
||||
getInitialized: builder.query<boolean, void>({
|
||||
query: () => ({
|
||||
url: `/admin/system/initialized`
|
||||
})
|
||||
@@ -210,15 +212,12 @@ export const {
|
||||
useLazyGetAgoraConfigQuery,
|
||||
useLazyGetSMTPConfigQuery,
|
||||
useLazyGetLoginConfigQuery,
|
||||
// useGetFirebaseConfigQuery,
|
||||
// useGetAgoraConfigQuery,
|
||||
useGetLoginConfigQuery,
|
||||
useUpdateLoginConfigMutation,
|
||||
useGetSMTPConfigQuery,
|
||||
useUpdateSMTPConfigMutation,
|
||||
useUpdateAgoraConfigMutation,
|
||||
useGetServerQuery,
|
||||
useGetMetricsQuery,
|
||||
useLazyGetServerQuery,
|
||||
useUpdateServerMutation,
|
||||
useUpdateLogoMutation,
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Server } from "../../types/server";
|
||||
|
||||
interface State {
|
||||
name: string;
|
||||
description: string;
|
||||
export interface StoredServer extends Server {
|
||||
logo: string;
|
||||
inviteLink: {
|
||||
inviteLink?: {
|
||||
link: string;
|
||||
expire: number;
|
||||
};
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
const initialState: StoredServer = {
|
||||
name: "",
|
||||
description: "",
|
||||
logo: "",
|
||||
@@ -27,7 +25,7 @@ const serverSlice = createSlice({
|
||||
resetServer() {
|
||||
return initialState;
|
||||
},
|
||||
fullfillServer(state, action: PayloadAction<State>) {
|
||||
fullfillServer(state, action: PayloadAction<StoredServer>) {
|
||||
const {
|
||||
inviteLink = {
|
||||
link: "",
|
||||
@@ -39,7 +37,7 @@ const serverSlice = createSlice({
|
||||
} = action.payload || {};
|
||||
return { name, logo, description, inviteLink };
|
||||
},
|
||||
updateInfo(state, action: PayloadAction<Partial<State>>) {
|
||||
updateInfo(state, action: PayloadAction<Partial<StoredServer>>) {
|
||||
const values = action.payload || {};
|
||||
// todo: check and remove old logic
|
||||
// Object.keys(values).forEach((_key) => {
|
||||
|
||||
+10
-10
@@ -12,20 +12,20 @@
|
||||
:root {
|
||||
/* border radius */
|
||||
--br: 8px;
|
||||
--rustchat-navs-bg: #e5e7eb;
|
||||
--rustchat-body-bg: #f5f6f7;
|
||||
--rustchat-chat-bg: #fff;
|
||||
--rustchat-channel-text-color: #1c1c1e;
|
||||
--rustchat-msg-text-color: #374151;
|
||||
---navs-bg: #e5e7eb;
|
||||
---body-bg: #f5f6f7;
|
||||
---chat-bg: #fff;
|
||||
---channel-text-color: #1c1c1e;
|
||||
---msg-text-color: #374151;
|
||||
}
|
||||
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
:root{
|
||||
--br:8px;
|
||||
--rustchat-navs-bg:#121926;
|
||||
--rustchat-body-bg:#1F2A37;
|
||||
--rustchat-chat-bg:#384250;
|
||||
--rustchat-channel-text-color:#fff;
|
||||
--rustchat-msg-text-color:#fff;
|
||||
---navs-bg:#121926;
|
||||
---body-bg:#1F2A37;
|
||||
---chat-bg:#384250;
|
||||
---channel-text-color:#fff;
|
||||
---msg-text-color:#fff;
|
||||
}
|
||||
} */
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { FC } from "react";
|
||||
import { useEffect, FC } from "react";
|
||||
import IconGithub from "../../assets/icons/github.svg";
|
||||
import styled from "styled-components";
|
||||
import { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
|
||||
|
||||
import Button from "./styled/Button";
|
||||
import { useLoginMutation } from "../../app/services/auth";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const StyledSocialButton = styled(Button)`
|
||||
width: 100%;
|
||||
@@ -20,26 +24,58 @@ const StyledSocialButton = styled(Button)`
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
config: {
|
||||
client_id: string;
|
||||
};
|
||||
}
|
||||
type Props = {
|
||||
client_id: string;
|
||||
type?: "login" | "register";
|
||||
};
|
||||
|
||||
const GithubLoginButton: FC<Props> = ({ config }) => {
|
||||
const { client_id } = config;
|
||||
const GithubLoginButton: FC<Props> = ({ type = "login", client_id }) => {
|
||||
//拿本地存的magic token
|
||||
const magic_token = localStorage.getItem(KEY_LOCAL_MAGIC_TOKEN);
|
||||
const [login, { isLoading, isSuccess, error }] = useLoginMutation();
|
||||
useEffect(() => {
|
||||
const query = new URLSearchParams(location.search);
|
||||
const isGithub = query.get("oauth") === "github";
|
||||
const code = query.get("code");
|
||||
if (isGithub && code) {
|
||||
login({
|
||||
magic_token,
|
||||
code: code,
|
||||
type: "github"
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
toast.success("Login Successfully");
|
||||
// navigateTo("/");
|
||||
}
|
||||
}, [isSuccess]);
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
switch (error.status) {
|
||||
case 410:
|
||||
toast.error(
|
||||
"No associated account found, please contact admin for an invitation link to join."
|
||||
);
|
||||
break;
|
||||
default:
|
||||
toast.error("Something Error");
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, [error]);
|
||||
const handleGithubLogin = () => {
|
||||
location.href = `https://github.com/login/oauth/authorize?client_id=${client_id}`;
|
||||
// console.log("github login");
|
||||
};
|
||||
// console.log("google login ", loaded);
|
||||
return (
|
||||
<StyledSocialButton onClick={handleGithubLogin}>
|
||||
<StyledSocialButton onClick={handleGithubLogin} disabled={isLoading}>
|
||||
<IconGithub className="icon" />
|
||||
Sign in with Github
|
||||
{/* {loaded ? `Sign in with Github` : `Initializing`} */}
|
||||
{` ${type === "login" ? "Sign in" : "Sign up"} with Github`}
|
||||
</StyledSocialButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default GithubLoginButton;
|
||||
|
||||
@@ -2,7 +2,8 @@ import { FC, useEffect } from "react";
|
||||
import { useGoogleLogin } from "react-google-login";
|
||||
import toast from "react-hot-toast";
|
||||
import styled from "styled-components";
|
||||
import googleSvg from "../../assets/icons/google.svg?url";
|
||||
import { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
|
||||
import IconGoogle from "../../assets/icons/google.svg";
|
||||
import Button from "./styled/Button";
|
||||
import { useLoginMutation } from "../../app/services/auth";
|
||||
|
||||
@@ -24,11 +25,13 @@ const StyledSocialButton = styled(Button)`
|
||||
|
||||
interface Props {
|
||||
clientId: string;
|
||||
type?: "login" | "register";
|
||||
}
|
||||
|
||||
const GoogleLoginButton: FC<Props> = ({ clientId }) => {
|
||||
const [login, { isSuccess, isLoading }] = useLoginMutation();
|
||||
|
||||
const GoogleLoginButton: FC<Props> = ({ type = "login", clientId }) => {
|
||||
const [login, { isSuccess, isLoading, error }] = useLoginMutation();
|
||||
//拿本地存的magic token
|
||||
const magic_token = localStorage.getItem(KEY_LOCAL_MAGIC_TOKEN);
|
||||
const { signIn, loaded } = useGoogleLogin({
|
||||
onScriptLoadFailure: (wtf) => {
|
||||
console.error("google login script load failure", wtf);
|
||||
@@ -37,6 +40,7 @@ const GoogleLoginButton: FC<Props> = ({ clientId }) => {
|
||||
onSuccess: ({ tokenId, ...rest }) => {
|
||||
console.info("google oauth success", tokenId, rest);
|
||||
login({
|
||||
magic_token,
|
||||
id_token: tokenId,
|
||||
type: "google"
|
||||
});
|
||||
@@ -52,6 +56,21 @@ const GoogleLoginButton: FC<Props> = ({ clientId }) => {
|
||||
// navigateTo("/");
|
||||
}
|
||||
}, [isSuccess]);
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
switch (error.status) {
|
||||
case 410:
|
||||
toast.error(
|
||||
"No associated account found, please contact admin for an invitation link to join."
|
||||
);
|
||||
break;
|
||||
default:
|
||||
toast.error("Something Error");
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, [error]);
|
||||
const handleGoogleLogin = () => {
|
||||
signIn();
|
||||
};
|
||||
@@ -59,8 +78,8 @@ const GoogleLoginButton: FC<Props> = ({ clientId }) => {
|
||||
// console.log("google login ", loaded);
|
||||
return (
|
||||
<StyledSocialButton disabled={!loaded || isLoading} onClick={handleGoogleLogin}>
|
||||
<img className="icon" src={googleSvg} alt="google icon" />
|
||||
{loaded ? "Sign in with Google" : "Initializing"}
|
||||
<IconGoogle className="icon" alt="google icon" />
|
||||
{loaded ? `${type === "login" ? "Sign in" : "Sign up"} with Google` : `Initializing`}
|
||||
</StyledSocialButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import Prompt from "./Prompt";
|
||||
import usePrompt from "./usePrompt";
|
||||
|
||||
export default function Manifest() {
|
||||
const { setCanneled, prompted } = usePrompt();
|
||||
const { setCanceled: setCanceled, prompted } = usePrompt();
|
||||
const deferredPromptRef = useRef(null);
|
||||
const [popup, setPopup] = useState(false);
|
||||
// const { data, isSuccess } = useGetServerQuery();
|
||||
@@ -57,7 +57,7 @@ export default function Manifest() {
|
||||
deferredPromptRef.current = null;
|
||||
};
|
||||
const handleClose = async () => {
|
||||
setCanneled();
|
||||
setCanceled();
|
||||
setPopup(false);
|
||||
};
|
||||
if (!popup || prompted) return null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "RustChat",
|
||||
"name": "VoceChat",
|
||||
"short_name": "Your private chat APP",
|
||||
"icons": [
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function usePrompt() {
|
||||
};
|
||||
|
||||
return {
|
||||
setCanneled: setPrompt,
|
||||
setCanceled: setPrompt,
|
||||
prompted: !!localStorage.getItem(KEY_PWA_INSTALLED),
|
||||
resetPrompt
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ import useUploadFile from "../../hook/useUploadFile";
|
||||
import Styled from "./styled";
|
||||
import { CONFIG } from "./config";
|
||||
import Contact from "../Contact";
|
||||
export const TEXT_EDITOR_PREFIX = "rustchat_text_editor";
|
||||
export const TEXT_EDITOR_PREFIX = "_text_editor";
|
||||
|
||||
let components = createPlateUI({
|
||||
// [ELEMENT_IMAGE]: ImageElement,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { isObjectEqual } from "../utils";
|
||||
import { isEqual } from "lodash";
|
||||
import {
|
||||
useUpdateLoginConfigMutation,
|
||||
useUpdateSMTPConfigMutation,
|
||||
@@ -101,7 +101,7 @@ export default function useConfig(config = "smtp") {
|
||||
useEffect(() => {
|
||||
// 空对象
|
||||
if (Object.keys(values).length == 0) return;
|
||||
if (!isObjectEqual(originalValue, values)) {
|
||||
if (!isEqual(originalValue, values)) {
|
||||
setChanged(true);
|
||||
} else {
|
||||
setChanged(false);
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function useMessageFeed({ context = "channel", id = null }) {
|
||||
}, [context, id]);
|
||||
useEffect(() => {
|
||||
if (items.length) {
|
||||
containerRef.current = document.querySelector(`#RUSTCHAT_FEED_${context}_${id}`);
|
||||
containerRef.current = document.querySelector(`#VOCECHAT_FEED_${context}_${id}`);
|
||||
if (containerRef.current) {
|
||||
const newScroll = containerRef.current.scrollHeight - containerRef.current.clientHeight;
|
||||
containerRef.current.scrollTop = curScrollPos + (newScroll - oldScroll);
|
||||
|
||||
@@ -219,7 +219,7 @@ export default function useStreaming() {
|
||||
const { gid, ...rest } = data;
|
||||
dispatch(
|
||||
updateChannel({
|
||||
id: gid,
|
||||
gid,
|
||||
...rest
|
||||
})
|
||||
);
|
||||
@@ -233,7 +233,7 @@ export default function useStreaming() {
|
||||
dispatch(
|
||||
updateChannel({
|
||||
operation: "add_member",
|
||||
id: gid,
|
||||
gid,
|
||||
members: uids
|
||||
})
|
||||
);
|
||||
@@ -242,13 +242,13 @@ export default function useStreaming() {
|
||||
case "user_leaved_group":
|
||||
{
|
||||
const { gid, uid: uids } = data;
|
||||
if (uids.findIndex((id) => id == loginUid) > -1) {
|
||||
if (uids.findIndex((uid) => uid == loginUid) > -1) {
|
||||
dispatch(removeChannel(gid));
|
||||
} else {
|
||||
dispatch(
|
||||
updateChannel({
|
||||
operation: "remove_member",
|
||||
id: gid,
|
||||
gid,
|
||||
members: uids
|
||||
})
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function useUploadFile(props: { context: string; id: string } | o
|
||||
if (!file) return;
|
||||
setData(null);
|
||||
const {
|
||||
name = `rustchat-${+new Date()}.${file.type.split("/")[1]}`,
|
||||
name = `-${+new Date()}.${file.type.split("/")[1]}`,
|
||||
type: file_type,
|
||||
size: file_size
|
||||
} = file;
|
||||
|
||||
+20
-61
@@ -11,27 +11,7 @@ export const isImage = (file_type = "", size = 0) => {
|
||||
return file_type.startsWith("image") && size <= FILE_IMAGE_SIZE;
|
||||
};
|
||||
|
||||
const deepSortObject = (obj: object): any => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj)
|
||||
.map((entry) => [
|
||||
entry[0],
|
||||
typeof entry[1] === "object" ? deepSortObject(entry[1]) : entry[1]
|
||||
])
|
||||
.sort()
|
||||
);
|
||||
};
|
||||
|
||||
export const isObjectEqual = (obj1: any, obj2: any) => {
|
||||
// Check for reference equal
|
||||
if (obj1 === obj2) return true;
|
||||
// Check for deep equal
|
||||
let o1 = JSON.stringify(deepSortObject(obj1 ?? {}));
|
||||
let o2 = JSON.stringify(deepSortObject(obj2 ?? {}));
|
||||
return o1 === o2;
|
||||
};
|
||||
|
||||
export const isTreatAsImage = (file: File | null) => {
|
||||
export const isTreatAsImage = (file) => {
|
||||
let isImage = false;
|
||||
if (!file) return isImage;
|
||||
const { type, size } = file;
|
||||
@@ -42,8 +22,8 @@ export const isTreatAsImage = (file: File | null) => {
|
||||
return isImage;
|
||||
};
|
||||
|
||||
export const getNonNullValues = (obj: any, whiteList = ["log_id"]): any => {
|
||||
const tmp: any = {};
|
||||
export const getNonNullValues = (obj, whiteList = ["log_id"]) => {
|
||||
const tmp = {};
|
||||
Object.keys(obj).forEach((k) => {
|
||||
if (!whiteList.includes(k) && obj[k] !== null) {
|
||||
tmp[k] = obj[k];
|
||||
@@ -51,20 +31,14 @@ export const getNonNullValues = (obj: any, whiteList = ["log_id"]): any => {
|
||||
});
|
||||
return tmp;
|
||||
};
|
||||
|
||||
interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export function getDefaultSize(size: Size | null = null, min: number = 480) {
|
||||
export function getDefaultSize(size = null, min = 480) {
|
||||
if (!size) return { width: 0, height: 0 };
|
||||
const { width: oWidth, height: oHeight } = size;
|
||||
if (oWidth == oHeight) {
|
||||
const tmp = min > oWidth ? oWidth : min;
|
||||
return { width: tmp, height: tmp };
|
||||
}
|
||||
const isVertical = oWidth <= oHeight;
|
||||
const isVertical = oWidth > oHeight ? false : true;
|
||||
let dWidth = 0;
|
||||
let dHeight = 0;
|
||||
if (isVertical) {
|
||||
@@ -76,8 +50,7 @@ export function getDefaultSize(size: Size | null = null, min: number = 480) {
|
||||
}
|
||||
return { width: dWidth, height: dHeight };
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number, decimals = 2) {
|
||||
export function formatBytes(bytes, decimals = 2) {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
const k = 1000;
|
||||
@@ -88,16 +61,9 @@ export function formatBytes(bytes: number, decimals = 2) {
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
export interface ImageSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const getImageSize = (url: string): Promise<ImageSize> => {
|
||||
const size: ImageSize = { width: 0, height: 0 };
|
||||
// todo: check inactive code
|
||||
// if (!url) return size;
|
||||
export const getImageSize = (url) => {
|
||||
const size = { width: 0, height: 0 };
|
||||
if (!url) return size;
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
@@ -111,15 +77,13 @@ export const getImageSize = (url: string): Promise<ImageSize> => {
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const getInitials = (name: string) => {
|
||||
export const getInitials = (name) => {
|
||||
const arr = name.split(" ").filter((n) => !!n);
|
||||
return arr
|
||||
.map((t) => t[0])
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
};
|
||||
|
||||
export const getInitialsAvatar = ({
|
||||
initials = "UK",
|
||||
initial_size = 0,
|
||||
@@ -138,7 +102,7 @@ export const getInitialsAvatar = ({
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
|
||||
const context = canvas.getContext("2d")!;
|
||||
const context = canvas.getContext("2d");
|
||||
context.scale(devicePixelRatio, devicePixelRatio);
|
||||
context.rect(0, 0, canvas.width, canvas.height);
|
||||
context.fillStyle = background;
|
||||
@@ -156,8 +120,12 @@ export const getInitialsAvatar = ({
|
||||
/* istanbul ignore next */
|
||||
return canvas.toDataURL("image/png");
|
||||
};
|
||||
|
||||
export function sliceFile(file: File | null, chunksAmount: number) {
|
||||
/**
|
||||
* @param {File|Blob} - file to slice
|
||||
* @param {Number} - chunksAmount
|
||||
* @return {Array} - an array of Blobs
|
||||
**/
|
||||
export function sliceFile(file, chunksAmount) {
|
||||
if (!file) return null;
|
||||
let byteIndex = 0;
|
||||
let chunks = [];
|
||||
@@ -170,8 +138,7 @@ export function sliceFile(file: File | null, chunksAmount: number) {
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
export const getFileIcon = (type: string, name: string = "") => {
|
||||
export const getFileIcon = (type, name = "") => {
|
||||
let icon = null;
|
||||
|
||||
const checks = {
|
||||
@@ -213,18 +180,10 @@ export const getFileIcon = (type: string, name: string = "") => {
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
|
||||
export const normalizeArchiveData = (
|
||||
data = null,
|
||||
filePath: string | null = null,
|
||||
uid: number | null = null
|
||||
) => {
|
||||
export const normalizeArchiveData = (data = null, filePath = null, uid = null) => {
|
||||
if (!data || !filePath) return [];
|
||||
const { messages, users } = data;
|
||||
const getUrls = (
|
||||
uid: number,
|
||||
{ content, content_type, file_id, thumbnail_id, filePath, avatar }
|
||||
) => {
|
||||
const getUrls = (uid, { content, content_type, file_id, thumbnail_id, filePath, avatar }) => {
|
||||
// uid存在,则favorite,否则archive
|
||||
const prefix = uid
|
||||
? `${BASE_URL}/favorite/attachment/${uid}/${filePath}/`
|
||||
|
||||
@@ -215,7 +215,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
|
||||
>
|
||||
<StyledChannelChat
|
||||
// ref={scrollObserveRef}
|
||||
id={`RUSTCHAT_FEED_channel_${cid}`}
|
||||
id={`VOCECHAT_FEED_channel_${cid}`}
|
||||
>
|
||||
{/* <div className="anchor"></div> */}
|
||||
{hasMore ? (
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function DMChat({ uid = "", dropFiles = [] }) {
|
||||
</StyledHeader>
|
||||
}
|
||||
>
|
||||
<StyledDMChat id={`RUSTCHAT_FEED_user_${uid}`}>
|
||||
<StyledDMChat id={`VOCECHAT_FEED_user_${uid}`}>
|
||||
{hasMore ? <LoadMore pullUp={pullUp} /> : null}
|
||||
{[...feeds].map((mid, idx) => {
|
||||
const curr = messageData[mid];
|
||||
|
||||
@@ -3,7 +3,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: var(--rustchat-navs-bg);
|
||||
background-color: var(---navs-bg);
|
||||
> .col {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function InvitePage() {
|
||||
<div className="form animate__animated animate__fadeInDown animate__faster">
|
||||
<div className="tips">
|
||||
<img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="logo" />
|
||||
<h2 className="title">Sign Up to Rustchat</h2>
|
||||
<h2 className="title">Sign Up to VoceChat</h2>
|
||||
<span className="desc">Please enter your details.</span>
|
||||
</div>
|
||||
<form onSubmit={handleReg}>
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function MetamaskLoginButton({ login }) {
|
||||
console.log("get sn");
|
||||
const signature = await ethereum.request({
|
||||
method: "personal_sign",
|
||||
params: [nonce, address, "hello from rustchat"]
|
||||
params: [nonce, address, "hello from "]
|
||||
});
|
||||
return signature;
|
||||
};
|
||||
|
||||
@@ -38,14 +38,6 @@ export default function LoginPage() {
|
||||
const exists = query.get("exists");
|
||||
if (oauth) {
|
||||
switch (oauth) {
|
||||
case "github":
|
||||
if (code) {
|
||||
login({
|
||||
code: code,
|
||||
type: "github"
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "oidc":
|
||||
if (code && state) {
|
||||
login({
|
||||
@@ -78,19 +70,18 @@ export default function LoginPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
console.log("login err", error);
|
||||
switch (error.status) {
|
||||
case "PARSING_ERROR":
|
||||
toast.error(error.data);
|
||||
break;
|
||||
case 401:
|
||||
toast.error("username or password incorrect");
|
||||
toast.error("Username or Password incorrect");
|
||||
break;
|
||||
case 404:
|
||||
toast.error("account not exsit");
|
||||
case 410:
|
||||
toast.error(
|
||||
"No associated account found, please contact admin for an invitation link to join."
|
||||
);
|
||||
break;
|
||||
default:
|
||||
toast.error("something error");
|
||||
toast.error("Something Error");
|
||||
break;
|
||||
}
|
||||
return;
|
||||
@@ -143,7 +134,7 @@ export default function LoginPage() {
|
||||
<div className="form">
|
||||
<div className="tips">
|
||||
<img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="logo" />
|
||||
<h2 className="title">Login to Rustchat</h2>
|
||||
<h2 className="title">Login to VoceChat</h2>
|
||||
<span className="desc">Please enter your details.</span>
|
||||
</div>
|
||||
<form onSubmit={handleLogin}>
|
||||
@@ -173,7 +164,7 @@ export default function LoginPage() {
|
||||
{hasDivider && <hr className="or" />}
|
||||
{enableMagicLink && <MagicLinkLogin />}
|
||||
{googleLogin && <GoogleLoginButton clientId={clientId} />}
|
||||
{enableGithubLogin && <GithubLoginButton config={githubAuthConfig} />}
|
||||
{enableGithubLogin && <GithubLoginButton client_id={githubAuthConfig?.client_id} />}
|
||||
{enableMetamaskLogin && <MetamaskLoginButton login={login} />}
|
||||
{oidc.length > 0 && <OidcLoginButton issuers={oidc} />}
|
||||
{whoCanSignUp === "EveryOne" && <SignUpLink />}
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function OnboardingPage() {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Rustchat Setup</title>
|
||||
<title>VoceChat Setup</title>
|
||||
</Helmet>
|
||||
<StyledOnboardingPage>
|
||||
<Navigator {...serverSetup} />
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function DonePage({ serverName }) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<span className="primaryText">Welcome to {serverName}</span>
|
||||
<span className="secondaryText">Proudly presented by Rustchat</span>
|
||||
<span className="secondaryText">Proudly presented by VoceChat</span>
|
||||
<span className="tip">
|
||||
More settings, including domain resolution, privileges, securities, and invites are
|
||||
available in <span className="strong">Settings</span>
|
||||
|
||||
@@ -46,7 +46,7 @@ const StyledWrapper = styled.div`
|
||||
export default function WelcomePage({ nextStep }) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<span className="primaryText">Welcome to your Rustchat!</span>
|
||||
<span className="primaryText">Welcome to your VoceChat!</span>
|
||||
<span className="secondaryText">
|
||||
Everything in this space is owned by you. Let’s set up your space!
|
||||
</span>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function ExpiredTip() {
|
||||
return (
|
||||
<Styled>
|
||||
<div className="title">Magic link expired</div>
|
||||
<p className="desc">Go back to your original Rustchat tab and request a new magic link.</p>
|
||||
<p className="desc">Go back to your original VoceChat tab and request a new magic link.</p>
|
||||
<p className="desc">You can close this window now.</p>
|
||||
</Styled>
|
||||
);
|
||||
|
||||
@@ -14,10 +14,14 @@ import { useRegisterMutation } from "../../app/services/auth";
|
||||
export default function RegWithUsername() {
|
||||
const [checkTokenInvalid, { data: isTokenValid, isLoading: checkingToken }] =
|
||||
useCheckMagicTokenValidMutation();
|
||||
const [login, { isLoading: loginLoading, error, isSuccess: loginSuccess, data: loginData }] =
|
||||
useLoginMutation();
|
||||
const [register, { isLoading: regLoading, isSuccess: regSuccess, data: regData }] =
|
||||
useRegisterMutation();
|
||||
const [
|
||||
login,
|
||||
{ isLoading: loginLoading, error: loginError, isSuccess: loginSuccess, data: loginData }
|
||||
] = useLoginMutation();
|
||||
const [
|
||||
register,
|
||||
{ isLoading: regLoading, isSuccess: regSuccess, data: regData, error: regError }
|
||||
] = useRegisterMutation();
|
||||
// const navigateTo = useNavigate();
|
||||
const { from = "reg" } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
@@ -32,8 +36,7 @@ export default function RegWithUsername() {
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("errr", error);
|
||||
switch (error?.status) {
|
||||
switch (loginError?.status) {
|
||||
case 401:
|
||||
toast.error("Invalided Token");
|
||||
break;
|
||||
@@ -41,7 +44,17 @@ export default function RegWithUsername() {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [error]);
|
||||
}, [loginError]);
|
||||
useEffect(() => {
|
||||
switch (regError?.status) {
|
||||
case 409:
|
||||
toast.error("Something Conflicted!");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [regError]);
|
||||
useEffect(() => {
|
||||
const isSuccess = loginSuccess || regSuccess;
|
||||
const data = loginData || regData;
|
||||
@@ -74,6 +87,7 @@ export default function RegWithUsername() {
|
||||
const { value } = evt.target;
|
||||
setUsername(value);
|
||||
};
|
||||
console.log("ffff", from);
|
||||
if (!token) return "No Token";
|
||||
if (checkingToken) return "Checking Magic Link...";
|
||||
if (!isTokenValid) return <ExpiredTip />;
|
||||
|
||||
+106
-41
@@ -1,13 +1,23 @@
|
||||
import { useState, useEffect, FormEvent, ChangeEvent } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
// import { useNavigate } from "react-router-dom";
|
||||
import BASE_URL, { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
|
||||
import Input from "../../common/component/styled/Input";
|
||||
import Button from "../../common/component/styled/Button";
|
||||
import { useSendRegMagicLinkMutation } from "../../app/services/auth";
|
||||
import { useLazyCheckEmailQuery, useSendRegMagicLinkMutation } from "../../app/services/auth";
|
||||
import EmailNextTip from "./EmailNextStepTip";
|
||||
import SignInLink from "./SignInLink";
|
||||
import { useGetLoginConfigQuery } from "../../app/services/server";
|
||||
import useGithubAuthConfig from "../../common/hook/useGithubAuthConfig";
|
||||
import useGoogleAuthConfig from "../../common/hook/useGoogleAuthConfig";
|
||||
import GoogleLoginButton from "../../common/component/GoogleLoginButton";
|
||||
import GithubLoginButton from "../../common/component/GithubLoginButton";
|
||||
|
||||
export default function Reg() {
|
||||
const [sendRegMagicLink, { isLoading, data, isSuccess }] = useSendRegMagicLinkMutation();
|
||||
const navigateTo = useNavigate();
|
||||
const [sendRegMagicLink, { isLoading: signingUp, data, isSuccess }] =
|
||||
useSendRegMagicLinkMutation();
|
||||
const [checkEmail, { isLoading: checkingEmail }] = useLazyCheckEmailQuery();
|
||||
// const navigateTo = useNavigate();
|
||||
const [magicToken, setMagicToken] = useState("");
|
||||
const [input, setInput] = useState({
|
||||
email: "",
|
||||
@@ -20,6 +30,8 @@ export default function Reg() {
|
||||
// const githubCode = query.get("gcode");
|
||||
const token = query.get("magic_token");
|
||||
if (token) {
|
||||
//本地存一下 magic token 后续oauth流程用到
|
||||
localStorage.setItem(KEY_LOCAL_MAGIC_TOKEN, token);
|
||||
setMagicToken(token);
|
||||
}
|
||||
}, []);
|
||||
@@ -29,23 +41,39 @@ export default function Reg() {
|
||||
const { new_magic_token, mail_is_sent } = data;
|
||||
if (!mail_is_sent && new_magic_token) {
|
||||
// 直接进入set_name流程
|
||||
navigateTo(`?magic_token=${new_magic_token}#/register/set_name`);
|
||||
location.href = `?magic_token=${new_magic_token}#/register/set_name`;
|
||||
// navigateTo(`/register/set_name?magic_token=${new_magic_token}`);
|
||||
}
|
||||
}
|
||||
}, [isSuccess, data]);
|
||||
|
||||
const handleReg = (evt: FormEvent<HTMLFormElement>) => {
|
||||
const handleReg = async (evt) => {
|
||||
evt.preventDefault();
|
||||
const { email, password } = input;
|
||||
sendRegMagicLink({
|
||||
magic_token: magicToken,
|
||||
email,
|
||||
password
|
||||
});
|
||||
const { email, password, confirmPassword } = input;
|
||||
if (password !== confirmPassword) {
|
||||
toast.error("Not Same Password!");
|
||||
return;
|
||||
}
|
||||
const { data: canReg } = await checkEmail(email);
|
||||
console.log("can reg", canReg);
|
||||
if (canReg) {
|
||||
sendRegMagicLink({
|
||||
magic_token: magicToken,
|
||||
email,
|
||||
password
|
||||
});
|
||||
} else {
|
||||
toast.error("Email already registered!");
|
||||
}
|
||||
// sendMagicLink(email);
|
||||
};
|
||||
|
||||
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const handleCompare = () => {
|
||||
const { password, confirmPassword } = input;
|
||||
if (password !== confirmPassword) {
|
||||
toast.error("Not Same Password!");
|
||||
}
|
||||
};
|
||||
const handleInput = (evt) => {
|
||||
const { type } = evt.target.dataset;
|
||||
const { value } = evt.target;
|
||||
// console.log(type, value);
|
||||
@@ -54,33 +82,70 @@ export default function Reg() {
|
||||
return { ...prev };
|
||||
});
|
||||
};
|
||||
|
||||
const { email, password } = input;
|
||||
const { clientId } = useGoogleAuthConfig();
|
||||
const { config: githubAuthConfig } = useGithubAuthConfig();
|
||||
const { data: loginConfig, isSuccess: loginConfigSuccess } = useGetLoginConfigQuery();
|
||||
if (!loginConfigSuccess) return null;
|
||||
const {
|
||||
github: enableGithubLogin,
|
||||
google: enableGoogleLogin,
|
||||
who_can_sign_up: whoCanSignUp
|
||||
} = loginConfig;
|
||||
const googleLogin = enableGoogleLogin && clientId;
|
||||
// magic token 没有并且没有开放注册
|
||||
if (whoCanSignUp !== "EveryOne" && !magicToken)
|
||||
return `Sign up method is updated to Invitation Link Only`;
|
||||
const { email, password, confirmPassword } = input;
|
||||
if (data?.mail_is_sent) return <EmailNextTip />;
|
||||
const isLoading = signingUp || checkingEmail;
|
||||
return (
|
||||
<form onSubmit={handleReg}>
|
||||
<Input
|
||||
className="large"
|
||||
name="email"
|
||||
value={email}
|
||||
required
|
||||
placeholder="Enter your email"
|
||||
data-type="email"
|
||||
onChange={handleInput}
|
||||
/>
|
||||
<Input
|
||||
className="large"
|
||||
type="password"
|
||||
value={password}
|
||||
name="password"
|
||||
required
|
||||
data-type="password"
|
||||
onChange={handleInput}
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading ? "Signing Up" : `Sign Up`}
|
||||
</Button>
|
||||
</form>
|
||||
<>
|
||||
<div className="tips">
|
||||
<img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="logo" />
|
||||
<h2 className="title">Sign Up to VoceChat</h2>
|
||||
<span className="desc">Please enter your details.</span>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleReg} autoSave={"false"} autoComplete={"true"}>
|
||||
<Input
|
||||
className="large"
|
||||
name="email"
|
||||
value={email}
|
||||
required
|
||||
placeholder="Enter email"
|
||||
data-type="email"
|
||||
onChange={handleInput}
|
||||
/>
|
||||
<Input
|
||||
className="large"
|
||||
type="password"
|
||||
value={password}
|
||||
name="password"
|
||||
required
|
||||
data-type="password"
|
||||
onChange={handleInput}
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
<Input
|
||||
required
|
||||
onBlur={handleCompare}
|
||||
type="password"
|
||||
name={"confirmPassword"}
|
||||
value={confirmPassword}
|
||||
data-type="confirmPassword"
|
||||
onChange={handleInput}
|
||||
placeholder="Confirm Password"
|
||||
></Input>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading ? "Signing Up" : `Sign Up`}
|
||||
</Button>
|
||||
</form>
|
||||
<hr className="or" />
|
||||
{googleLogin && <GoogleLoginButton type="register" clientId={clientId} />}
|
||||
{enableGithubLogin && (
|
||||
<GithubLoginButton type="register" client_id={githubAuthConfig?.client_id} />
|
||||
)}
|
||||
<SignInLink />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,13 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { useMatch } from "react-router-dom";
|
||||
import BASE_URL from "../../app/config";
|
||||
import SignInLink from "./SignInLink";
|
||||
import { useGetLoginConfigQuery } from "../../app/services/server";
|
||||
import useGithubAuthConfig from "../../common/hook/useGithubAuthConfig";
|
||||
import useGoogleAuthConfig from "../../common/hook/useGoogleAuthConfig";
|
||||
import GoogleLoginButton from "../../common/component/GoogleLoginButton";
|
||||
import GithubLoginButton from "../../common/component/GithubLoginButton";
|
||||
import StyledWrapper from "./styled";
|
||||
|
||||
export default function Reg() {
|
||||
const isRegHome = useMatch(`/register`);
|
||||
const { clientId } = useGoogleAuthConfig();
|
||||
const { config: githubAuthConfig } = useGithubAuthConfig();
|
||||
const { data: loginConfig, isSuccess: loginConfigSuccess } = useGetLoginConfigQuery();
|
||||
if (!loginConfigSuccess) return null;
|
||||
const {
|
||||
github: enableGithubLogin,
|
||||
google: enableGoogleLogin,
|
||||
who_can_sign_up: whoCanSignUp
|
||||
} = loginConfig;
|
||||
const googleLogin = enableGoogleLogin && clientId;
|
||||
// 没有开放注册
|
||||
if (whoCanSignUp !== "EveryOne") return `Open Register is Closed!`;
|
||||
export default function RegContainer() {
|
||||
// const isRegHome = useMatch(`/register`);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="form">
|
||||
{isRegHome && (
|
||||
<>
|
||||
<div className="tips">
|
||||
<img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="logo" />
|
||||
<h2 className="title">Sign Up to Rustchat</h2>
|
||||
<span className="desc">Please enter your details.</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Outlet />
|
||||
{isRegHome && (
|
||||
<>
|
||||
<hr className="or" />
|
||||
{googleLogin && <GoogleLoginButton clientId={clientId} />}
|
||||
{enableGithubLogin && <GithubLoginButton config={githubAuthConfig} />}
|
||||
<SignInLink />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ const checkFilter = (data, filter, channelMessage) => {
|
||||
return selected;
|
||||
};
|
||||
|
||||
let msnry = null;
|
||||
let msnry: typeof Masonry | null;
|
||||
function ResourceManagement({ fileMessages }) {
|
||||
const listContainerRef = useRef(null);
|
||||
const [filter, setFilter] = useState({});
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function SendMagicLinkPage() {
|
||||
<>
|
||||
<div className="tips">
|
||||
<img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="logo" />
|
||||
<h2 className="title">Login to Rustchat</h2>
|
||||
<h2 className="title">Login to VoceChat</h2>
|
||||
<span className="desc">Please enter your Email</span>
|
||||
</div>
|
||||
<form onSubmit={handleLogin}>
|
||||
|
||||
@@ -99,8 +99,8 @@ export default function APIConfig() {
|
||||
<Button>Update Secret</Button>
|
||||
</Tippy>
|
||||
<div className="tip">
|
||||
Tip: The security key agreed between the rustchat server and the third-party app is used to
|
||||
encrypt the communication data.{" "}
|
||||
Tip: The security key agreed between the server and the third-party app is used to encrypt
|
||||
the communication data.{" "}
|
||||
</div>
|
||||
</Styled>
|
||||
);
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function Logins() {
|
||||
<div className="title">
|
||||
<div className="txt">
|
||||
<Label>Google</Label>
|
||||
<Tooltip link="https://doc.rustchat.com/en-us/login-google.html" />
|
||||
<Tooltip link="https://doc.voce.chat/en-us/login-google.html" />
|
||||
</div>
|
||||
<span className="desc">Allows members login with Google.</span>
|
||||
</div>
|
||||
@@ -130,7 +130,7 @@ export default function Logins() {
|
||||
<div className="title">
|
||||
<div className="txt">
|
||||
<Label>Github</Label>
|
||||
<Tooltip link="https://doc.rustchat.com/en-us/login-github.html" />
|
||||
<Tooltip link="https://doc.voce.chat/en-us/login-github.html" />
|
||||
</div>
|
||||
<span className="desc">Allows members login with Github.</span>
|
||||
</div>
|
||||
@@ -161,7 +161,7 @@ export default function Logins() {
|
||||
<div className="title">
|
||||
<div className="txt">
|
||||
<Label>Metamask</Label>
|
||||
<Tooltip link="https://doc.rustchat.com/en-us/login-metamask.html" />
|
||||
<Tooltip link="https://doc.voce.chat/en-us/login-metamask.html" />
|
||||
</div>
|
||||
<span className="desc">Allows members login with Metamask.</span>
|
||||
</div>
|
||||
@@ -176,7 +176,7 @@ export default function Logins() {
|
||||
<div className="title">
|
||||
<div className="txt">
|
||||
<Label htmlFor="desc">OIDC</Label>
|
||||
<Tooltip link="https://doc.rustchat.com/en-us/login-webid.html" />
|
||||
<Tooltip link="https://doc.voce.chat/en-us/login-webid.html" />
|
||||
</div>
|
||||
<span className="desc">Save my login details for next time.</span>
|
||||
</div>
|
||||
|
||||
@@ -125,7 +125,7 @@ export default function ConfigSMTP() {
|
||||
<div className="tip">
|
||||
<img src={iconQuestion} alt="question icon" />
|
||||
<a
|
||||
href="https://rustchat.com/doc/smtp-setting"
|
||||
href="https://voce.chat/doc/smtp-setting"
|
||||
target="_blank"
|
||||
className="link"
|
||||
rel="noreferrer"
|
||||
|
||||
@@ -23,3 +23,49 @@ export interface AuthData extends AuthToken {
|
||||
initialized?: boolean;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export type PasswordCredential = {
|
||||
email: string;
|
||||
password: string;
|
||||
type: "password";
|
||||
};
|
||||
export type MagicLinkCredential = {
|
||||
magic_token: string;
|
||||
extra_name?: string;
|
||||
type: "magiclink";
|
||||
};
|
||||
export type GoogleCredential = {
|
||||
id_token: string;
|
||||
magic_token?: string | null;
|
||||
type: "google";
|
||||
};
|
||||
export type GithubCredential = {
|
||||
code: string;
|
||||
magic_token?: string | null;
|
||||
type: "github";
|
||||
};
|
||||
export type OIDCCredential = {
|
||||
code: string;
|
||||
state: string;
|
||||
magic_token?: string | null;
|
||||
type: "oidc";
|
||||
};
|
||||
export type MetamaskCredential = {
|
||||
public_address: string;
|
||||
nonce: string;
|
||||
signature: string;
|
||||
magic_token?: string | null;
|
||||
type: "metamask";
|
||||
};
|
||||
export type ThirdPartyCredential = {
|
||||
key: string;
|
||||
type: "thirdparty";
|
||||
};
|
||||
export type LoginCredential =
|
||||
| PasswordCredential
|
||||
| OIDCCredential
|
||||
| MetamaskCredential
|
||||
| GithubCredential
|
||||
| GoogleCredential
|
||||
| ThirdPartyCredential
|
||||
| MagicLinkCredential;
|
||||
|
||||
@@ -2,7 +2,7 @@ export interface ChannelMember {}
|
||||
|
||||
export interface Message {}
|
||||
|
||||
export type ContentType = "text/plain" | "text/markdown" | "rustchat/file" | "rustchat/archive";
|
||||
export type ContentType = "text/plain" | "text/markdown" | "vocechat/file" | "vocechat/archive";
|
||||
|
||||
export interface PinnedMessage {
|
||||
mid: number;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { User } from "./auth";
|
||||
export interface Server {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
export interface GithubAuthConfig {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
}
|
||||
export interface FirebaseConfig {
|
||||
enabled: boolean;
|
||||
token_url: string;
|
||||
project_id: string;
|
||||
private_key: string;
|
||||
client_email: string;
|
||||
}
|
||||
export interface AgoraConfig {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
project_id: string;
|
||||
app_id: string;
|
||||
app_certificate: string;
|
||||
rtm_key: string;
|
||||
rtm_secret: string;
|
||||
}
|
||||
export interface SMTPConfig {
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
from: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface GoogleAuthConfig {
|
||||
client_id: string;
|
||||
}
|
||||
export interface TestEmailDTO {
|
||||
to: string;
|
||||
content: string;
|
||||
subject: string;
|
||||
}
|
||||
export type WhoCanSignUp = "EveryOne" | "InvitationOnly";
|
||||
export type OIDCSetting = {
|
||||
enable: boolean;
|
||||
favicon: string;
|
||||
domain: string;
|
||||
};
|
||||
export interface LoginConfig {
|
||||
who_can_sign_up: WhoCanSignUp;
|
||||
password: boolean;
|
||||
magic_link: boolean;
|
||||
google: boolean;
|
||||
github: boolean;
|
||||
oidc: OIDCSetting[];
|
||||
metamask: boolean;
|
||||
third_party: boolean;
|
||||
}
|
||||
export interface NewAdminDTO extends Pick<User, "email" | "name" | "gender"> {
|
||||
password: string;
|
||||
}
|
||||
Reference in New Issue
Block a user