mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: support open api with webhooks
This commit is contained in:
@@ -43,6 +43,7 @@ const MenuBtnsPopup: React.FC<Props> = (props: Props) => {
|
||||
const handleSignOutBtnClick = async () => {
|
||||
await userService.doSignOut();
|
||||
locationService.replaceHistory("/signin");
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@@ -3,7 +3,8 @@ import appContext from "../stores/appContext";
|
||||
import { userService } from "../services";
|
||||
import utils from "../helpers/utils";
|
||||
import { validate, ValidatorConfig } from "../helpers/validator";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import useToggle from "../hooks/useToggle";
|
||||
import toastHelper from "./Toast";
|
||||
import showChangePasswordDialog from "./ChangePasswordDialog";
|
||||
import "../less/my-account-section.less";
|
||||
@@ -21,7 +22,9 @@ const MyAccountSection: React.FC<Props> = () => {
|
||||
const { userState } = useContext(appContext);
|
||||
const user = userState.user as Model.User;
|
||||
const [username, setUsername] = useState<string>(user.username);
|
||||
const [showConfirmUnbindGithubBtn, setShowConfirmUnbindGithubBtn] = useState(false);
|
||||
const resetBtnClickLoadingState = useLoading(false);
|
||||
const [showConfirmResetAPIBtn, toggleConfirmResetAPIBtn] = useToggle(false);
|
||||
const openAPIRoute = `${window.location.origin}/api/whs/memo/${user.openId}`;
|
||||
|
||||
const handleUsernameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const nextUsername = e.target.value as string;
|
||||
@@ -69,18 +72,23 @@ const MyAccountSection: React.FC<Props> = () => {
|
||||
showChangePasswordDialog();
|
||||
};
|
||||
|
||||
const handleUnbindGithubBtnClick = async () => {
|
||||
if (showConfirmUnbindGithubBtn) {
|
||||
try {
|
||||
await userService.removeGithubName();
|
||||
await userService.doSignIn();
|
||||
} catch (error: any) {
|
||||
toastHelper.error(error.message);
|
||||
}
|
||||
setShowConfirmUnbindGithubBtn(false);
|
||||
} else {
|
||||
setShowConfirmUnbindGithubBtn(true);
|
||||
const handleResetOpenIdBtnClick = async () => {
|
||||
if (!showConfirmResetAPIBtn) {
|
||||
toggleConfirmResetAPIBtn(true);
|
||||
return;
|
||||
}
|
||||
if (resetBtnClickLoadingState.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetBtnClickLoadingState.setLoading();
|
||||
try {
|
||||
await userService.resetOpenId();
|
||||
} catch (error) {
|
||||
// do nth
|
||||
}
|
||||
resetBtnClickLoadingState.setFinish();
|
||||
toggleConfirmResetAPIBtn(false);
|
||||
};
|
||||
|
||||
const handlePreventDefault = (e: React.MouseEvent) => {
|
||||
@@ -124,39 +132,17 @@ const MyAccountSection: React.FC<Props> = () => {
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/* Account Binding Settings: only can use for domain: memos.justsven.top */}
|
||||
<Only when={window.location.origin.includes("justsven.top")}>
|
||||
<div className="section-container connect-section-container">
|
||||
<p className="title-text">关联账号</p>
|
||||
<label className="form-label input-form-label">
|
||||
<span className="normal-text">GitHub:</span>
|
||||
{user.githubName ? (
|
||||
<>
|
||||
<a className="value-text" href={"https://github.com/" + user.githubName}>
|
||||
{user.githubName}
|
||||
</a>
|
||||
<span
|
||||
className={`btn-text unbind-btn ${showConfirmUnbindGithubBtn ? "final-confirm" : ""}`}
|
||||
onMouseLeave={() => setShowConfirmUnbindGithubBtn(false)}
|
||||
onClick={handleUnbindGithubBtnClick}
|
||||
>
|
||||
{showConfirmUnbindGithubBtn ? "确定取消绑定!" : "取消绑定"}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="value-text">空</span>
|
||||
<a
|
||||
className="btn-text link-btn"
|
||||
href="https://github.com/login/oauth/authorize?client_id=187ba36888f152b06612&scope=read:user,gist"
|
||||
>
|
||||
前往绑定
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
<div className="section-container openapi-section-container">
|
||||
<p className="title-text">Open API(实验性功能)</p>
|
||||
<p className="value-text">{openAPIRoute}</p>
|
||||
<span className={`reset-btn ${resetBtnClickLoadingState.isLoading ? "loading" : ""}`} onClick={handleResetOpenIdBtnClick}>
|
||||
{showConfirmResetAPIBtn ? "⚠️ 确定重置 API" : "重置 API"}
|
||||
</span>
|
||||
<div className="usage-guide-container">
|
||||
<p className="title-text">使用方法:</p>
|
||||
<pre>{`POST ${openAPIRoute}\nContent-type: application/json\n{\n "content": "Hello, #memos ${window.location.origin}"\n}`}</pre>
|
||||
</div>
|
||||
</Only>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -87,7 +87,7 @@ namespace api {
|
||||
});
|
||||
}
|
||||
|
||||
export function updateUserinfo(userinfo: Partial<{ username: string; password: string; githubName: string }>) {
|
||||
export function updateUserinfo(userinfo: Partial<{ username: string; password: string }>) {
|
||||
return request({
|
||||
method: "PATCH",
|
||||
url: "/api/user/me",
|
||||
@@ -95,6 +95,13 @@ namespace api {
|
||||
});
|
||||
}
|
||||
|
||||
export function resetOpenId() {
|
||||
return request<string>({
|
||||
method: "POST",
|
||||
url: "/api/user/open_id/new",
|
||||
});
|
||||
}
|
||||
|
||||
export function getMyMemos() {
|
||||
return request<Model.Memo[]>({
|
||||
method: "GET",
|
||||
|
@@ -11,6 +11,7 @@
|
||||
@bg-lightgray: #eaeaea;
|
||||
@bg-blue: #1337a3;
|
||||
@bg-yellow: yellow;
|
||||
@bg-red: #fcf0f0;
|
||||
@bg-light-blue: #eef3fe;
|
||||
@bg-paper-yellow: #fbf4de;
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
.account-section-container {
|
||||
> .form-label {
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
|
||||
&.username-label {
|
||||
> input {
|
||||
@@ -62,43 +62,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
.connect-section-container {
|
||||
> .form-label {
|
||||
height: 28px;
|
||||
.openapi-section-container {
|
||||
> .value-text {
|
||||
width: 100%;
|
||||
border: 1px solid lightgray;
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
> .value-text {
|
||||
max-width: 128px;
|
||||
min-height: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
> .reset-btn {
|
||||
margin-top: 4px;
|
||||
padding: 4px 8px;
|
||||
background-color: @bg-red;
|
||||
border: 1px solid red;
|
||||
color: red;
|
||||
border-radius: 4px;
|
||||
line-height: 1.6;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
> .btn-text {
|
||||
padding: 0 8px;
|
||||
margin-left: 12px;
|
||||
&.loading {
|
||||
opacity: 0.6;
|
||||
cursor: wait;
|
||||
}
|
||||
}
|
||||
|
||||
> .usage-guide-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
margin-top: 8px;
|
||||
|
||||
> .title-text {
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
> pre {
|
||||
background-color: @bg-whitegray;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 28px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.bind-btn {
|
||||
color: white;
|
||||
background-color: @text-green;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.unbind-btn {
|
||||
color: #d28653;
|
||||
background-color: @bg-lightgray;
|
||||
|
||||
&.final-confirm {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
> .form-label {
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
cursor: pointer;
|
||||
|
||||
> .icon-img {
|
||||
|
@@ -3,7 +3,6 @@ import api from "../helpers/api";
|
||||
import { validate, ValidatorConfig } from "../helpers/validator";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import { locationService, userService } from "../services";
|
||||
import Only from "../components/common/OnlyWhen";
|
||||
import toastHelper from "../components/Toast";
|
||||
import "../less/signin.less";
|
||||
|
||||
@@ -20,7 +19,7 @@ const Signin: React.FC<Props> = () => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [showAutoSigninAsGuest, setShowAutoSigninAsGuest] = useState(true);
|
||||
const signinBtnClickLoadingState = useLoading(false);
|
||||
const signinBtnsClickLoadingState = useLoading(false);
|
||||
const autoSigninAsGuestBtn = useRef<HTMLDivElement>(null);
|
||||
const signinBtn = useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -49,12 +48,8 @@ const Signin: React.FC<Props> = () => {
|
||||
setPassword(text);
|
||||
};
|
||||
|
||||
const handleSignUpBtnClick = async () => {
|
||||
toastHelper.info("注册已关闭");
|
||||
};
|
||||
|
||||
const handleSignInBtnClick = async () => {
|
||||
if (signinBtnClickLoadingState.isLoading) {
|
||||
const handleSigninBtnsClick = async (action: "signin" | "signup" = "signin") => {
|
||||
if (signinBtnsClickLoadingState.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,8 +66,11 @@ const Signin: React.FC<Props> = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
signinBtnClickLoadingState.setLoading();
|
||||
const actionFunc = api.signin;
|
||||
signinBtnsClickLoadingState.setLoading();
|
||||
let actionFunc = api.signin;
|
||||
if (action === "signup") {
|
||||
actionFunc = api.signup;
|
||||
}
|
||||
const { succeed, message } = await actionFunc(username, password);
|
||||
|
||||
if (!succeed && message) {
|
||||
@@ -90,11 +88,11 @@ const Signin: React.FC<Props> = () => {
|
||||
console.error(error);
|
||||
toastHelper.error("😟 " + error.message);
|
||||
}
|
||||
signinBtnClickLoadingState.setFinish();
|
||||
signinBtnsClickLoadingState.setFinish();
|
||||
};
|
||||
|
||||
const handleSwitchAccountSigninBtnClick = () => {
|
||||
if (signinBtnClickLoadingState.isLoading) {
|
||||
if (signinBtnsClickLoadingState.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,12 +100,12 @@ const Signin: React.FC<Props> = () => {
|
||||
};
|
||||
|
||||
const handleAutoSigninAsGuestBtnClick = async () => {
|
||||
if (signinBtnClickLoadingState.isLoading) {
|
||||
if (signinBtnsClickLoadingState.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
signinBtnClickLoadingState.setLoading();
|
||||
signinBtnsClickLoadingState.setLoading();
|
||||
const { succeed, message } = await api.signin("guest", "123456");
|
||||
|
||||
if (!succeed && message) {
|
||||
@@ -125,7 +123,7 @@ const Signin: React.FC<Props> = () => {
|
||||
console.error(error);
|
||||
toastHelper.error("😟 " + error.message);
|
||||
}
|
||||
signinBtnClickLoadingState.setFinish();
|
||||
signinBtnsClickLoadingState.setFinish();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -141,13 +139,13 @@ const Signin: React.FC<Props> = () => {
|
||||
<div className="quickly-btns-container">
|
||||
<div
|
||||
ref={autoSigninAsGuestBtn}
|
||||
className={`btn guest-signin ${signinBtnClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
className={`btn guest-signin ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
onClick={handleAutoSigninAsGuestBtnClick}
|
||||
>
|
||||
👉 快速登录进行体验
|
||||
</div>
|
||||
<div
|
||||
className={`btn ${signinBtnClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
className={`btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
onClick={handleSwitchAccountSigninBtnClick}
|
||||
>
|
||||
已有账号,我要自己登录
|
||||
@@ -167,32 +165,26 @@ const Signin: React.FC<Props> = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-footer-container">
|
||||
<div className="btns-container">
|
||||
<Only when={window.location.origin.includes("justsven.top")}>
|
||||
<a
|
||||
className="btn-text"
|
||||
href="https://github.com/login/oauth/authorize?client_id=187ba36888f152b06612&scope=read:user,gist"
|
||||
>
|
||||
Sign In with GitHub
|
||||
</a>
|
||||
</Only>
|
||||
</div>
|
||||
<div className="btns-container">{/* nth */}</div>
|
||||
<div className="btns-container">
|
||||
<button
|
||||
className={`btn ${signinBtnClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
className={`btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
onClick={handleAutoSigninAsGuestBtnClick}
|
||||
>
|
||||
体验一下
|
||||
</button>
|
||||
<span className="split-text">/</span>
|
||||
<button className="btn signup-btn disabled" onClick={handleSignUpBtnClick}>
|
||||
<button
|
||||
className={`btn signin-btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
onClick={() => handleSigninBtnsClick("signup")}
|
||||
>
|
||||
注册
|
||||
</button>
|
||||
<span className="split-text">/</span>
|
||||
<button
|
||||
className={`btn signin-btn ${signinBtnClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
ref={signinBtn}
|
||||
onClick={handleSignInBtnClick}
|
||||
className={`btn signin-btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
|
||||
onClick={() => handleSigninBtnsClick("signin")}
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
|
@@ -40,12 +40,6 @@ class UserService {
|
||||
});
|
||||
}
|
||||
|
||||
public async removeGithubName(): Promise<void> {
|
||||
await api.updateUserinfo({
|
||||
githubName: "",
|
||||
});
|
||||
}
|
||||
|
||||
public async checkPasswordValid(password: string): Promise<boolean> {
|
||||
const { data: isValid } = await api.checkPasswordValid(password);
|
||||
return isValid;
|
||||
@@ -56,6 +50,15 @@ class UserService {
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
public async resetOpenId(): Promise<string> {
|
||||
const { data: openId } = await api.resetOpenId();
|
||||
appStore.dispatch({
|
||||
type: "RESET_OPENID",
|
||||
payload: openId,
|
||||
});
|
||||
return openId;
|
||||
}
|
||||
}
|
||||
|
||||
const userService = new UserService();
|
||||
|
@@ -12,7 +12,12 @@ interface SignOutAction {
|
||||
payload: null;
|
||||
}
|
||||
|
||||
export type Actions = SignInAction | SignOutAction;
|
||||
interface ResetOpenIdAction {
|
||||
type: "RESET_OPENID";
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export type Actions = SignInAction | SignOutAction | ResetOpenIdAction;
|
||||
|
||||
export function reducer(state: State, action: Actions): State {
|
||||
switch (action.type) {
|
||||
@@ -26,6 +31,18 @@ export function reducer(state: State, action: Actions): State {
|
||||
user: null,
|
||||
};
|
||||
}
|
||||
case "RESET_OPENID": {
|
||||
if (!state.user) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
user: {
|
||||
...state.user,
|
||||
openId: action.payload,
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
|
2
web/src/types/models.d.ts
vendored
2
web/src/types/models.d.ts
vendored
@@ -7,7 +7,7 @@ declare namespace Model {
|
||||
|
||||
interface User extends BaseModel {
|
||||
username: string;
|
||||
githubName: string;
|
||||
openId: string;
|
||||
}
|
||||
|
||||
interface Memo extends BaseModel {
|
||||
|
Reference in New Issue
Block a user