Skip to content

Commit 00f590c

Browse files
authored
Merge pull request #2 from wneessen/v0.1.2_refactor
v0.1.2: Complete refactor of crucial code
2 parents 582a9f9 + 69e117b commit 00f590c

File tree

12 files changed

+205
-145
lines changed

12 files changed

+205
-145
lines changed

README.md

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ Again the JSON syntax of the form configuration is very simple, yet flexible.
6969

7070
```json
7171
{
72-
"id": 1,
72+
"id": "test_form",
7373
"secret": "SuperSecretsString",
7474
"recipients": ["who@cares.net"],
7575
"sender": "website@example.com",
@@ -89,7 +89,7 @@ Again the JSON syntax of the form configuration is very simple, yet flexible.
8989
}
9090
}
9191
```
92-
* `id (type: int)`: The id of the form (will be looked for in the `formid` parameter of the submission)
92+
* `id (type: string)`: The id of the form (will be looked for in the `formid` parameter of the token request)
9393
* `secret (type: string)`: Secret for the form. This will be used for the token generation
9494
* `recipients (type: []string)`: List of recipients, that should receive the mails with the submitted form data
9595
* `domains (type: []string)`: List of origin domains, that are allowed to use this form
@@ -110,41 +110,77 @@ Again the JSON syntax of the form configuration is very simple, yet flexible.
110110
`JS-Mailer` follows a two-step workflow. First your JavaScript requests a token from the API using the `/api/v1/token`
111111
endpoint. If the request is valid and website is authorized to request a token, the API will respond with a
112112
TokenResponseJson. This holds some data, which needs to be included into your form as hidden inputs. It will also
113-
provide a submission URL endpoint `/api/v1/send` that can be used as action in your form. Once the form is submitted,
114-
the API will then validate that all submitted data is correct and submit the form data to the configured recipients.
113+
provide a submission URL endpoint `/api/v1/send/<formid>/<token>` that can be used as action in your form. Once the form
114+
is submitted, the API will then validate that all submitted data is correct and submit the form data to the configured
115+
recipients.
115116

116-
### API responses
117-
#### Token request
118-
The API response to a token request (`/api/v1/token`) looks like this:
117+
## API responses
118+
The API basically responds with two different types of JSON objects. A `success` response or an `error` response.
119+
120+
### Success response
121+
The succss response JSON struct is very simple:
122+
123+
```json
124+
{
125+
"status_code": 200,
126+
"status": "Ok",
127+
"data": {}
128+
}
129+
```
130+
* `status_code (type: uint32)`: The HTTP status code of the success response
131+
* `status (type: string)`: The HTTP status string of the success response
132+
* `data (type: object)`: An object with abritrary data, based on the type of response
133+
134+
#### Successful token retrieval data object
135+
The `data` object of the success response for a successful token retrieval looks like this:
119136

120137
```json
121138
{
122-
"token": "0587ba3fff63ce2c54af0320b5a2d06612a0200aa139c1c150cbfae8a17084a8",
123-
"create_time": 1628633657,
124-
"expire_time": 1628634257,
125-
"form_id": 1,
126-
"url": "https://jsmailer.example.com/api/v1/send"
139+
"token": "5b19fca2b154a2681f8d6014c63b5f81bdfdd01036a64f8a835465ab5247feff",
140+
"form_id": "test_form",
141+
"create_time": 1628670201,
142+
"expire_time": 1628670801,
143+
"url": "https://jsmailer.example.com/api/v1/send/test_form/5b19fca2b154a2681f8d6014c63b5f81bdfdd01036a64f8a835465ab5247feff",
144+
"enc_type": "multipart/form-data",
145+
"method": "post"
127146
}
128147
```
129-
* `token (type: string)`: The security token that needs to be part of the actual form sending request
148+
* `token (type: string)`: The security token of this send request
149+
* `form_id (type: string)`: The form id of the current form (for reference or automatic inclusion via JS)
130150
* `create_time (type: int64)`: The epoch timestamp when the token was created
131151
* `expire_time (type: int64)`: The epoch timestamp when the token will expire
132-
* `form_id (type: uint)`: The form id of the current form (for reference or automatic inclusion via JS)
133152
* `url (type: string)`: API endpoint to set your form action to
153+
* `enc_type (type: string)`: The enctype for your form
154+
* `method (type: string)`: The method for your form
134155

135-
#### Send response
156+
#### Sent successful data object
157+
The `data` object of the success response for a successfully sent message looks like this:
136158

137-
The API response to a send request (`/api/v1/send`) looks like this:
159+
The API response to a send request (`/api/v1/send/<formid>/<token>`) looks like this:
138160
```json
139161
{
140-
"status_code": 200,
141-
"success_message": "Message successfully sent",
142-
"form_id": 1
162+
"form_id": "test_form",
163+
"send_time": 1628670331
164+
}
165+
```
166+
* `form_id (type: string)`: The form id of the current form (for reference)
167+
* `send_time (type: int64)`: The epoch timestamp when the message was sent
168+
169+
### Error response
170+
The error response JSON struct is also very simple:
171+
172+
```json
173+
{
174+
"status_code": 404,
175+
"status": "Not Found",
176+
"error_message": "Validation failed",
177+
"error_data": "Not a valid send URL"
143178
}
144179
```
145-
* `status_code (type: uint32)`: The HTTP status code
146-
* `success_message (type: string)`: The success message
147-
* `form_id (type: uint)`: The form id of the current form (for reference)
180+
* `status_code (type: uint32)`: The HTTP status code of the success response
181+
* `status (type: string)`: The HTTP status string of the success response
182+
* `error_message (type: string)`: The general error message why this request failed
183+
* `error_data (type: interface{})`: Optional details in addtion to the error message (i. e. missing fields)
148184

149185
## Example implementation
150186

apirequest/apirequest.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import (
44
"github.com/ReneKroon/ttlcache/v2"
55
log "github.com/sirupsen/logrus"
66
"github.com/wneessen/js-mailer/config"
7+
"github.com/wneessen/js-mailer/form"
78
"github.com/wneessen/js-mailer/response"
89
"net/http"
10+
"strings"
911
)
1012

1113
// ApiRequest reflects a new Api request object
@@ -14,6 +16,9 @@ type ApiRequest struct {
1416
Config *config.Config
1517
IsHttps bool
1618
Scheme string
19+
FormId string
20+
Token string
21+
FormObj *form.Form
1722
}
1823

1924
// RequestHandler handles an incoming HTTP request on the API routes and
@@ -41,14 +46,25 @@ func (a *ApiRequest) RequestHandler(w http.ResponseWriter, r *http.Request) {
4146
// Set general response header
4247
w.Header().Set("Access-Control-Allow-Origin", "*")
4348

44-
switch r.URL.String() {
45-
case "/api/v1/token":
49+
switch {
50+
case r.URL.String() == "/api/v1/token":
4651
a.GetToken(w, r)
4752
return
48-
case "/api/v1/send":
53+
case strings.HasPrefix(r.URL.String(), "/api/v1/send/"):
54+
code, err := a.SendFormParse(r)
55+
if err != nil {
56+
l.Errorf("Failed to parse send request: %s", err)
57+
response.ErrorJsonData(w, code, "Failed parsing send request", err.Error())
58+
return
59+
}
60+
code, err = a.SendFormValidate(r)
61+
if err != nil {
62+
response.ErrorJsonData(w, code, "Validation failed", err.Error())
63+
return
64+
}
4965
a.SendForm(w, r)
5066
return
5167
default:
52-
response.ErrorJson(w, 404, "Not found")
68+
response.ErrorJson(w, 404, "Unknown API route")
5369
}
5470
}

apirequest/form.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func (a *ApiRequest) GetForm(i string) (form.Form, error) {
2121
if err != nil {
2222
return formObj, err
2323
}
24-
if err := a.Cache.Set(fmt.Sprintf("formObj_%d", formObj.Id), formObj); err != nil {
24+
if err := a.Cache.Set(fmt.Sprintf("formObj_%s", formObj.Id), formObj); err != nil {
2525
return formObj, err
2626
}
2727
}

apirequest/gettoken.go

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package apirequest
22

33
import (
44
"crypto/sha256"
5-
"encoding/json"
65
"fmt"
76
log "github.com/sirupsen/logrus"
87
"github.com/wneessen/js-mailer/response"
@@ -13,10 +12,12 @@ import (
1312
// TokenResponseJson reflects the JSON response struct for token request
1413
type TokenResponseJson struct {
1514
Token string `json:"token"`
15+
FormId string `json:"form_id"`
1616
CreateTime int64 `json:"create_time,omitempty"`
1717
ExpireTime int64 `json:"expire_time,omitempty"`
18-
FormId int `json:"form_id"`
1918
Url string `json:"url"`
19+
EncType string `json:"enc_type"`
20+
Method string `json:"method"`
2021
}
2122

2223
// GetToken handles the HTTP token requests and return a TokenResponseJson on success or
@@ -29,20 +30,20 @@ func (a *ApiRequest) GetToken(w http.ResponseWriter, r *http.Request) {
2930
var formId string
3031
if err := r.ParseMultipartForm(a.Config.Forms.MaxLength); err != nil {
3132
l.Errorf("Failed to parse form parameters: %s", err)
32-
response.ErrorJson(w, 500, "Internal Server Error")
33+
response.ErrorJson(w, 500, err.Error())
3334
return
3435
}
3536
formId = r.Form.Get("formid")
3637
if formId == "" {
37-
response.ErrorJson(w, 400, "Bad Request")
38+
response.ErrorJson(w, 400, "Missing formid")
3839
return
3940
}
4041

4142
// Let's try to read formobj from cache
4243
formObj, err := a.GetForm(formId)
4344
if err != nil {
4445
l.Errorf("Failed get formObj: %s", err)
45-
response.ErrorJson(w, 500, "Internal Server Error")
46+
response.ErrorJson(w, 500, err.Error())
4647
return
4748
}
4849

@@ -51,7 +52,7 @@ func (a *ApiRequest) GetToken(w http.ResponseWriter, r *http.Request) {
5152
reqOrigin := r.Header.Get("origin")
5253
if reqOrigin == "" {
5354
l.Errorf("No origin domain set in HTTP request")
54-
response.ErrorJson(w, 401, "Unauthorized")
55+
response.ErrorJson(w, 401, "Domain is not allowed to access the requested form")
5556
return
5657
}
5758
for _, d := range formObj.Domains {
@@ -61,32 +62,30 @@ func (a *ApiRequest) GetToken(w http.ResponseWriter, r *http.Request) {
6162
}
6263
}
6364
if !isValid {
64-
l.Errorf("Domain %q not in allowed domains list for form %d", reqOrigin, formObj.Id)
65-
response.ErrorJson(w, 401, "Unauthorized")
65+
l.Errorf("Domain %q not in allowed domains list for form %s", reqOrigin, formObj.Id)
66+
response.ErrorJson(w, 401, "Domain is not allowed to access the requested form")
6667
return
6768
}
6869
w.Header().Set("Access-Control-Allow-Origin", reqOrigin)
6970

7071
// Generate the token
7172
nowTime := time.Now()
7273
expTime := time.Now().Add(time.Minute * 10)
73-
tokenText := fmt.Sprintf("%s_%d_%d_%d_%s", reqOrigin, nowTime.Unix(), expTime.Unix(), formObj.Id, formObj.Secret)
74+
tokenText := fmt.Sprintf("%s_%d_%d_%s_%s", reqOrigin, nowTime.Unix(), expTime.Unix(), formObj.Id, formObj.Secret)
7475
tokenSha := fmt.Sprintf("%x", sha256.Sum256([]byte(tokenText)))
7576
respToken := TokenResponseJson{
7677
Token: tokenSha,
78+
FormId: formObj.Id,
7779
CreateTime: nowTime.Unix(),
7880
ExpireTime: expTime.Unix(),
79-
FormId: formObj.Id,
80-
Url: fmt.Sprintf("%s://%s/api/v1/send", a.Scheme, r.Host),
81+
Url: fmt.Sprintf("%s://%s/api/v1/send/%s/%s", a.Scheme, r.Host, formObj.Id, tokenSha),
82+
EncType: "multipart/form-data",
83+
Method: "post",
8184
}
8285
if err := a.Cache.Set(tokenSha, respToken); err != nil {
8386
l.Errorf("Failed to store response token in cache: %s", err)
8487
response.ErrorJson(w, 500, "Internal Server Error")
8588
return
8689
}
87-
if err := json.NewEncoder(w).Encode(respToken); err != nil {
88-
l.Errorf("Failed to encode response token JSON: %s", err)
89-
response.ErrorJson(w, 500, "Internal Server Error")
90-
return
91-
}
90+
response.SuccessJson(w, 200, respToken)
9291
}

0 commit comments

Comments
 (0)