commit 2f72bfa946a24d8a0654955afdd70e1e877d91b2 Author: LeeShuang Date: Wed Dec 8 23:43:14 2021 +0800 init project diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..78660904 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Temp output +*.out +*.log +tmp + +# Air (hot reload) generated +.air + +# Frontend asset +dist + +# Dev database +data + +# build folder +build + +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..15ed47e3 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# ✍️ Memos + +🏗 In heavily development. diff --git a/api/auth.go b/api/auth.go new file mode 100644 index 00000000..1d408b12 --- /dev/null +++ b/api/auth.go @@ -0,0 +1,82 @@ +package api + +import ( + "encoding/json" + "memos/common/error" + "memos/store" + "net/http" + + "github.com/gorilla/mux" +) + +type UserSignUp struct { + Username string `json:"username"` + Password string `json:"password"` +} + +func handleUserSignUp(w http.ResponseWriter, r *http.Request) { + var userSignup UserSignUp + err := json.NewDecoder(r.Body).Decode(&userSignup) + + if err != nil { + error.ErrorHandler(w, "REQUEST_BODY_ERROR") + return + } + + user, err := store.CreateNewUser(userSignup.Username, userSignup.Password, "", "") + + if err != nil { + error.ErrorHandler(w, "") + return + } + + json.NewEncoder(w).Encode(user) +} + +type UserSignin struct { + Username string `json:"username"` + Password string `json:"password"` +} + +func handleUserSignIn(w http.ResponseWriter, r *http.Request) { + var userSignin UserSignin + err := json.NewDecoder(r.Body).Decode(&userSignin) + + if err != nil { + error.ErrorHandler(w, "") + return + } + + user, err := store.GetUserByUsernameAndPassword(userSignin.Username, userSignin.Password) + + if err != nil { + error.ErrorHandler(w, "") + return + } + + userIdCookie := &http.Cookie{ + Name: "user_id", + Value: user.Id, + MaxAge: 3600 * 24 * 30, + } + http.SetCookie(w, userIdCookie) + + json.NewEncoder(w).Encode(user) +} + +func handleUserSignOut(w http.ResponseWriter, r *http.Request) { + userIdCookie := &http.Cookie{ + Name: "user_id", + Value: "", + MaxAge: 0, + } + http.SetCookie(w, userIdCookie) +} + +func RegisterAuthRoutes(r *mux.Router) { + authRouter := r.PathPrefix("/api/auth").Subrouter() + + authRouter.HandleFunc("/signup", handleUserSignUp).Methods("POST") + authRouter.HandleFunc("/signin", handleUserSignIn).Methods("POST") + authRouter.HandleFunc("/signout", handleUserSignOut).Methods("POST") +} diff --git a/api/memo.go b/api/memo.go new file mode 100644 index 00000000..1b8a1c01 --- /dev/null +++ b/api/memo.go @@ -0,0 +1,82 @@ +package api + +import ( + "encoding/json" + "memos/common/error" + "memos/store" + "net/http" + + "github.com/gorilla/mux" +) + +func handleGetMyMemos(w http.ResponseWriter, r *http.Request) { + userId, _ := GetUserIdInCookie(r) + + memos, err := store.GetMemosByUserId(userId) + + if err != nil { + error.ErrorHandler(w, "DATABASE_ERROR") + return + } + + json.NewEncoder(w).Encode(memos) +} + +type CreateMemo struct { + Content string `json:"content"` +} + +func handleCreateMemo(w http.ResponseWriter, r *http.Request) { + userId, _ := GetUserIdInCookie(r) + + var createMemo CreateMemo + err := json.NewDecoder(r.Body).Decode(&createMemo) + + if err != nil { + error.ErrorHandler(w, "") + return + } + + memo, err := store.CreateNewMemo(createMemo.Content, userId) + + if err != nil { + error.ErrorHandler(w, "") + return + } + + json.NewEncoder(w).Encode(memo) +} + +func handleUpdateMemo(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + memoId := vars["id"] + + userId, _ := GetUserIdInCookie(r) + + var createMemo CreateMemo + err := json.NewDecoder(r.Body).Decode(&createMemo) + + if err != nil { + error.ErrorHandler(w, "") + return + } + + memo, err := store.UpdateMemo(memoId, createMemo.Content, userId) + + if err != nil { + error.ErrorHandler(w, "") + return + } + + json.NewEncoder(w).Encode(memo) +} + +func RegisterMemoRoutes(r *mux.Router) { + memoRouter := r.PathPrefix("/api/memo").Subrouter() + + memoRouter.Use(AuthCheckerMiddleWare) + + memoRouter.HandleFunc("/all", handleGetMyMemos).Methods("GET") + memoRouter.HandleFunc("/", handleCreateMemo).Methods("PUT") + memoRouter.HandleFunc("/{id}", handleUpdateMemo).Methods("PATCH") +} diff --git a/api/middlewares.go b/api/middlewares.go new file mode 100644 index 00000000..095abaf8 --- /dev/null +++ b/api/middlewares.go @@ -0,0 +1,19 @@ +package api + +import ( + "memos/common/error" + "net/http" +) + +func AuthCheckerMiddleWare(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userId, err := GetUserIdInCookie(r) + + if err != nil || userId == "" { + error.ErrorHandler(w, "NOT_AUTH") + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/api/user.go b/api/user.go new file mode 100644 index 00000000..ffa58836 --- /dev/null +++ b/api/user.go @@ -0,0 +1,60 @@ +package api + +import ( + "encoding/json" + "memos/common/error" + "memos/store" + "net/http" + + "github.com/gorilla/mux" +) + +func handleGetMyUserInfo(w http.ResponseWriter, r *http.Request) { + userId, _ := GetUserIdInCookie(r) + + user, err := store.GetUserById(userId) + + if err != nil { + error.ErrorHandler(w, "DATABASE_ERROR") + return + } + + json.NewEncoder(w).Encode(user) +} + +type UpdateUser struct { + Username string `json:"username"` + Password string `json:"password"` + GithubName string `json:"githubName"` + WxOpenId string `json:"wxOpenId"` +} + +func handleUpdateMyUserInfo(w http.ResponseWriter, r *http.Request) { + userId, _ := GetUserIdInCookie(r) + + user, err := store.GetUserById(userId) + + if err != nil { + error.ErrorHandler(w, "DATABASE_ERROR") + return + } + + var updateUser UpdateUser + err = json.NewDecoder(r.Body).Decode(&updateUser) + + if err != nil { + error.ErrorHandler(w, "REQUEST_BODY_ERROR") + return + } + + json.NewEncoder(w).Encode(user) +} + +func RegisterUserRoutes(r *mux.Router) { + userRouter := r.PathPrefix("/api/user").Subrouter() + + userRouter.Use(AuthCheckerMiddleWare) + + userRouter.HandleFunc("/me", handleGetMyUserInfo).Methods("GET") + userRouter.HandleFunc("/me", handleUpdateMyUserInfo).Methods("PATCH") +} diff --git a/api/utils.go b/api/utils.go new file mode 100644 index 00000000..c4eec6cb --- /dev/null +++ b/api/utils.go @@ -0,0 +1,11 @@ +package api + +import ( + "net/http" +) + +func GetUserIdInCookie(r *http.Request) (string, error) { + userIdCookie, err := r.Cookie("user_id") + + return userIdCookie.Value, err +} diff --git a/common/error/codes.go b/common/error/codes.go new file mode 100644 index 00000000..35d9d6b2 --- /dev/null +++ b/common/error/codes.go @@ -0,0 +1,9 @@ +package error + +var Codes = map[string]int{ + "NOT_AUTH": 20001, + + "REQUEST_BODY_ERROR": 40001, + + "DATABASE_ERROR": 50001, +} diff --git a/common/error/error.go b/common/error/error.go new file mode 100644 index 00000000..f52d866f --- /dev/null +++ b/common/error/error.go @@ -0,0 +1,48 @@ +package error + +import ( + "encoding/json" + "net/http" +) + +type ServerError struct { + Code int + Message string +} + +type ErrorResponse struct { + StatusCode int `json:"statusCode"` + StatusMessage string `json:"statusMessage"` + Succeed bool `json:"succeed"` + Data interface{} `json:"data"` +} + +func getServerError(err string) ServerError { + code, exists := Codes[err] + + if !exists { + err = "Bad Request" + code = 40000 + } + + return ServerError{ + Code: code, + Message: err, + } +} + +func ErrorHandler(w http.ResponseWriter, err string) { + serverError := getServerError(err) + + res := ErrorResponse{ + StatusCode: serverError.Code, + StatusMessage: serverError.Message, + Succeed: false, + Data: nil, + } + + statusCode := int(serverError.Code / 100) + + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(res) +} diff --git a/common/utils.go b/common/utils.go new file mode 100644 index 00000000..ac89f3a8 --- /dev/null +++ b/common/utils.go @@ -0,0 +1,15 @@ +package common + +import ( + "time" + + "github.com/google/uuid" +) + +func GenUUID() string { + return uuid.New().String() +} + +func GetNowDateTimeStr() string { + return time.Now().Format("RFC3339") +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..973cdcf3 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module memos + +go 1.17 + +require github.com/gorilla/mux v1.8.0 + +require github.com/mattn/go-sqlite3 v1.14.9 + +require github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..fda9a009 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= diff --git a/main.go b/main.go new file mode 100644 index 00000000..181b5951 --- /dev/null +++ b/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "memos/api" + "memos/store" + "net/http" + + "github.com/gorilla/mux" +) + +func main() { + store.InitDBConn() + + r := mux.NewRouter().StrictSlash(true) + + api.RegisterUserRoutes(r) + api.RegisterAuthRoutes(r) + + http.ListenAndServe("localhost:8080", r) +} diff --git a/resources/memos.db b/resources/memos.db new file mode 100644 index 00000000..71a6abcd Binary files /dev/null and b/resources/memos.db differ diff --git a/resources/sqlite.sql b/resources/sqlite.sql new file mode 100644 index 00000000..2f8b6d18 --- /dev/null +++ b/resources/sqlite.sql @@ -0,0 +1,30 @@ +CREATE TABLE `users` ( + `id` TEXT NOT NULL PRIMARY KEY, + `username` TEXT NOT NULL, + `password` TEXT NOT NULL, + `github_name` TEXT NULL DEFAULT '', + `wx_open_id` TEXT NULL DEFAULT '', + `created_at` TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE `memos` ( + `id` TEXT NOT NULL PRIMARY KEY, + `content` TEXT NOT NULL, + `user_id` TEXT NOT NULL, + `created_at` TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + `deleted_at` TEXT, + FOREIGN KEY(`user_id`) REFERENCES `users`(`id`) +); + +CREATE TABLE `queries` ( + `id` TEXT NOT NULL PRIMARY KEY, + `user_id` TEXT NOT NULL, + `title` TEXT NOT NULL, + `querystring` TEXT NOT NULL, + `created_at` TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + `pinned_at` TEXT NULL, + FOREIGN KEY(`user_id`) REFERENCES `users`(`id`) +); diff --git a/scripts/.air.toml b/scripts/.air.toml new file mode 100644 index 00000000..d845bf98 --- /dev/null +++ b/scripts/.air.toml @@ -0,0 +1,13 @@ +root = "." +tmp_dir = "tmp" + +[build] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor"] + exclude_file = [] + exclude_regex = [] + exclude_unchanged = false + follow_symlink = false + full_bin = "" diff --git a/store/memo.go b/store/memo.go new file mode 100644 index 00000000..e0e0a67a --- /dev/null +++ b/store/memo.go @@ -0,0 +1,72 @@ +package store + +import "memos/common" + +type Memo struct { + Id string `json:"id"` + Content string `json:"content"` + UserId string `json:"userId"` + DeletedAt string `json:"deletedAt"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func CreateNewMemo(content string, userId string) (Memo, error) { + nowDateTimeStr := common.GetNowDateTimeStr() + newMemo := Memo{ + Id: common.GenUUID(), + Content: content, + UserId: userId, + DeletedAt: "", + CreatedAt: nowDateTimeStr, + UpdatedAt: nowDateTimeStr, + } + + query := `INSERT INTO memos (id, content, user_id, deleted_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)` + _, err := DB.Exec(query, newMemo.Id, newMemo.Content, newMemo.UserId, newMemo.DeletedAt, newMemo.CreatedAt, newMemo.UpdatedAt) + + return newMemo, err +} + +func UpdateMemo(id string, content string, deletedAt string) (Memo, error) { + nowDateTimeStr := common.GetNowDateTimeStr() + memo, _ := GetMemoById(id) + + if content != "" { + memo.Content = content + } + if deletedAt != "" { + memo.DeletedAt = deletedAt + } + + memo.UpdatedAt = nowDateTimeStr + + query := `UPDATE memos SET (content, deleted_at, updated_at) VALUES (?, ?, ?)` + _, err := DB.Exec(query, memo.Content, memo.DeletedAt, memo.UpdatedAt) + + return memo, err +} + +func GetMemoById(id string) (Memo, error) { + query := `SELECT id, content, user_id, deleted_at, created_at, updated_at FROM memos WHERE id=?` + var memo Memo + err := DB.QueryRow(query, id).Scan(&memo.Id, &memo.Content, &memo.UserId, &memo.DeletedAt, &memo.CreatedAt, &memo.UpdatedAt) + return memo, err +} + +func GetMemosByUserId(userId string) ([]Memo, error) { + query := `SELECT id, content, user_id, deleted_at, created_at, updated_at FROM memos WHERE user_id=?` + + rows, err := DB.Query(query, userId) + + var memos []Memo + + for rows.Next() { + var memo Memo + err = rows.Scan(&memo.Id, &memo.Content, &memo.UserId, &memo.DeletedAt, &memo.CreatedAt, &memo.UpdatedAt) + + memos = append(memos, memo) + } + + return memos, err +} diff --git a/store/sqlite.go b/store/sqlite.go new file mode 100644 index 00000000..19fbc25f --- /dev/null +++ b/store/sqlite.go @@ -0,0 +1,31 @@ +package store + +import ( + "database/sql" + "fmt" + + _ "github.com/mattn/go-sqlite3" +) + +var DB *sql.DB + +func InitDBConn() { + db, err := sql.Open("sqlite3", "./resources/memos.db") + if err != nil { + fmt.Println("connect failed") + } else { + DB = db + fmt.Println("connect to sqlite succeed") + } +} + +func FormatDBError(err error) error { + if err == nil { + return nil + } + + switch err.Error() { + default: + return err + } +} diff --git a/store/user.go b/store/user.go new file mode 100644 index 00000000..0a0d855c --- /dev/null +++ b/store/user.go @@ -0,0 +1,139 @@ +package store + +import ( + "database/sql" + "fmt" + "memos/common" +) + +type User struct { + Id string `json:"id"` + Username string `json:"username"` + Password string `json:"password"` + WxOpenId string `json:"wxOpenId"` + GithubName string `json:"githubName"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func CreateNewUser(username string, password string, githubName string, wxOpenId string) (User, error) { + nowDateTimeStr := common.GetNowDateTimeStr() + newUser := User{ + Id: common.GenUUID(), + Username: username, + Password: password, + WxOpenId: wxOpenId, + GithubName: githubName, + CreatedAt: nowDateTimeStr, + UpdatedAt: nowDateTimeStr, + } + + query := `INSERT INTO users (id, username, password, wx_open_id, github_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)` + _, err := DB.Exec(query, newUser.Id, newUser.Username, newUser.Password, newUser.WxOpenId, newUser.GithubName, newUser.CreatedAt, newUser.UpdatedAt) + + return newUser, err +} + +func UpdateUser(id string, username string, password string, githubName string, wxOpenId string) (User, error) { + nowDateTimeStr := common.GetNowDateTimeStr() + user, _ := GetUserById(id) + + if username != "" { + user.Username = username + } + if password != "" { + user.Password = password + } + if githubName != "" { + user.GithubName = githubName + } + if wxOpenId != "" { + user.WxOpenId = wxOpenId + } + + user.UpdatedAt = nowDateTimeStr + + query := `UPDATE users SET (username, password, wx_open_id, github_name, updated_at) VALUES (?, ?, ?, ?, ?)` + _, err := DB.Exec(query, user.Username, user.Password, user.WxOpenId, user.GithubName, user.UpdatedAt) + + return user, err +} + +func GetUserById(id string) (User, error) { + query := `SELECT id, username, password, wx_open_id, github_name, created_at, updated_at FROM users WHERE id=?` + var user User + err := DB.QueryRow(query, id).Scan(&user.Id, &user.Username, &user.Password, &user.WxOpenId, &user.GithubName, &user.CreatedAt, &user.UpdatedAt) + return user, err +} + +func GetUserByUsernameAndPassword(username string, password string) (User, error) { + query := `SELECT id, username, password, wx_open_id, github_name, created_at, updated_at FROM users WHERE username=? AND password=?` + var user User + err := DB.QueryRow(query, username, password).Scan(&user.Id, &user.Username, &user.Password, &user.WxOpenId, &user.GithubName, &user.CreatedAt, &user.UpdatedAt) + return user, err +} + +func GetUserByGithubName(githubName string) (User, error) { + query := `SELECT id, username, password, wx_open_id, github_name, created_at, updated_at FROM users WHERE github_name=?` + var user User + err := DB.QueryRow(query, githubName).Scan(&user.Id, &user.Username, &user.Password, &user.WxOpenId, &user.GithubName, &user.CreatedAt, &user.UpdatedAt) + return user, err +} + +func GetUserByWxOpenId(wxOpenId string) (User, error) { + query := `SELECT id, username, password, wx_open_id, github_name, created_at, updated_at FROM users WHERE id=?` + var user User + err := DB.QueryRow(query, wxOpenId).Scan(&user.Id, &user.Username, &user.Password, &user.WxOpenId, &user.GithubName, &user.CreatedAt, &user.UpdatedAt) + return user, err +} + +func CheckUsernameUsable(username string) (bool, error) { + query := `SELECT * FROM users WHERE username=?` + query = fmt.Sprintf("SELECT COUNT(*) FROM (%s)", query) + + var count uint + err := DB.QueryRow(query, username).Scan(&count) + if err != nil && err != sql.ErrNoRows { + return false, FormatDBError(err) + } + + if count > 0 { + return false, nil + } else { + return true, 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) { + query := `SELECT * FROM users WHERE id=? AND password=?` + query = fmt.Sprintf("SELECT COUNT(*) FROM (%s)", query) + + var count uint + err := DB.QueryRow(query, id, password).Scan(&count) + if err != nil && err != sql.ErrNoRows { + return false, FormatDBError(err) + } + + if count > 0 { + return true, nil + } else { + return false, nil + } +}