aboutsummaryrefslogtreecommitdiff
path: root/src/server/handlers
diff options
context:
space:
mode:
authorNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-13 18:04:18 +1100
committerNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-13 18:04:18 +1100
commit93dfe2be64e8658839bcfe5356adf35f8cde7075 (patch)
treec60b1e20d569b74dbde85123e1b2bf3590c66244 /src/server/handlers
initial commit
Diffstat (limited to 'src/server/handlers')
-rw-r--r--src/server/handlers/image.go41
-rw-r--r--src/server/handlers/login.go55
-rw-r--r--src/server/handlers/logout.go20
-rw-r--r--src/server/handlers/post.go121
-rw-r--r--src/server/handlers/posts.go60
-rw-r--r--src/server/handlers/refresh.go32
-rw-r--r--src/server/handlers/signup.go75
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
+ }
+}