diff --git a/.gitignore b/.gitignore index 723ef36..c48e205 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -.idea \ No newline at end of file +.idea +l1nk4i/config.toml +.gitignore +l1nk4i/test \ No newline at end of file diff --git a/l1nk4i/Dockerfile b/l1nk4i/Dockerfile new file mode 100644 index 0000000..0ce0926 --- /dev/null +++ b/l1nk4i/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.23.1 AS builder + +WORKDIR /app + +COPY . . + +RUN go build -o myapp + +FROM ubuntu:latest + +WORKDIR /app + +COPY --from=builder /app/myapp /app/ +COPY config.toml . +COPY entrypoint.sh . +RUN chmod +x entrypoint.sh + +ENTRYPOINT ["./entrypoint.sh"] + +EXPOSE 8080 \ No newline at end of file diff --git a/l1nk4i/api/admin/deleteAnswer.go b/l1nk4i/api/admin/deleteAnswer.go new file mode 100644 index 0000000..470b4e9 --- /dev/null +++ b/l1nk4i/api/admin/deleteAnswer.go @@ -0,0 +1,32 @@ +package admin + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "l1nk4i/db" + "net/http" +) + +func DeleteAnswer(c *gin.Context) { + answerID := c.Param("answer-id") + + session := sessions.Default(c) + role, exists := session.Get("role").(string) + if !exists { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"}) + return + } + + if role != "admin" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + + err := db.DeleteAnswer(answerID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer-id"}) + return + } + + c.JSON(http.StatusOK, gin.H{"msg": "delete answer successful!"}) +} diff --git a/l1nk4i/api/admin/deleteQuestion.go b/l1nk4i/api/admin/deleteQuestion.go new file mode 100644 index 0000000..511a339 --- /dev/null +++ b/l1nk4i/api/admin/deleteQuestion.go @@ -0,0 +1,32 @@ +package admin + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "l1nk4i/db" + "net/http" +) + +func DeleteQuestion(c *gin.Context) { + questionID := c.Param("question-id") + + session := sessions.Default(c) + role, exists := session.Get("role").(string) + if !exists { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"}) + return + } + + if role != "admin" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + + err := db.DeleteQuestion(questionID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question-id"}) + return + } + + c.JSON(http.StatusOK, gin.H{"msg": "delete question successful!"}) +} diff --git a/l1nk4i/api/answer/create.go b/l1nk4i/api/answer/create.go new file mode 100644 index 0000000..a8590fe --- /dev/null +++ b/l1nk4i/api/answer/create.go @@ -0,0 +1,50 @@ +package answer + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "l1nk4i/db" + "net/http" +) + +func Create(c *gin.Context) { + questionID := c.Param("question-id") + + var answerInfo struct { + Content string `json:"content"` + } + + if err := c.ShouldBind(&answerInfo); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"}) + return + } + + session := sessions.Default(c) + userID, exists := session.Get("user_id").(string) + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session"}) + return + } + + // Verify that the question exists + _, err := db.GetQuestionByQuestionID(questionID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"}) + return + } + + answer := db.Answer{ + AnswerID: uuid.New().String(), + UserID: userID, + QuestionID: questionID, + Content: answerInfo.Content, + } + if err := db.CreateAnswer(&answer); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "create answer error"}) + return + } + + c.JSON(http.StatusOK, gin.H{"answer_id": answer.AnswerID}) + return +} diff --git a/l1nk4i/api/answer/delete.go b/l1nk4i/api/answer/delete.go new file mode 100644 index 0000000..77bb250 --- /dev/null +++ b/l1nk4i/api/answer/delete.go @@ -0,0 +1,40 @@ +package answer + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "l1nk4i/db" + "net/http" +) + +func Delete(c *gin.Context) { + answerID := c.Param("answer-id") + + // Verify user identity + session := sessions.Default(c) + userid, exists := session.Get("user_id").(string) + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session"}) + return + } + + answer, err := db.GetAnswerByAnswerID(answerID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer_id"}) + return + } + + if answer.UserID != userid { + c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"}) + return + } + + // Delete answer + err = db.DeleteAnswer(answerID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "delete answer error"}) + return + } + + c.JSON(http.StatusOK, gin.H{"msg": "delete answer successful!"}) +} diff --git a/l1nk4i/api/answer/get.go b/l1nk4i/api/answer/get.go new file mode 100644 index 0000000..ede658c --- /dev/null +++ b/l1nk4i/api/answer/get.go @@ -0,0 +1,20 @@ +package answer + +import ( + "github.com/gin-gonic/gin" + "l1nk4i/db" + "net/http" +) + +// Get gets answers to the question by question_id +func Get(c *gin.Context) { + questionID := c.Param("question-id") + + answers, err := db.GetAnswersByQuestionID(questionID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"}) + return + } + + c.JSON(http.StatusOK, gin.H{"data": answers}) +} diff --git a/l1nk4i/api/answer/update.go b/l1nk4i/api/answer/update.go new file mode 100644 index 0000000..48b4fd6 --- /dev/null +++ b/l1nk4i/api/answer/update.go @@ -0,0 +1,49 @@ +package answer + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "l1nk4i/db" + "net/http" +) + +func Update(c *gin.Context) { + answerID := c.Param("answer-id") + + var answerInfo struct { + Content string `json:"content"` + } + + if err := c.ShouldBindJSON(&answerInfo); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"}) + return + } + + // Verify user identity + session := sessions.Default(c) + userid, exists := session.Get("user_id").(string) + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session"}) + return + } + + answer, err := db.GetAnswerByAnswerID(answerID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer_id"}) + return + } + + if answer.UserID != userid { + c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"}) + return + } + + // Update answer + err = db.UpdateAnswer(answerID, answerInfo.Content) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer"}) + return + } + + c.JSON(http.StatusOK, gin.H{"msg": "update answer successful!"}) +} diff --git a/l1nk4i/api/question/best.go b/l1nk4i/api/question/best.go new file mode 100644 index 0000000..231a1c5 --- /dev/null +++ b/l1nk4i/api/question/best.go @@ -0,0 +1,53 @@ +package question + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "l1nk4i/db" + "net/http" +) + +func Best(c *gin.Context) { + answerID := c.Param("answer-id") + questionID := c.Param("question-id") + + // Verify user identity + session := sessions.Default(c) + userid, exists := session.Get("user_id").(string) + if !exists { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"}) + return + } + + question, err := db.GetQuestionByQuestionID(questionID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question-id"}) + return + } + + if question.UserID != userid { + c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"}) + return + } + + // Validate answer-id + answer, err := db.GetAnswerByAnswerID(answerID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer-id"}) + return + } + + if answer.QuestionID != questionID { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid answer-id"}) + return + } + + // Set the best answer + err = db.UpdateBestAnswer(questionID, answerID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "update best answer failed"}) + return + } + + c.JSON(http.StatusOK, gin.H{"msg": "update best answer successful!"}) +} diff --git a/l1nk4i/api/question/create.go b/l1nk4i/api/question/create.go new file mode 100644 index 0000000..6cbbbbe --- /dev/null +++ b/l1nk4i/api/question/create.go @@ -0,0 +1,42 @@ +package question + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "l1nk4i/db" + "net/http" +) + +func Create(c *gin.Context) { + var questionInfo struct { + Title string `json:"title"` + Content string `json:"content"` + } + + if err := c.ShouldBind(&questionInfo); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"}) + return + } + + session := sessions.Default(c) + userID, exists := session.Get("user_id").(string) + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session"}) + return + } + + question := db.Question{ + QuestionID: uuid.New().String(), + UserID: userID, + Title: questionInfo.Title, + Content: questionInfo.Content, + } + if err := db.CreateQuestion(&question); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "create question error"}) + return + } + + c.JSON(http.StatusOK, gin.H{"question_id": question.QuestionID}) + return +} diff --git a/l1nk4i/api/question/delete.go b/l1nk4i/api/question/delete.go new file mode 100644 index 0000000..6394260 --- /dev/null +++ b/l1nk4i/api/question/delete.go @@ -0,0 +1,40 @@ +package question + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "l1nk4i/db" + "net/http" +) + +func Delete(c *gin.Context) { + questionID := c.Param("question-id") + + // Verify user identity + session := sessions.Default(c) + userid, exists := session.Get("user_id").(string) + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session"}) + return + } + + question, err := db.GetQuestionByQuestionID(questionID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"}) + return + } + + if question.UserID != userid { + c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"}) + return + } + + // Delete question + err = db.DeleteQuestion(questionID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "delete question error"}) + return + } + + c.JSON(http.StatusOK, gin.H{"msg": "delete question successful!"}) +} diff --git a/l1nk4i/api/question/get.go b/l1nk4i/api/question/get.go new file mode 100644 index 0000000..f372bd4 --- /dev/null +++ b/l1nk4i/api/question/get.go @@ -0,0 +1,20 @@ +package question + +import ( + "github.com/gin-gonic/gin" + "l1nk4i/db" + "net/http" +) + +// Get gets question by question_id +func Get(c *gin.Context) { + questionID := c.Param("question-id") + + question, err := db.GetQuestionByQuestionID(questionID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"}) + return + } + + c.JSON(http.StatusOK, gin.H{"data": question}) +} diff --git a/l1nk4i/api/question/list.go b/l1nk4i/api/question/list.go new file mode 100644 index 0000000..43d8468 --- /dev/null +++ b/l1nk4i/api/question/list.go @@ -0,0 +1,31 @@ +package question + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "l1nk4i/db" + "net/http" +) + +// List lists user's all question_id +func List(c *gin.Context) { + session := sessions.Default(c) + userID, exists := session.Get("user_id").(string) + if !exists { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"}) + return + } + + questions, err := db.GetQuestionByUserID(userID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"}) + return + } + + var questionIDs []string + for _, question := range *questions { + questionIDs = append(questionIDs, question.QuestionID) + } + + c.JSON(http.StatusOK, gin.H{"question_id": questionIDs}) +} diff --git a/l1nk4i/api/question/search.go b/l1nk4i/api/question/search.go new file mode 100644 index 0000000..0af64bf --- /dev/null +++ b/l1nk4i/api/question/search.go @@ -0,0 +1,18 @@ +package question + +import ( + "github.com/gin-gonic/gin" + "l1nk4i/db" + "net/http" +) + +func Search(c *gin.Context) { + searchContent := c.Query("content") + + questions, err := db.SearchQuestions(searchContent) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid content"}) + } + + c.JSON(http.StatusOK, gin.H{"data": questions}) +} diff --git a/l1nk4i/api/question/update.go b/l1nk4i/api/question/update.go new file mode 100644 index 0000000..5bc6d71 --- /dev/null +++ b/l1nk4i/api/question/update.go @@ -0,0 +1,50 @@ +package question + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "l1nk4i/db" + "net/http" +) + +func Update(c *gin.Context) { + questionID := c.Param("question-id") + + var questionInfo struct { + Title string `json:"title"` + Content string `json:"content"` + } + + if err := c.ShouldBind(&questionInfo); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"}) + return + } + + // Verify user identity + session := sessions.Default(c) + userid, exists := session.Get("user_id").(string) + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session"}) + return + } + + question, err := db.GetQuestionByQuestionID(questionID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid question_id"}) + return + } + + if question.UserID != userid { + c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"}) + return + } + + // Update question + err = db.UpdateQuestion(questionID, questionInfo.Title, questionInfo.Content) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "update question error"}) + return + } + + c.JSON(http.StatusOK, gin.H{"msg": "update question successful!"}) +} diff --git a/l1nk4i/api/user/login.go b/l1nk4i/api/user/login.go new file mode 100644 index 0000000..cbe3bff --- /dev/null +++ b/l1nk4i/api/user/login.go @@ -0,0 +1,51 @@ +package user + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "l1nk4i/db" + "l1nk4i/utils" + "net/http" +) + +func Login(c *gin.Context) { + var loginInfo struct { + Username string `json:"username"` + Password string `json:"password"` + } + + if err := c.ShouldBind(&loginInfo); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"}) + return + } + + if !validateUsername(loginInfo.Username) { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid Username"}) + return + } + + if !validatePassword(loginInfo.Password) { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid Password"}) + return + } + + if user, err := db.GetUserByUsername(loginInfo.Username); err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid Username"}) + return + } else { + if utils.CheckPasswordHash(loginInfo.Password, user.Password) { + session := sessions.Default(c) + session.Clear() + session.Set("user_id", user.UserID) + session.Set("role", user.Role) + session.Save() + + c.JSON(http.StatusOK, gin.H{"msg": "login successful!"}) + return + } else { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid Password"}) + return + } + } + +} diff --git a/l1nk4i/api/user/logout.go b/l1nk4i/api/user/logout.go new file mode 100644 index 0000000..058eabf --- /dev/null +++ b/l1nk4i/api/user/logout.go @@ -0,0 +1,16 @@ +package user + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "net/http" +) + +func Logout(c *gin.Context) { + session := sessions.Default(c) + session.Clear() + session.Set("role", "guest") + session.Save() + c.JSON(http.StatusOK, gin.H{"msg": "Logout successful!"}) + return +} diff --git a/l1nk4i/api/user/register.go b/l1nk4i/api/user/register.go new file mode 100644 index 0000000..8b184ef --- /dev/null +++ b/l1nk4i/api/user/register.go @@ -0,0 +1,50 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "l1nk4i/db" + "l1nk4i/utils" + "net/http" +) + +func Register(c *gin.Context) { + var registerInfo struct { + Username string `json:"username"` + Password string `json:"password"` + } + + if err := c.ShouldBindJSON(®isterInfo); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"}) + return + } + + if !validateUsername(registerInfo.Username) { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid Username"}) + return + } + + if !validatePassword(registerInfo.Password) { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid Password"}) + return + } + + exists, _ := db.GetUserByUsername(registerInfo.Username) + if exists != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "username exists"}) + return + } + + user := db.User{ + UserID: uuid.New().String(), + Username: registerInfo.Username, + Password: utils.HashPassword(registerInfo.Password), + Role: "user", + } + if err := db.CreateUser(&user); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Create user error"}) + return + } + + c.JSON(http.StatusOK, gin.H{"msg": "register successful!"}) +} diff --git a/l1nk4i/api/user/userinfo.go b/l1nk4i/api/user/userinfo.go new file mode 100644 index 0000000..a277afa --- /dev/null +++ b/l1nk4i/api/user/userinfo.go @@ -0,0 +1,36 @@ +package user + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "l1nk4i/db" + "net/http" +) + +// UserInfo get Username by session +func UserInfo(c *gin.Context) { + session := sessions.Default(c) + userid, exists := session.Get("user_id").(string) + if !exists { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"}) + return + } + + if _, err := uuid.Parse(userid); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session"}) + return + } + + user, err := db.GetUserByUUID(userid) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "user not found"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "user_id": user.UserID, + "username": user.Username, + "role": user.Role, + }) +} diff --git a/l1nk4i/api/user/utils.go b/l1nk4i/api/user/utils.go new file mode 100644 index 0000000..5c7fb0a --- /dev/null +++ b/l1nk4i/api/user/utils.go @@ -0,0 +1,25 @@ +package user + +import "regexp" + +// 2 <= length <= 20 +// ^[a-zA-Z0-9]+$ +func validateUsername(username string) bool { + if len(username) < 2 || len(username) > 20 { + return false + } + + re := regexp.MustCompile(`^[a-zA-Z0-9]+$`) + return re.MatchString(username) +} + +// 8 <= length <= 30 +// ^[a-zA-Z0-9\W_]+$ +func validatePassword(password string) bool { + if len(password) < 8 || len(password) > 30 { + return false + } + + re := regexp.MustCompile(`^[a-zA-Z0-9\W_]+$`) + return re.MatchString(password) +} diff --git a/l1nk4i/config/config.go b/l1nk4i/config/config.go new file mode 100644 index 0000000..295c611 --- /dev/null +++ b/l1nk4i/config/config.go @@ -0,0 +1,24 @@ +package config + +import "github.com/pelletier/go-toml" + +var Mysql struct { + Username string `toml:"username"` + Password string `toml:"password"` + Host string `toml:"host"` + Port string `toml:"port"` + Dbname string `toml:"dbname"` +} + +var configFile = "config.toml" + +func init() { + conf, err := toml.LoadFile(configFile) + if err != nil { + panic("load config file failed: " + err.Error()) + } + + if err := conf.Get("mysql").(*toml.Tree).Unmarshal(&Mysql); err != nil { + panic("unmarshal config file failed: " + err.Error()) + } +} diff --git a/l1nk4i/db/answer.go b/l1nk4i/db/answer.go new file mode 100644 index 0000000..137f428 --- /dev/null +++ b/l1nk4i/db/answer.go @@ -0,0 +1,50 @@ +package db + +import "log" + +func CreateAnswer(answer *Answer) error { + err := db.Create(answer).Error + if err != nil { + log.Printf("[ERROR] Create answer error:%s\n", err.Error()) + return err + } + return nil +} + +func GetAnswerByAnswerID(answerID string) (*Answer, error) { + var answer Answer + err := db.Where("answer_id = ?", answerID).First(&answer).Error + if err != nil { + log.Printf("[ERROR] Get answer by answerID error:%s\n", err.Error()) + return nil, err + } + return &answer, nil +} + +func GetAnswersByQuestionID(questionID string) (*[]Answer, error) { + var answers []Answer + err := db.Where("question_id = ?", questionID).Limit(100).Find(&answers).Error + if err != nil { + log.Printf("[ERROR] Get answer by questionID error:%s\n", err.Error()) + return nil, err + } + return &answers, nil +} + +func DeleteAnswer(answerID string) error { + err := db.Unscoped().Where("answer_id = ?", answerID).Delete(&Answer{}).Error + if err != nil { + log.Printf("[ERROR] Delete answer error:%s\n", err.Error()) + return err + } + return nil +} + +func UpdateAnswer(answerID, content string) error { + err := db.Model(&Answer{}).Where("answer_id = ?", answerID).Update("content", content).Error + if err != nil { + log.Printf("[ERROR] Update answer error:%s\n", err.Error()) + return err + } + return nil +} diff --git a/l1nk4i/db/mysql.go b/l1nk4i/db/mysql.go new file mode 100644 index 0000000..e449827 --- /dev/null +++ b/l1nk4i/db/mysql.go @@ -0,0 +1,63 @@ +package db + +import ( + "fmt" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "l1nk4i/config" + "log" +) + +type User struct { + gorm.Model + UserID string `gorm:"not null;column:user_id;unique;type:varchar(36)"` + + Username string `gorm:"not null;column:username;unique"` + Password string `gorm:"not null;column:password"` + Role string `gorm:"not null;column:role"` +} + +type Question struct { + gorm.Model + QuestionID string `gorm:"not null;column:question_id;unique;type:varchar(36)"` + UserID string `gorm:"not null;column:user_id;type:varchar(36)"` + + BestAnswerID string `gorm:"column:best_answer_id;type:varchar(36)"` + //IsAccessible bool `gorm:"not null;column:is_accessible;type:bool;default:false"` + + Title string `gorm:"not null;column:title"` + Content string `gorm:"not null;column:content"` +} + +type Answer struct { + gorm.Model + AnswerID string `gorm:"not null;column:answer_id;unique;type:varchar(36)"` + UserID string `gorm:"not null;column:user_id;type:varchar(36)"` + QuestionID string `gorm:"not null;column:question_id;"` + + Content string `gorm:"not null;column:content"` +} + +var db *gorm.DB + +func init() { + username := config.Mysql.Username + password := config.Mysql.Password + host := config.Mysql.Host + port := config.Mysql.Port + dbname := config.Mysql.Dbname + params := "charset=utf8mb4&parseTime=True&loc=Local" + + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?%s", username, password, host, port, dbname, params) + conn, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + panic("Cannot connect to mysql: " + err.Error()) + } + log.Printf("[INFO] Connect to mysql successfully\n") + + if err = conn.AutoMigrate(&User{}, &Question{}, &Answer{}); err != nil { + panic("AutoMigrate failed: " + err.Error()) + } + + db = conn +} diff --git a/l1nk4i/db/question.go b/l1nk4i/db/question.go new file mode 100644 index 0000000..5ab2dfc --- /dev/null +++ b/l1nk4i/db/question.go @@ -0,0 +1,77 @@ +package db + +import "log" + +func CreateQuestion(question *Question) error { + err := db.Create(question).Error + if err != nil { + log.Printf("[ERROR] Create question failed %s\n", err.Error()) + return err + } + return nil +} + +func GetQuestionByQuestionID(questionID string) (*Question, error) { + var question Question + err := db.Where("question_id = ?", questionID).First(&question).Error + if err != nil { + log.Printf("[ERROR] Get question by question_id failed %s\n", err.Error()) + return nil, err + } + return &question, nil +} + +func GetQuestionByUserID(userID string) (*[]Question, error) { + var questions []Question + err := db.Where("user_id = ?", userID).Limit(100).Find(&questions).Error + if err != nil { + log.Printf("[ERROR] Get Questions by user_id failed %s\n", err.Error()) + return nil, err + } + return &questions, nil +} + +func DeleteQuestion(questionID string) error { + err := db.Unscoped().Where("question_id = ?", questionID).Delete(&Question{}).Error + if err != nil { + log.Printf("[ERROR] Delete question failed %s\n", err.Error()) + return err + } + + // Delete answers + err = db.Unscoped().Where("question_id = ?", questionID).Delete(&Answer{}).Error + if err != nil { + log.Printf("[ERROR] Delete answers error:%s\n", err.Error()) + return err + } + return nil +} + +func UpdateQuestion(questionID, title, content string) error { + err := db.Model(&Question{}).Where("question_id = ?", questionID).Updates(Question{Title: title, Content: content}).Error + if err != nil { + log.Printf("[ERROR] Update question failed %s\n", err.Error()) + return err + } + return nil +} + +func SearchQuestions(content string) (*[]Question, error) { + var questions []Question + searchPattern := "%" + content + "%" + err := db.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern).Limit(20).Find(&questions).Error + if err != nil { + log.Printf("[ERROR] Search questions failed %s\n", err.Error()) + return nil, err + } + return &questions, nil +} + +func UpdateBestAnswer(questionID, answerID string) error { + err := db.Model(&Question{}).Where("question_id = ?", questionID).Updates(Question{BestAnswerID: answerID}).Error + if err != nil { + log.Printf("[ERROR] Update question benst answer failed %s\n", err.Error()) + return err + } + return nil +} diff --git a/l1nk4i/db/user.go b/l1nk4i/db/user.go new file mode 100644 index 0000000..ddc4ed5 --- /dev/null +++ b/l1nk4i/db/user.go @@ -0,0 +1,32 @@ +package db + +import "log" + +func CreateUser(user *User) error { + err := db.Create(user).Error + if err != nil { + log.Printf("[ERROR] Create user failed: %s\n", err.Error()) + return err + } + return nil +} + +func GetUserByUsername(username string) (*User, error) { + var user User + err := db.Where("username = ?", username).First(&user).Error + if err != nil { + log.Printf("[ERROR] Get user failed: %s\n", err.Error()) + return nil, err + } + return &user, nil +} + +func GetUserByUUID(uuid string) (*User, error) { + var user User + err := db.Where("user_id = ?", uuid).First(&user).Error + if err != nil { + log.Printf("[ERROR] Get user failed: %s\n", err.Error()) + return nil, err + } + return &user, nil +} diff --git a/l1nk4i/docker-compose.yml b/l1nk4i/docker-compose.yml new file mode 100644 index 0000000..f6acf33 --- /dev/null +++ b/l1nk4i/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + db: + image: mysql:latest + environment: + MYSQL_RANDOM_ROOT_PASSWORD: true + volumes: + - /home/l1nk/mysql_data:/var/lib/mysql + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + expose: + - "3306" + - "33060" + ports: + - "3306:3306" # debug + + + app: + build: . + ports: + - "8080:8080" + depends_on: + - db diff --git a/l1nk4i/docs/apiDoc.md b/l1nk4i/docs/apiDoc.md new file mode 100644 index 0000000..6a849ca --- /dev/null +++ b/l1nk4i/docs/apiDoc.md @@ -0,0 +1,890 @@ +--- +title: hduhelp_backend_task +language_tabs: + - shell: Shell + - http: HTTP + - javascript: JavaScript + - ruby: Ruby + - python: Python + - php: PHP + - java: Java + - go: Go +toc_footers: [] +includes: [] +search: true +code_clipboard: true +highlight_theme: darkula +headingLevel: 2 +generator: "@tarslib/widdershins v4.0.23" + +--- + +# hduhelp_backend_task + +Base URLs: + +# Authentication + +# users + +## POST 用户注册 + +POST /api/users/register + +注册 + +> Body 请求参数 + +```json +{ + "username": "l1nkQAQ", + "password": "vidarteam" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|body|body|object| 否 |none| +|» username|body|string| 是 |none| +|» password|body|string| 是 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "msg": "string" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|none|Inline| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» msg|string|true|none||none| + +状态码 **400** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» error|string|true|none||none| + +状态码 **500** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» error|string|true|none||none| + +## GET 获取当前用户信息 + +GET /api/users/userinfo + +查询当前用户信息 + +> 返回示例 + +> 200 Response + +```json +{ + "role": "string", + "user_id": "string", + "username": "string" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» role|string|true|none||none| +|» user_id|string|true|none||none| +|» username|string|true|none||none| + +状态码 **400** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» error|string|true|none||none| + +## POST 用户登录 + +POST /api/users/login + +登录 + +> Body 请求参数 + +```json +{ + "username": "l1nkQAQ", + "password": "vidarteam" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|body|body|object| 否 |none| +|» username|body|string| 是 |none| +|» password|body|string| 是 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "msg": "string" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» msg|string|true|none||none| + +## POST 用户登出 + +POST /api/users/logout + +登出 + +> 返回示例 + +> 200 Response + +```json +{ + "msg": "string" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» msg|string|true|none||none| + +# questions + +## POST 创建问题 + +POST /api/questions + +创建问题 + +> Body 请求参数 + +```json +{ + "title": "man", + "content": "what can i say" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|body|body|object| 否 |none| +|» title|body|string| 是 |none| +|» content|body|string| 是 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "msg": "string" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» msg|string|true|none||none| + +## GET 列出用户发表的所有问题 + +GET /api/questions + +列出用户发表的所有问题 + +> 返回示例 + +> 200 Response + +```json +{ + "question_id": [ + "string" + ] +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» question_id|[string]|true|none||none| + +## POST 创建指定问题的回答 + +POST /api/questions/{question-id}/answers + +发送回答 + +> Body 请求参数 + +```json +{ + "content": "manman" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|question-id|path|string| 是 |none| +|body|body|object| 否 |none| +|» question_id|body|string| 是 |none| +|» content|body|string| 是 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "answer_id": "string" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» answer_id|string|true|none||none| + +## GET 获取指定问题的所有回答 + +GET /api/questions/{question-id}/answers + +获得问题对应的所有回答 + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|question-id|path|string| 是 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "data": [ + { + "ID": 0, + "CreatedAt": "string", + "UpdatedAt": "string", + "DeletedAt": null, + "AnswerID": "string", + "UserID": "string", + "QuestionID": "string", + "Content": "string" + } + ] +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» data|[object]|true|none||none| +|»» ID|integer|true|none||none| +|»» CreatedAt|string|true|none||none| +|»» UpdatedAt|string|true|none||none| +|»» DeletedAt|null|true|none||none| +|»» AnswerID|string|true|none||none| +|»» UserID|string|true|none||none| +|»» QuestionID|string|true|none||none| +|»» Content|string|true|none||none| + +## DELETE 删除指定问题 + +DELETE /api/questions/{question-id} + +删除问题 + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|question-id|path|string| 是 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "msg": "string" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» msg|string|true|none||none| + +## PUT 更新指定问题 + +PUT /api/questions/{question-id} + +更改问题的标题或者内容 + +> Body 请求参数 + +```json +{ + "title": "manba", + "content": "out" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|question-id|path|string| 是 |none| +|body|body|object| 否 |none| +|» question_id|body|string| 是 |none| +|» title|body|string| 是 |none| +|» content|body|string| 是 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "msg": "string" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» msg|string|true|none||none| + +## GET 获取指定问题 + +GET /api/questions/{question-id} + +通过question_id查找对应问题对象 + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|question-id|path|string| 是 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "data": { + "ID": 0, + "CreatedAt": "string", + "UpdatedAt": "string", + "DeletedAt": null, + "QuestionID": "string", + "UserID": "string", + "BestAnswerID": "string", + "Title": "string", + "Content": "string" + } +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» data|object|true|none||none| +|»» ID|integer|true|none||none| +|»» CreatedAt|string|true|none||none| +|»» UpdatedAt|string|true|none||none| +|»» DeletedAt|null|true|none||none| +|»» QuestionID|string|true|none||none| +|»» UserID|string|true|none||none| +|»» BestAnswerID|string|true|none||none| +|»» Title|string|true|none||none| +|»» Content|string|true|none||none| + +## GET 搜索问题 + +GET /api/questions/search + +通过输入匹配问题标题和内容,搜索问题(无鉴权) + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|content|query|string| 否 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "data": [ + { + "ID": 0, + "CreatedAt": "string", + "UpdatedAt": "string", + "DeletedAt": null, + "QuestionID": "string", + "UserID": "string", + "BestAnswerID": "string", + "Title": "string", + "Content": "string" + } + ] +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» data|[object]|true|none||none| +|»» ID|integer|true|none||none| +|»» CreatedAt|string|true|none||none| +|»» UpdatedAt|string|true|none||none| +|»» DeletedAt|null|true|none||none| +|»» QuestionID|string|true|none||none| +|»» UserID|string|true|none||none| +|»» BestAnswerID|string|true|none||none| +|»» Title|string|true|none||none| +|»» Content|string|true|none||none| + +## PUT 更新最佳答案 + +PUT /api/questions/{question-id}/best/{answer-id} + +更新最佳答案 + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|question-id|path|string| 是 |none| +|answer-id|path|string| 是 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "msg": "string" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» msg|string|true|none||none| + +# answers + +## DELETE 删除指定回答 + +DELETE /api/answers/{answer-id} + +删除回答 + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|answer-id|path|string| 是 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "msg": "string" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» msg|string|true|none||none| + +## PUT 更新指定回答 + +PUT /api/answers/{answer-id} + +更改回答内容 + +> Body 请求参数 + +```json +{ + "content": "manba out" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|answer-id|path|string| 是 |none| +|body|body|object| 否 |none| +|» answer_id|body|string| 是 |none| +|» content|body|string| 是 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "msg": "string" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» msg|string|true|none||none| + +# admin + +## DELETE 删除问题 + +DELETE /api/admin/questions/{question-id} + +删除问题 + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|question-id|path|string| 是 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "msg": "string" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» msg|string|true|none||none| + +## DELETE 删除答案 + +DELETE /api/admin/answers/{answer-id} + +删除答案 + +### 请求参数 + +|名称|位置|类型|必选|说明| +|---|---|---|---|---| +|answer-id|path|string| 是 |none| + +> 返回示例 + +> 200 Response + +```json +{ + "msg": "string" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +状态码 **200** + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|» msg|string|true|none||none| + +# 数据模型 + +

User

+ + + + + + +```json +{ + "username": "string", + "password": "string", + "role": "string", + "created_at": "2019-08-24T14:15:22Z", + "deleted_at": "2019-08-24T14:15:22Z", + "id": 1, + "updated_at": "2019-08-24T14:15:22Z", + "user_id": "string" +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|username|string|true|none||none| +|password|string|true|none||none| +|role|string|true|none||none| +|created_at|string(date-time)|false|none||none| +|deleted_at|string(date-time)|false|none||none| +|id|integer|true|none||none| +|updated_at|string(date-time)|false|none||none| +|user_id|string|true|none||none| + +

Question

+ + + + + + +```json +{ + "title": "string", + "content": "string", + "best_answer_id": "string", + "created_at": "2019-08-24T14:15:22Z", + "deleted_at": "2019-08-24T14:15:22Z", + "id": 1, + "question_id": "string", + "updated_at": "2019-08-24T14:15:22Z", + "user_id": "string" +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|title|string|true|none||none| +|content|string|true|none||none| +|best_answer_id|string|false|none||none| +|created_at|string(date-time)|false|none||none| +|deleted_at|string(date-time)|false|none||none| +|id|integer|true|none||none| +|question_id|string|true|none||none| +|updated_at|string(date-time)|false|none||none| +|user_id|string|true|none||none| + +

Answer

+ + + + + + +```json +{ + "content": "string", + "answer_id": "string", + "created_at": "2019-08-24T14:15:22Z", + "deleted_at": "2019-08-24T14:15:22Z", + "id": 1, + "question_id": "string", + "updated_at": "2019-08-24T14:15:22Z", + "user_id": "string" +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|content|string|true|none||none| +|answer_id|string|true|none||none| +|created_at|string(date-time)|false|none||none| +|deleted_at|string(date-time)|false|none||none| +|id|integer|true|none||none| +|question_id|string|true|none||none| +|updated_at|string(date-time)|false|none||none| +|user_id|string|true|none||none| + diff --git a/l1nk4i/entrypoint.sh b/l1nk4i/entrypoint.sh new file mode 100644 index 0000000..17a3f78 --- /dev/null +++ b/l1nk4i/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# 循环执行 myapp +while true; do + ./myapp # 运行应用 + + # 检查上一个命令的退出状态 + if [ $? -eq 0 ]; then + echo "myapp executed successfully." + break # 如果成功,退出循环 + else + echo "myapp failed. Retrying in 2 seconds..." + sleep 2 # 等待 2 秒后重试 + fi +done diff --git a/l1nk4i/go.mod b/l1nk4i/go.mod new file mode 100644 index 0000000..ceedce0 --- /dev/null +++ b/l1nk4i/go.mod @@ -0,0 +1,48 @@ +module l1nk4i + +go 1.23.1 + +require ( + github.com/gin-contrib/sessions v1.0.1 + github.com/gin-gonic/gin v1.10.0 + golang.org/x/crypto v0.27.0 + gorm.io/driver/mysql v1.5.7 + gorm.io/gorm v1.25.12 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/bytedance/sonic v1.12.3 // indirect + github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.5 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.10.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/l1nk4i/go.sum b/l1nk4i/go.sum new file mode 100644 index 0000000..66a1deb --- /dev/null +++ b/l1nk4i/go.sum @@ -0,0 +1,114 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= +github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= +github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI= +github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= +github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= +golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/l1nk4i/init.sql b/l1nk4i/init.sql new file mode 100644 index 0000000..5b6ce06 --- /dev/null +++ b/l1nk4i/init.sql @@ -0,0 +1,4 @@ +CREATE DATABASE appdb; +CREATE USER 'app'@'%' IDENTIFIED BY 'safe_password'; +GRANT ALL PRIVILEGES ON appdb.* TO 'app'@'%'; +FLUSH PRIVILEGES; \ No newline at end of file diff --git a/l1nk4i/main.go b/l1nk4i/main.go new file mode 100644 index 0000000..e03e837 --- /dev/null +++ b/l1nk4i/main.go @@ -0,0 +1,7 @@ +package main + +import "l1nk4i/router" + +func main() { + router.Run() +} diff --git a/l1nk4i/router/router.go b/l1nk4i/router/router.go new file mode 100644 index 0000000..7fcb1ff --- /dev/null +++ b/l1nk4i/router/router.go @@ -0,0 +1,56 @@ +package router + +import ( + "crypto/rand" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" + "l1nk4i/api/admin" + "l1nk4i/api/answer" + "l1nk4i/api/question" + "l1nk4i/api/user" +) + +func Run() { + r := gin.Default() + + secret := make([]byte, 32) + _, _ = rand.Read(secret) + store := cookie.NewStore(secret) + r.Use(sessions.Sessions("session", store)) + + apiGroup := r.Group("/api") + { + userApiGroup := apiGroup.Group("/users") + { + userApiGroup.POST("/login", user.Login) + userApiGroup.POST("/register", user.Register) + userApiGroup.POST("/logout", user.Logout) + userApiGroup.GET("/userinfo", user.UserInfo) + } + questionApiGroup := apiGroup.Group("/questions") + { + questionApiGroup.POST("/", question.Create) + questionApiGroup.DELETE("/:question-id", question.Delete) + questionApiGroup.PUT("/:question-id", question.Update) + questionApiGroup.GET("/:question-id", question.Get) + questionApiGroup.GET("/", question.List) + questionApiGroup.GET("/search", question.Search) + questionApiGroup.POST("/:question-id/answers", answer.Create) + questionApiGroup.GET("/:question-id/answers", answer.Get) + questionApiGroup.PUT("/:question-id/best/:answer-id", question.Best) + } + answerApiGroup := apiGroup.Group("/answers") + { + answerApiGroup.DELETE("/:answer-id", answer.Delete) + answerApiGroup.PUT("/:answer-id", answer.Update) + } + adminApiGroup := apiGroup.Group("/admin") + { + adminApiGroup.DELETE("/questions/:question-id", admin.DeleteQuestion) + adminApiGroup.DELETE("/answers/:answer-id", admin.DeleteAnswer) + } + } + + r.Run(":8080") +} diff --git a/l1nk4i/utils/hash.go b/l1nk4i/utils/hash.go new file mode 100644 index 0000000..b069491 --- /dev/null +++ b/l1nk4i/utils/hash.go @@ -0,0 +1,15 @@ +package utils + +import ( + "golang.org/x/crypto/bcrypt" +) + +func HashPassword(password string) string { + hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(hashedPassword) +} + +func CheckPasswordHash(password, hashedPassword string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) + return err == nil +}