feat: support open api with webhooks

This commit is contained in:
email 2022-01-09 11:44:12 +08:00
parent aed1004f39
commit 9bec29a03e
17 changed files with 238 additions and 330 deletions

@ -1,14 +1,9 @@
package api package api
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt"
"io/ioutil"
"memos/api/e" "memos/api/e"
"memos/config"
"memos/store" "memos/store"
"memos/utils"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -38,7 +33,7 @@ func handleUserSignUp(w http.ResponseWriter, r *http.Request) {
return return
} }
user, err := store.CreateNewUser(userSignup.Username, userSignup.Password, "") user, err := store.CreateNewUser(userSignup.Username, userSignup.Password)
if err != nil { if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error()) e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
@ -107,127 +102,6 @@ func handleUserSignOut(w http.ResponseWriter, r *http.Request) {
}) })
} }
func handleGithubAuthCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
requestBody := map[string]string{
"client_id": config.GITHUB_CLIENTID,
"client_secret": config.GITHUB_SECRET,
"code": code,
}
requestJSON, _ := json.Marshal(requestBody)
// POST request to get access_token
req, err := http.NewRequest(
"POST",
"https://github.com/login/oauth/access_token",
bytes.NewBuffer(requestJSON),
)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Error in request github api")
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Error in request github api")
return
}
// Response body converted to stringified JSON
respBody, _ := ioutil.ReadAll(resp.Body)
// Represents the response received from Github
type GithubAccessTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
}
ghResp := GithubAccessTokenResponse{}
json.Unmarshal(respBody, &ghResp)
githubAccessToken := ghResp.AccessToken
// Get request to a set URL
req, err = http.NewRequest(
"GET",
"https://api.github.com/user",
nil,
)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Error in request github api")
return
}
authorizationHeaderValue := fmt.Sprintf("token %s", githubAccessToken)
req.Header.Set("Authorization", authorizationHeaderValue)
resp, err = http.DefaultClient.Do(req)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Error in request github api")
return
}
respBody, _ = ioutil.ReadAll(resp.Body)
githubData := string(respBody)
type GithubUser struct {
Login string `json:"login"`
Name string `json:"name"`
}
githubUser := GithubUser{}
json.Unmarshal([]byte(githubData), &githubUser)
session, _ := SessionStore.Get(r, "session")
userId := fmt.Sprintf("%v", session.Values["user_id"])
if userId != "" {
githubNameUsable, err := store.CheckGithubNameUsable(githubUser.Login)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", "Error in CheckGithubNameUsable")
return
}
if !githubNameUsable {
e.ErrorHandler(w, "DATABASE_ERROR", "Error in CheckGithubNameUsable")
return
}
userPatch := store.UserPatch{
GithubName: &githubUser.Login,
}
store.UpdateUser(userId, &userPatch)
}
user, err := store.GetUserByGithubName(githubUser.Login)
if err != nil {
username := githubUser.Name
usernameUsable, _ := store.CheckUsernameUsable(username)
for !usernameUsable {
username = githubUser.Name + utils.GenUUID()
usernameUsable, _ = store.CheckUsernameUsable(username)
}
user, _ = store.CreateNewUser(username, username, githubUser.Login)
}
session.Values["user_id"] = user.Id
session.Save(r, w)
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
func RegisterAuthRoutes(r *mux.Router) { func RegisterAuthRoutes(r *mux.Router) {
authRouter := r.PathPrefix("/api/auth").Subrouter() authRouter := r.PathPrefix("/api/auth").Subrouter()
@ -236,5 +110,4 @@ func RegisterAuthRoutes(r *mux.Router) {
authRouter.HandleFunc("/signup", handleUserSignUp).Methods("POST") authRouter.HandleFunc("/signup", handleUserSignUp).Methods("POST")
authRouter.HandleFunc("/signin", handleUserSignIn).Methods("POST") authRouter.HandleFunc("/signin", handleUserSignIn).Methods("POST")
authRouter.HandleFunc("/signout", handleUserSignOut).Methods("POST") authRouter.HandleFunc("/signout", handleUserSignOut).Methods("POST")
authRouter.HandleFunc("/github", handleGithubAuthCallback).Methods("GET")
} }

@ -29,16 +29,16 @@ func handleGetMyUserInfo(w http.ResponseWriter, r *http.Request) {
func handleUpdateMyUserInfo(w http.ResponseWriter, r *http.Request) { func handleUpdateMyUserInfo(w http.ResponseWriter, r *http.Request) {
userId, _ := GetUserIdInSession(r) userId, _ := GetUserIdInSession(r)
userPatch := store.UserPatch{} updateUserPatch := store.UpdateUserPatch{}
err := json.NewDecoder(r.Body).Decode(&userPatch) err := json.NewDecoder(r.Body).Decode(&updateUserPatch)
if err != nil { if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request") e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request")
return return
} }
if userPatch.Username != nil { if updateUserPatch.Username != nil {
usernameUsable, _ := store.CheckUsernameUsable(*userPatch.Username) usernameUsable, _ := store.CheckUsernameUsable(*updateUserPatch.Username)
if !usernameUsable { if !usernameUsable {
json.NewEncoder(w).Encode(Response{ json.NewEncoder(w).Encode(Response{
Succeed: false, Succeed: false,
@ -49,19 +49,7 @@ func handleUpdateMyUserInfo(w http.ResponseWriter, r *http.Request) {
} }
} }
if userPatch.GithubName != nil { user, err := store.UpdateUser(userId, &updateUserPatch)
githubNameUsable, _ := store.CheckGithubNameUsable(*userPatch.GithubName)
if !githubNameUsable {
json.NewEncoder(w).Encode(Response{
Succeed: false,
Message: "GitHub name is existed",
Data: nil,
})
return
}
}
user, err := store.UpdateUser(userId, &userPatch)
if err != nil { if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error()) e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
@ -75,10 +63,10 @@ func handleUpdateMyUserInfo(w http.ResponseWriter, r *http.Request) {
}) })
} }
func handleRefreshUserOpenId(w http.ResponseWriter, r *http.Request) { func handleResetUserOpenId(w http.ResponseWriter, r *http.Request) {
userId, _ := GetUserIdInSession(r) userId, _ := GetUserIdInSession(r)
openId, err := store.UpdateUserOpenId(userId) openId, err := store.ResetUserOpenId(userId)
if err != nil { if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error()) e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
@ -155,7 +143,7 @@ func RegisterUserRoutes(r *mux.Router) {
userRouter.HandleFunc("/me", handleGetMyUserInfo).Methods("GET") userRouter.HandleFunc("/me", handleGetMyUserInfo).Methods("GET")
userRouter.HandleFunc("/me", handleUpdateMyUserInfo).Methods("PATCH") userRouter.HandleFunc("/me", handleUpdateMyUserInfo).Methods("PATCH")
userRouter.HandleFunc("/open_id/new", handleRefreshUserOpenId).Methods("POST") userRouter.HandleFunc("/open_id/new", handleResetUserOpenId).Methods("POST")
userRouter.HandleFunc("/checkusername", handleCheckUsername).Methods("POST") userRouter.HandleFunc("/checkusername", handleCheckUsername).Methods("POST")
userRouter.HandleFunc("/validpassword", handleValidPassword).Methods("POST") userRouter.HandleFunc("/validpassword", handleValidPassword).Methods("POST")
} }

55
api/webhooks.go Normal file

@ -0,0 +1,55 @@
package api
import (
"encoding/json"
"memos/api/e"
"memos/store"
"net/http"
"github.com/gorilla/mux"
)
func handleCreateMemoByWH(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
openId := vars["openId"]
type CreateMemoDataBody struct {
Content string `json:"content"`
}
createMemo := CreateMemoDataBody{}
err := json.NewDecoder(r.Body).Decode(&createMemo)
if err != nil {
e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request")
return
}
user, err := store.GetUserByOpenId(openId)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
memo, err := store.CreateNewMemo(createMemo.Content, user.Id)
if err != nil {
e.ErrorHandler(w, "DATABASE_ERROR", err.Error())
return
}
json.NewEncoder(w).Encode(Response{
Succeed: true,
Message: "",
Data: memo,
})
}
func RegisterWebHooksRoutes(r *mux.Router) {
memoRouter := r.PathPrefix("/api/whs").Subrouter()
memoRouter.Use(JSONResponseMiddleWare)
memoRouter.HandleFunc("/memo/{openId}", handleCreateMemoByWH).Methods("POST")
}

@ -8,10 +8,9 @@ CREATE TABLE `users` (
`username` TEXT NOT NULL, `username` TEXT NOT NULL,
`password` TEXT NOT NULL, `password` TEXT NOT NULL,
`open_id` TEXT NOT NULL DEFAULT '', `open_id` TEXT NOT NULL DEFAULT '',
`github_name` TEXT NOT NULL DEFAULT '',
`created_at` TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')), `created_at` TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')),
`updated_at` TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')), `updated_at` TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')),
UNIQUE(`username`, `github_name`) UNIQUE(`username`, `open_id`)
); );
CREATE TABLE `queries` ( CREATE TABLE `queries` (
@ -48,10 +47,10 @@ CREATE TABLE `resources` (
INSERT INTO `users` INSERT INTO `users`
(`id`, `username`, `password`) (`id`, `username`, `password`, `open_id`)
VALUES VALUES
('1', 'guest', '123456'), ('1', 'guest', '123456', 'guest_open_id'),
('2', 'test', '123456'); ('2', 'mine', '123456', 'mine_open_id');
INSERT INTO `memos` INSERT INTO `memos`
(`id`, `content`, `user_id`) (`id`, `content`, `user_id`)

Binary file not shown.

@ -18,6 +18,7 @@ func main() {
api.RegisterMemoRoutes(r) api.RegisterMemoRoutes(r)
api.RegisterQueryRoutes(r) api.RegisterQueryRoutes(r)
api.RegisterResourceRoutes(r) api.RegisterResourceRoutes(r)
api.RegisterWebHooksRoutes(r)
webServe := api.SPAHandler{ webServe := api.SPAHandler{
StaticPath: "./web/dist", StaticPath: "./web/dist",

@ -8,40 +8,37 @@ import (
) )
type User struct { type User struct {
Id string `json:"id"` Id string `json:"id"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
OpenId string `json:"openId"` OpenId string `json:"openId"`
GithubName string `json:"githubName"` CreatedAt string `json:"createdAt"`
CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"`
UpdatedAt string `json:"updatedAt"`
} }
func CreateNewUser(username string, password string, githubName string) (User, error) { func CreateNewUser(username string, password string) (User, error) {
nowDateTimeStr := utils.GetNowDateTimeStr() nowDateTimeStr := utils.GetNowDateTimeStr()
newUser := User{ newUser := User{
Id: utils.GenUUID(), Id: utils.GenUUID(),
Username: username, Username: username,
Password: password, Password: password,
OpenId: utils.GenUUID(), OpenId: utils.GenUUID(),
GithubName: githubName, CreatedAt: nowDateTimeStr,
CreatedAt: nowDateTimeStr, UpdatedAt: nowDateTimeStr,
UpdatedAt: nowDateTimeStr,
} }
query := `INSERT INTO users (id, username, password, open_id, github_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)` query := `INSERT INTO users (id, username, password, open_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`
_, err := DB.Exec(query, newUser.Id, newUser.Username, newUser.Password, newUser.OpenId, newUser.GithubName, newUser.CreatedAt, newUser.UpdatedAt) _, err := DB.Exec(query, newUser.Id, newUser.Username, newUser.Password, newUser.OpenId, newUser.CreatedAt, newUser.UpdatedAt)
return newUser, FormatDBError(err) return newUser, FormatDBError(err)
} }
type UserPatch struct { type UpdateUserPatch struct {
Username *string Username *string
Password *string Password *string
GithubName *string
} }
func UpdateUser(id string, userPatch *UserPatch) (User, error) { func UpdateUser(id string, updateUserPatch *UpdateUserPatch) (User, error) {
user := User{} user := User{}
user, err := GetUserById(id) user, err := GetUserById(id)
@ -51,18 +48,14 @@ func UpdateUser(id string, userPatch *UserPatch) (User, error) {
set, args := []string{}, []interface{}{} set, args := []string{}, []interface{}{}
if v := userPatch.Username; v != nil { if v := updateUserPatch.Username; v != nil {
user.Username = *v user.Username = *v
set, args = append(set, "username=?"), append(args, *v) set, args = append(set, "username=?"), append(args, *v)
} }
if v := userPatch.Password; v != nil { if v := updateUserPatch.Password; v != nil {
user.Password = *v user.Password = *v
set, args = append(set, "password=?"), append(args, *v) set, args = append(set, "password=?"), append(args, *v)
} }
if v := userPatch.GithubName; v != nil {
user.GithubName = *v
set, args = append(set, "github_name=?"), append(args, *v)
}
set, args = append(set, "updated_at=?"), append(args, utils.GetNowDateTimeStr()) set, args = append(set, "updated_at=?"), append(args, utils.GetNowDateTimeStr())
args = append(args, id) args = append(args, id)
@ -72,7 +65,7 @@ func UpdateUser(id string, userPatch *UserPatch) (User, error) {
return user, FormatDBError(err) return user, FormatDBError(err)
} }
func UpdateUserOpenId(userId string) (string, error) { func ResetUserOpenId(userId string) (string, error) {
openId := utils.GenUUID() openId := utils.GenUUID()
query := `UPDATE users SET open_id=? WHERE id=?` query := `UPDATE users SET open_id=? WHERE id=?`
_, err := DB.Exec(query, openId, userId) _, err := DB.Exec(query, openId, userId)
@ -80,23 +73,23 @@ func UpdateUserOpenId(userId string) (string, error) {
} }
func GetUserById(id string) (User, error) { func GetUserById(id string) (User, error) {
query := `SELECT id, username, password, open_id, github_name, created_at, updated_at FROM users WHERE id=?` query := `SELECT id, username, password, open_id, created_at, updated_at FROM users WHERE id=?`
user := User{} user := User{}
err := DB.QueryRow(query, id).Scan(&user.Id, &user.Username, &user.Password, &user.OpenId, &user.GithubName, &user.CreatedAt, &user.UpdatedAt) err := DB.QueryRow(query, id).Scan(&user.Id, &user.Username, &user.Password, &user.OpenId, &user.CreatedAt, &user.UpdatedAt)
return user, FormatDBError(err)
}
func GetUserByOpenId(openId string) (User, error) {
query := `SELECT id, username, password, open_id, created_at, updated_at FROM users WHERE open_id=?`
user := User{}
err := DB.QueryRow(query, openId).Scan(&user.Id, &user.Username, &user.Password, &user.OpenId, &user.CreatedAt, &user.UpdatedAt)
return user, FormatDBError(err) return user, FormatDBError(err)
} }
func GetUserByUsernameAndPassword(username string, password string) (User, error) { func GetUserByUsernameAndPassword(username string, password string) (User, error) {
query := `SELECT id, username, password, open_id, github_name, created_at, updated_at FROM users WHERE username=? AND password=?` query := `SELECT id, username, password, open_id, created_at, updated_at FROM users WHERE username=? AND password=?`
user := User{} user := User{}
err := DB.QueryRow(query, username, password).Scan(&user.Id, &user.Username, &user.Password, &user.OpenId, &user.GithubName, &user.CreatedAt, &user.UpdatedAt) err := DB.QueryRow(query, username, password).Scan(&user.Id, &user.Username, &user.Password, &user.OpenId, &user.CreatedAt, &user.UpdatedAt)
return user, FormatDBError(err)
}
func GetUserByGithubName(githubName string) (User, error) {
query := `SELECT id, username, password, open_id, github_name, created_at, updated_at FROM users WHERE github_name=?`
user := User{}
err := DB.QueryRow(query, githubName).Scan(&user.Id, &user.Username, &user.Password, &user.OpenId, &user.GithubName, &user.CreatedAt, &user.UpdatedAt)
return user, FormatDBError(err) return user, FormatDBError(err)
} }
@ -118,23 +111,6 @@ func CheckUsernameUsable(username string) (bool, error) {
return usable, nil return usable, nil
} }
func CheckGithubNameUsable(githubName string) (bool, error) {
query := `SELECT * FROM users WHERE github_name=?`
query = fmt.Sprintf("SELECT COUNT(*) FROM (%s)", query)
var count uint
err := DB.QueryRow(query, githubName).Scan(&count)
if err != nil && err != sql.ErrNoRows {
return false, FormatDBError(err)
}
if count > 0 {
return false, nil
} else {
return true, nil
}
}
func CheckPasswordValid(id string, password string) (bool, error) { func CheckPasswordValid(id string, password string) (bool, error) {
query := `SELECT * FROM users WHERE id=? AND password=?` query := `SELECT * FROM users WHERE id=? AND password=?`
query = fmt.Sprintf("SELECT COUNT(*) FROM (%s)", query) query = fmt.Sprintf("SELECT COUNT(*) FROM (%s)", query)

@ -43,6 +43,7 @@ const MenuBtnsPopup: React.FC<Props> = (props: Props) => {
const handleSignOutBtnClick = async () => { const handleSignOutBtnClick = async () => {
await userService.doSignOut(); await userService.doSignOut();
locationService.replaceHistory("/signin"); locationService.replaceHistory("/signin");
window.location.reload();
}; };
return ( return (

@ -3,7 +3,8 @@ import appContext from "../stores/appContext";
import { userService } from "../services"; import { userService } from "../services";
import utils from "../helpers/utils"; import utils from "../helpers/utils";
import { validate, ValidatorConfig } from "../helpers/validator"; 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 toastHelper from "./Toast";
import showChangePasswordDialog from "./ChangePasswordDialog"; import showChangePasswordDialog from "./ChangePasswordDialog";
import "../less/my-account-section.less"; import "../less/my-account-section.less";
@ -21,7 +22,9 @@ const MyAccountSection: React.FC<Props> = () => {
const { userState } = useContext(appContext); const { userState } = useContext(appContext);
const user = userState.user as Model.User; const user = userState.user as Model.User;
const [username, setUsername] = useState<string>(user.username); 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 handleUsernameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const nextUsername = e.target.value as string; const nextUsername = e.target.value as string;
@ -69,18 +72,23 @@ const MyAccountSection: React.FC<Props> = () => {
showChangePasswordDialog(); showChangePasswordDialog();
}; };
const handleUnbindGithubBtnClick = async () => { const handleResetOpenIdBtnClick = async () => {
if (showConfirmUnbindGithubBtn) { if (!showConfirmResetAPIBtn) {
try { toggleConfirmResetAPIBtn(true);
await userService.removeGithubName(); return;
await userService.doSignIn();
} catch (error: any) {
toastHelper.error(error.message);
}
setShowConfirmUnbindGithubBtn(false);
} else {
setShowConfirmUnbindGithubBtn(true);
} }
if (resetBtnClickLoadingState.isLoading) {
return;
}
resetBtnClickLoadingState.setLoading();
try {
await userService.resetOpenId();
} catch (error) {
// do nth
}
resetBtnClickLoadingState.setFinish();
toggleConfirmResetAPIBtn(false);
}; };
const handlePreventDefault = (e: React.MouseEvent) => { const handlePreventDefault = (e: React.MouseEvent) => {
@ -124,39 +132,17 @@ const MyAccountSection: React.FC<Props> = () => {
</span> </span>
</label> </label>
</div> </div>
{/* Account Binding Settings: only can use for domain: memos.justsven.top */} <div className="section-container openapi-section-container">
<Only when={window.location.origin.includes("justsven.top")}> <p className="title-text">Open API</p>
<div className="section-container connect-section-container"> <p className="value-text">{openAPIRoute}</p>
<p className="title-text"></p> <span className={`reset-btn ${resetBtnClickLoadingState.isLoading ? "loading" : ""}`} onClick={handleResetOpenIdBtnClick}>
<label className="form-label input-form-label"> {showConfirmResetAPIBtn ? "⚠️ 确定重置 API" : "重置 API"}
<span className="normal-text">GitHub</span> </span>
{user.githubName ? ( <div className="usage-guide-container">
<> <p className="title-text">使</p>
<a className="value-text" href={"https://github.com/" + user.githubName}> <pre>{`POST ${openAPIRoute}\nContent-type: application/json\n{\n "content": "Hello, #memos ${window.location.origin}"\n}`}</pre>
{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> </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({ return request({
method: "PATCH", method: "PATCH",
url: "/api/user/me", 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() { export function getMyMemos() {
return request<Model.Memo[]>({ return request<Model.Memo[]>({
method: "GET", method: "GET",

@ -11,6 +11,7 @@
@bg-lightgray: #eaeaea; @bg-lightgray: #eaeaea;
@bg-blue: #1337a3; @bg-blue: #1337a3;
@bg-yellow: yellow; @bg-yellow: yellow;
@bg-red: #fcf0f0;
@bg-light-blue: #eef3fe; @bg-light-blue: #eef3fe;
@bg-paper-yellow: #fbf4de; @bg-paper-yellow: #fbf4de;

@ -2,7 +2,7 @@
.account-section-container { .account-section-container {
> .form-label { > .form-label {
height: 28px; min-height: 28px;
&.username-label { &.username-label {
> input { > input {
@ -62,43 +62,52 @@
} }
} }
.connect-section-container { .openapi-section-container {
> .form-label { > .value-text {
height: 28px; width: 100%;
border: 1px solid lightgray;
padding: 4px 6px;
border-radius: 4px;
line-height: 1.6;
}
> .value-text { > .reset-btn {
max-width: 128px; margin-top: 4px;
min-height: 20px; padding: 4px 8px;
overflow: hidden; background-color: @bg-red;
text-overflow: ellipsis; border: 1px solid red;
color: red;
border-radius: 4px;
line-height: 1.6;
cursor: pointer;
user-select: none;
&:hover {
opacity: 0.8;
} }
> .btn-text { &.loading {
padding: 0 8px; opacity: 0.6;
margin-left: 12px; 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; border-radius: 4px;
font-size: 12px; line-height: 1.4;
line-height: 28px; word-break: break-all;
cursor: pointer; word-wrap: break-word;
white-space: pre-wrap;
&: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;
}
}
} }
} }
} }

@ -9,7 +9,7 @@
} }
> .form-label { > .form-label {
height: 28px; min-height: 28px;
cursor: pointer; cursor: pointer;
> .icon-img { > .icon-img {

@ -3,7 +3,6 @@ import api from "../helpers/api";
import { validate, ValidatorConfig } from "../helpers/validator"; import { validate, ValidatorConfig } from "../helpers/validator";
import useLoading from "../hooks/useLoading"; import useLoading from "../hooks/useLoading";
import { locationService, userService } from "../services"; import { locationService, userService } from "../services";
import Only from "../components/common/OnlyWhen";
import toastHelper from "../components/Toast"; import toastHelper from "../components/Toast";
import "../less/signin.less"; import "../less/signin.less";
@ -20,7 +19,7 @@ const Signin: React.FC<Props> = () => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [showAutoSigninAsGuest, setShowAutoSigninAsGuest] = useState(true); const [showAutoSigninAsGuest, setShowAutoSigninAsGuest] = useState(true);
const signinBtnClickLoadingState = useLoading(false); const signinBtnsClickLoadingState = useLoading(false);
const autoSigninAsGuestBtn = useRef<HTMLDivElement>(null); const autoSigninAsGuestBtn = useRef<HTMLDivElement>(null);
const signinBtn = useRef<HTMLButtonElement>(null); const signinBtn = useRef<HTMLButtonElement>(null);
@ -49,12 +48,8 @@ const Signin: React.FC<Props> = () => {
setPassword(text); setPassword(text);
}; };
const handleSignUpBtnClick = async () => { const handleSigninBtnsClick = async (action: "signin" | "signup" = "signin") => {
toastHelper.info("注册已关闭"); if (signinBtnsClickLoadingState.isLoading) {
};
const handleSignInBtnClick = async () => {
if (signinBtnClickLoadingState.isLoading) {
return; return;
} }
@ -71,8 +66,11 @@ const Signin: React.FC<Props> = () => {
} }
try { try {
signinBtnClickLoadingState.setLoading(); signinBtnsClickLoadingState.setLoading();
const actionFunc = api.signin; let actionFunc = api.signin;
if (action === "signup") {
actionFunc = api.signup;
}
const { succeed, message } = await actionFunc(username, password); const { succeed, message } = await actionFunc(username, password);
if (!succeed && message) { if (!succeed && message) {
@ -90,11 +88,11 @@ const Signin: React.FC<Props> = () => {
console.error(error); console.error(error);
toastHelper.error("😟 " + error.message); toastHelper.error("😟 " + error.message);
} }
signinBtnClickLoadingState.setFinish(); signinBtnsClickLoadingState.setFinish();
}; };
const handleSwitchAccountSigninBtnClick = () => { const handleSwitchAccountSigninBtnClick = () => {
if (signinBtnClickLoadingState.isLoading) { if (signinBtnsClickLoadingState.isLoading) {
return; return;
} }
@ -102,12 +100,12 @@ const Signin: React.FC<Props> = () => {
}; };
const handleAutoSigninAsGuestBtnClick = async () => { const handleAutoSigninAsGuestBtnClick = async () => {
if (signinBtnClickLoadingState.isLoading) { if (signinBtnsClickLoadingState.isLoading) {
return; return;
} }
try { try {
signinBtnClickLoadingState.setLoading(); signinBtnsClickLoadingState.setLoading();
const { succeed, message } = await api.signin("guest", "123456"); const { succeed, message } = await api.signin("guest", "123456");
if (!succeed && message) { if (!succeed && message) {
@ -125,7 +123,7 @@ const Signin: React.FC<Props> = () => {
console.error(error); console.error(error);
toastHelper.error("😟 " + error.message); toastHelper.error("😟 " + error.message);
} }
signinBtnClickLoadingState.setFinish(); signinBtnsClickLoadingState.setFinish();
}; };
return ( return (
@ -141,13 +139,13 @@ const Signin: React.FC<Props> = () => {
<div className="quickly-btns-container"> <div className="quickly-btns-container">
<div <div
ref={autoSigninAsGuestBtn} ref={autoSigninAsGuestBtn}
className={`btn guest-signin ${signinBtnClickLoadingState.isLoading ? "requesting" : ""}`} className={`btn guest-signin ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
onClick={handleAutoSigninAsGuestBtnClick} onClick={handleAutoSigninAsGuestBtnClick}
> >
👉 👉
</div> </div>
<div <div
className={`btn ${signinBtnClickLoadingState.isLoading ? "requesting" : ""}`} className={`btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
onClick={handleSwitchAccountSigninBtnClick} onClick={handleSwitchAccountSigninBtnClick}
> >
@ -167,32 +165,26 @@ const Signin: React.FC<Props> = () => {
</div> </div>
</div> </div>
<div className="page-footer-container"> <div className="page-footer-container">
<div className="btns-container"> <div className="btns-container">{/* nth */}</div>
<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"> <div className="btns-container">
<button <button
className={`btn ${signinBtnClickLoadingState.isLoading ? "requesting" : ""}`} className={`btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
onClick={handleAutoSigninAsGuestBtnClick} onClick={handleAutoSigninAsGuestBtnClick}
> >
</button> </button>
<span className="split-text">/</span> <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> </button>
<span className="split-text">/</span> <span className="split-text">/</span>
<button <button
className={`btn signin-btn ${signinBtnClickLoadingState.isLoading ? "requesting" : ""}`}
ref={signinBtn} ref={signinBtn}
onClick={handleSignInBtnClick} className={`btn signin-btn ${signinBtnsClickLoadingState.isLoading ? "requesting" : ""}`}
onClick={() => handleSigninBtnsClick("signin")}
> >
</button> </button>

@ -40,12 +40,6 @@ class UserService {
}); });
} }
public async removeGithubName(): Promise<void> {
await api.updateUserinfo({
githubName: "",
});
}
public async checkPasswordValid(password: string): Promise<boolean> { public async checkPasswordValid(password: string): Promise<boolean> {
const { data: isValid } = await api.checkPasswordValid(password); const { data: isValid } = await api.checkPasswordValid(password);
return isValid; return isValid;
@ -56,6 +50,15 @@ class UserService {
password, 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(); const userService = new UserService();

@ -12,7 +12,12 @@ interface SignOutAction {
payload: null; 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 { export function reducer(state: State, action: Actions): State {
switch (action.type) { switch (action.type) {
@ -26,6 +31,18 @@ export function reducer(state: State, action: Actions): State {
user: null, user: null,
}; };
} }
case "RESET_OPENID": {
if (!state.user) {
return state;
}
return {
user: {
...state.user,
openId: action.payload,
},
};
}
default: { default: {
return state; return state;
} }

@ -7,7 +7,7 @@ declare namespace Model {
interface User extends BaseModel { interface User extends BaseModel {
username: string; username: string;
githubName: string; openId: string;
} }
interface Memo extends BaseModel { interface Memo extends BaseModel {