diff options
| author | Nicolas James <Eele1Ephe7uZahRie@tutanota.com> | 2025-02-13 18:04:18 +1100 |
|---|---|---|
| committer | Nicolas James <Eele1Ephe7uZahRie@tutanota.com> | 2025-02-13 18:04:18 +1100 |
| commit | 93dfe2be64e8658839bcfe5356adf35f8cde7075 (patch) | |
| tree | c60b1e20d569b74dbde85123e1b2bf3590c66244 /src/server/handlers | |
initial commit
Diffstat (limited to 'src/server/handlers')
| -rw-r--r-- | src/server/handlers/image.go | 41 | ||||
| -rw-r--r-- | src/server/handlers/login.go | 55 | ||||
| -rw-r--r-- | src/server/handlers/logout.go | 20 | ||||
| -rw-r--r-- | src/server/handlers/post.go | 121 | ||||
| -rw-r--r-- | src/server/handlers/posts.go | 60 | ||||
| -rw-r--r-- | src/server/handlers/refresh.go | 32 | ||||
| -rw-r--r-- | src/server/handlers/signup.go | 75 |
7 files changed, 404 insertions, 0 deletions
diff --git a/src/server/handlers/image.go b/src/server/handlers/image.go new file mode 100644 index 0000000..99c9c18 --- /dev/null +++ b/src/server/handlers/image.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "net/http" + "regexp" + "server/database" + "server/helper" + "strconv" +) + +func Image(writer http.ResponseWriter, request *http.Request) { + if request.Method != "GET" { + helper.WriteErrorJson("expected GET method", writer, http.StatusBadRequest) + return + } + + re := regexp.MustCompile(`^/image/([0-9]+).png$`) + submatches := re.FindStringSubmatch(request.URL.Path) + if len(submatches) != 2 { + http.Error(writer, "invalid URL", http.StatusBadRequest) + return + } + + uid, err := strconv.Atoi(submatches[1]) + if err != nil { + helper.WriteInternalError(err, writer) + return + } + + image_blob, err := database.GetImage(uid, request.URL.Query().Get("thumbnail") == "1") + if err != nil { + helper.WriteInternalError(err, writer) + return + } + if image_blob == nil { + http.Error(writer, "resource not found", http.StatusNotFound) + return + } + + writer.Write(image_blob) +} diff --git a/src/server/handlers/login.go b/src/server/handlers/login.go new file mode 100644 index 0000000..745e64a --- /dev/null +++ b/src/server/handlers/login.go @@ -0,0 +1,55 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "server/database" + "server/helper" +) + +type loginRequest struct { + Email string + Password string +} + +func Login(writer http.ResponseWriter, request *http.Request) { + if request.Method != "POST" { + helper.WriteErrorJson("expected POST method", writer, http.StatusBadRequest) + return + } + + var login_request loginRequest + err := json.NewDecoder(request.Body).Decode(&login_request) + if err != nil { + helper.WriteErrorJson(err.Error(), writer, http.StatusBadRequest) + return + } + + user, err := database.MaybeGetUser(login_request.Email) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } + if user == nil { + helper.WriteErrorJson("incorrect email or password", writer, http.StatusForbidden) + return + } + + hash, err := helper.GenerateHash(login_request.Password, user.Password_salt) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } + if hash != user.Password_hash { + helper.WriteErrorJson("incorrect email or password", writer, http.StatusForbidden) + return + } + + // Login is successful, issue a valid jwt. + err = helper.IssueToken(user.Uid, writer) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } +} diff --git a/src/server/handlers/logout.go b/src/server/handlers/logout.go new file mode 100644 index 0000000..d3c8b9b --- /dev/null +++ b/src/server/handlers/logout.go @@ -0,0 +1,20 @@ +package handlers + +import ( + "net/http" + "server/helper" + "time" +) + +func Logout(writer http.ResponseWriter, request *http.Request) { + if request.Method != "POST" { + helper.WriteErrorJson("expected POST method", writer, http.StatusBadRequest) + return + } + + http.SetCookie(writer, &http.Cookie{ + Name: "token", + Expires: time.Time{}, // zero value for time + Path: "/", + }) +} diff --git a/src/server/handlers/post.go b/src/server/handlers/post.go new file mode 100644 index 0000000..2c014ca --- /dev/null +++ b/src/server/handlers/post.go @@ -0,0 +1,121 @@ +package handlers + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "image" + _ "image/gif" + _ "image/jpeg" + "image/png" + "strconv" + + "golang.org/x/image/draw" + + "net/http" + "server/database" + "server/helper" +) + +const thumbnail_size = 128 + +func makeThumbnail(source image.Image) ([]byte, error) { + dest := image.NewRGBA(image.Rect(0, 0, thumbnail_size, thumbnail_size)) + draw.NearestNeighbor.Scale(dest, dest.Rect, source, source.Bounds(), draw.Over, nil) + + var buffer bytes.Buffer + err := png.Encode(&buffer, dest) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +type postRequest struct { + Title string + Contents string + Subreact string + File string +} + +type postResponse struct { + Uid int `json:"uid"` +} + +func Post(writer http.ResponseWriter, request *http.Request) { + if request.Method != "POST" { + helper.WriteErrorJson("expected GET method", writer, http.StatusBadRequest) + return + } + + claims := helper.GetValidClaims(writer, request) + if claims == nil { + return + } + user_uid, err := strconv.Atoi(claims.Subject) // TODO UID + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } + + var post_request postRequest + err = json.NewDecoder(request.Body).Decode(&post_request) + if err != nil { + helper.WriteErrorJson(err.Error(), writer, http.StatusBadRequest) + return + } + + post_request.Title = helper.CleanTitle(post_request.Title) + if !helper.IsValidRange(post_request.Title, "title", 8, 128, writer) { + return + } + + post_request.Contents = helper.CleanContents(post_request.Contents) + if !helper.IsValidRange(post_request.Contents, "contents", 8, 2048, writer) { + return + } + + if !helper.IsValidSubreact(post_request.Subreact, writer) { + return + } + + image_blob, err := base64.StdEncoding.DecodeString(post_request.File) + if err != nil { + helper.WriteErrorJson("failed to decode image from base64", writer, http.StatusBadRequest) + return + } + image, format, err := image.Decode(bytes.NewReader(image_blob)) + if err != nil { + helper.WriteErrorJson("failed to decode image", writer, http.StatusBadRequest) + return + } + if !helper.IsImageSupported(image, format, writer) { + return + } + + thumbnail_blob, err := makeThumbnail(image) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } + + image_uid, err := database.WritePost(user_uid, + nil, + post_request.Title, + post_request.Contents, + post_request.Subreact, + image_blob, + thumbnail_blob) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } + + resp, err := json.Marshal(postResponse{Uid: image_uid}) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } + + writer.Write(resp) +} diff --git a/src/server/handlers/posts.go b/src/server/handlers/posts.go new file mode 100644 index 0000000..00144db --- /dev/null +++ b/src/server/handlers/posts.go @@ -0,0 +1,60 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "strconv" + + "server/database" + "server/helper" +) + +type postsResponse struct { + Posts []database.Post `json:"posts"` +} + +func Posts(writer http.ResponseWriter, request *http.Request) { + if request.Method != "GET" { + helper.WriteErrorJson("expected GET method", writer, http.StatusBadRequest) + return + } + + page, err := strconv.Atoi(request.URL.Query().Get("page")) + if err != nil { + helper.WriteErrorJson("expected page parameter", writer, http.StatusBadRequest) + return + } + if page < 0 { + helper.WriteErrorJson("expected page parameter >= 0", writer, http.StatusBadRequest) + return + } + + amount, err := strconv.Atoi(request.URL.Query().Get("amount")) + if err != nil { + helper.WriteErrorJson("expected amount parameter", writer, http.StatusBadRequest) + return + } + if amount <= 0 { + helper.WriteErrorJson("expected amount parameter > 0", writer, http.StatusBadRequest) + return + } + + subreact := request.URL.Query().Get("subreact") + if !helper.IsValidSubreact(subreact, writer) { + return + } + + posts, err := database.GetPosts(subreact, page, amount) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } + + resp, err := json.Marshal(postsResponse{Posts: posts}) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } + + writer.Write(resp) +} diff --git a/src/server/handlers/refresh.go b/src/server/handlers/refresh.go new file mode 100644 index 0000000..bc60aa7 --- /dev/null +++ b/src/server/handlers/refresh.go @@ -0,0 +1,32 @@ +package handlers + +import ( + "net/http" + "strconv" + + "server/helper" +) + +// Extends the current token's lifetime (by replacing it with a newer one). +func Refresh(writer http.ResponseWriter, request *http.Request) { + if request.Method != "POST" { + helper.WriteErrorJson("expected POST method", writer, http.StatusBadRequest) + return + } + + claims := helper.GetValidClaims(writer, request) + if claims == nil { + return + } + + uid, err := strconv.Atoi(claims.Subject) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } + err = helper.IssueToken(uid, writer) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } +} diff --git a/src/server/handlers/signup.go b/src/server/handlers/signup.go new file mode 100644 index 0000000..e92b869 --- /dev/null +++ b/src/server/handlers/signup.go @@ -0,0 +1,75 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "server/database" + "server/helper" +) + +type signupRequest struct { + Email string + Password string +} + +func Signup(writer http.ResponseWriter, request *http.Request) { + if request.Method != "POST" { + helper.WriteErrorJson("expected POST method", writer, http.StatusBadRequest) + return + } + + var signup_request signupRequest + err := json.NewDecoder(request.Body).Decode(&signup_request) + if err != nil { + helper.WriteErrorJson(err.Error(), writer, http.StatusBadRequest) + return + } + + if len(signup_request.Email) < 3 || len(signup_request.Email) > 254 { + helper.WriteErrorJson("invalid email address", writer, http.StatusBadRequest) + return + } + if len(signup_request.Password) < 8 { + helper.WriteErrorJson("password too short", writer, http.StatusBadRequest) + return + } + if len(signup_request.Password) > 64 { + helper.WriteErrorJson("password too long", writer, http.StatusBadRequest) + return + } + + user, err := database.MaybeGetUser(signup_request.Email) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } + if user != nil { + helper.WriteErrorJson("a user with that email already exists", writer, http.StatusForbidden) + return + } + + salt, err := helper.GenerateSalt() + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } + + hash, err := helper.GenerateHash(signup_request.Password, salt) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } + + uid, err := database.WriteNewUser(signup_request.Email, hash, salt) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } + + err = helper.IssueToken(uid, writer) + if err != nil { + helper.WriteInternalErrorJson(err, writer) + return + } +} |
