Skip to content

Commit

Permalink
Merge pull request #5 from rarimo/feat/anti-spam
Browse files Browse the repository at this point in the history
Feat/anti spam
  • Loading branch information
Zaptoss authored Jul 24, 2024
2 parents 93c3fbb + b636427 commit fa7df43
Show file tree
Hide file tree
Showing 14 changed files with 171 additions and 72 deletions.
1 change: 1 addition & 0 deletions .github/workflows/tag.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+'

env:
CI_JOB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
3 changes: 2 additions & 1 deletion docs/spec/components/schemas/FormStatus.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ allOf:
properties:
status:
type: string
enum: [ accepted, processed ]
enum: [ created, accepted, processed ]
description: |
Created - the empty form was created and now user can't use legacy submit
Accepted - the data was saved by the service for further processing
Processed - the data is processed and stored
created_at:
Expand Down
16 changes: 8 additions & 8 deletions docs/spec/paths/integrations@forms-svc@[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ post:
- User form
summary: Submit lightweight user answers
description: |
Send user answers and return their current status.
Sending the filled form. Requires created empty form
linked to the name of the file with the photo.
Return form current status.
The image is a link to s3 storage
Only a user with a confirmed passport can send the form ('verified: true' in JWT).
There is a configurable delay before the same user
can submit another form.
The image is a link to s3 storage
operationId: lightweightSubmitForm
security:
- BearerAuth: []
Expand All @@ -34,11 +32,13 @@ post:
$ref: '#/components/responses/invalidParameter'
401:
$ref: '#/components/responses/invalidAuth'
429:
description: "It is necessary to wait some time before sending the next form"
403:
description: "Empty form absent for user, but processed or accepted one exists"
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/Errors'
404:
$ref: '#/components/responses/notFound'
500:
$ref: '#/components/responses/internalError'
1 change: 1 addition & 0 deletions docs/spec/paths/integrations@forms-svc@[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ post:
in S3 Storage. The name is generated on the server side.
'verified: true' must be specified in the JWT.
The cooldown of this endpoint is the same as in the submit form.
This endpoint creates an empty form that can be submitted on `/form`
operationId: uploadImage
security:
- BearerAuth: []
Expand Down
24 changes: 22 additions & 2 deletions internal/data/forms.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,29 @@ import (
)

const (
CreatedStatus = "created"
AcceptedStatus = "accepted"
ProcessedStatus = "processed"
)

const (
ColStatus = "status"
ColName = "name"
ColSurname = "surname"
ColIDNum = "id_num"
ColBirthday = "birthday"
ColCitizen = "citizen"
ColVisited = "visited"
ColPurpose = "purpose"
ColCountry = "country"
ColCity = "city"
ColAddress = "address"
ColPostal = "postal"
ColPhone = "phone"
ColEmail = "email"
ColImageURL = "image_url"
)

type Form struct {
ID string `db:"id"`
Nullifier string `db:"nullifier"`
Expand Down Expand Up @@ -45,7 +64,8 @@ type FormStatus struct {
type FormsQ interface {
New() FormsQ
Insert(*Form) (*FormStatus, error)
Update(status string) error

Update(map[string]interface{}) error

Select() ([]*Form, error)
Limit(uint64) FormsQ
Expand All @@ -54,5 +74,5 @@ type FormsQ interface {
Last(nullifier string) (*FormStatus, error)

FilterByID(ids ...string) FormsQ
FilterByStatus(status string) FormsQ
FilterByStatus(status ...string) FormsQ
}
21 changes: 15 additions & 6 deletions internal/data/pg/forms.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ const (
type formsQ struct {
db *pgdb.DB
selector squirrel.SelectBuilder
last squirrel.SelectBuilder
updater squirrel.UpdateBuilder
}

func NewForms(db *pgdb.DB) data.FormsQ {
return &formsQ{
db: db,
selector: squirrel.Select("*").From(formsTable),
last: squirrel.Select(formsStatusFields).From(formsTable),
updater: squirrel.Update(formsTable),
}
}
Expand All @@ -36,7 +38,7 @@ func (q *formsQ) New() data.FormsQ {
func (q *formsQ) Insert(form *data.Form) (*data.FormStatus, error) {
var res data.FormStatus

stmt := squirrel.Insert(formsTable).SetMap(map[string]interface{}{
values := map[string]interface{}{
"nullifier": form.Nullifier,
"status": form.Status,
"name": form.Name,
Expand All @@ -54,7 +56,13 @@ func (q *formsQ) Insert(form *data.Form) (*data.FormStatus, error) {
"email": form.Email,
"image": form.Image,
"image_url": form.ImageURL,
}).Suffix("RETURNING id, nullifier, status, created_at, updated_at")
}

if form.ID != "" {
values["id"] = form.ID
}

stmt := squirrel.Insert(formsTable).SetMap(values).Suffix("RETURNING id, nullifier, status, created_at, updated_at")

if err := q.db.Get(&res, stmt); err != nil {
return nil, fmt.Errorf("insert form: %w", err)
Expand All @@ -63,8 +71,8 @@ func (q *formsQ) Insert(form *data.Form) (*data.FormStatus, error) {
return &res, nil
}

func (q *formsQ) Update(status string) error {
if err := q.db.Exec(q.updater.Set("status", status)); err != nil {
func (q *formsQ) Update(fields map[string]any) error {
if err := q.db.Exec(q.updater.SetMap(fields)); err != nil {
return fmt.Errorf("update forms: %w", err)
}

Expand Down Expand Up @@ -98,7 +106,7 @@ func (q *formsQ) Get(id string) (*data.FormStatus, error) {
func (q *formsQ) Last(nullifier string) (*data.FormStatus, error) {
var res data.FormStatus

stmt := squirrel.Select(formsStatusFields).From(formsTable).Where(squirrel.Eq{"nullifier": nullifier}).OrderBy("created_at DESC")
stmt := q.last.Where(squirrel.Eq{"nullifier": nullifier}).OrderBy("created_at DESC")
if err := q.db.Get(&res, stmt); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
Expand All @@ -122,12 +130,13 @@ func (q *formsQ) FilterByNullifier(nullifier string) data.FormsQ {
return q.applyCondition(squirrel.Eq{"nullifier": nullifier})
}

func (q *formsQ) FilterByStatus(status string) data.FormsQ {
func (q *formsQ) FilterByStatus(status ...string) data.FormsQ {
return q.applyCondition(squirrel.Eq{"status": status})
}

func (q *formsQ) applyCondition(cond squirrel.Sqlizer) data.FormsQ {
q.selector = q.selector.Where(cond)
q.last = q.selector.Where(cond)
q.updater = q.updater.Where(cond)
return q
}
2 changes: 1 addition & 1 deletion internal/service/handlers/last_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func LastStatus(w http.ResponseWriter, r *http.Request) {

func newFormStatusResponse(formStatus *data.FormStatus) resources.FormStatusResponse {
untilNextForm := time.Now().UTC().Unix() - formStatus.NextFormAt.Unix()
if untilNextForm < 0 {
if untilNextForm < 0 || formStatus.Status == data.CreatedStatus {
untilNextForm = 0
}

Expand Down
7 changes: 6 additions & 1 deletion internal/service/handlers/status_by_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strings"

"github.com/rarimo/geo-auth-svc/pkg/auth"
"github.com/rarimo/geo-forms-svc/internal/data"
"github.com/rarimo/geo-forms-svc/internal/service/requests"
"gitlab.com/distributed_lab/ape"
"gitlab.com/distributed_lab/ape/problems"
Expand Down Expand Up @@ -54,7 +55,11 @@ func StatusByID(w http.ResponseWriter, r *http.Request) {
ape.RenderErr(w, problems.Unauthorized())
return
}
formStatus.NextFormAt = lastStatus.CreatedAt.Add(Forms(r).Cooldown)

formStatus.NextFormAt = lastStatus.CreatedAt
if lastStatus.Status != data.CreatedStatus {
formStatus.NextFormAt = lastStatus.CreatedAt.Add(Forms(r).Cooldown)
}

ape.Render(w, newFormStatusResponse(formStatus))
}
66 changes: 35 additions & 31 deletions internal/service/handlers/submit_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"net/http"
"net/url"
"strings"
"time"

validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/rarimo/geo-auth-svc/pkg/auth"
Expand Down Expand Up @@ -36,13 +35,15 @@ func SubmitForm(w http.ResponseWriter, r *http.Request) {
return
}

if lastForm != nil {
next := lastForm.CreatedAt.Add(Forms(r).Cooldown)
if next.After(time.Now().UTC()) {
Log(r).Debugf("Form submitted time: %s; next available time: %s", lastForm.CreatedAt, next)
ape.RenderErr(w, problems.TooManyRequests())
return
}
if lastForm == nil {
ape.RenderErr(w, problems.NotFound())
return
}

if lastForm.Status != data.CreatedStatus {
Log(r).Debugf("User last form must have %s status, got %s", data.CreatedStatus, lastForm.Status)
ape.RenderErr(w, problems.Forbidden())
return
}

imageURL, err := url.Parse(req.Data.Attributes.Image)
Expand All @@ -52,7 +53,7 @@ func SubmitForm(w http.ResponseWriter, r *http.Request) {
return
}

if err = Storage(r).ValidateImage(imageURL); err != nil {
if err = Storage(r).ValidateImage(imageURL, lastForm.ID); err != nil {
if storage.IsBadRequestError(err) {
ape.RenderErr(w, problems.BadRequest(validation.Errors{
"image": err,
Expand All @@ -66,34 +67,37 @@ func SubmitForm(w http.ResponseWriter, r *http.Request) {
}

userData := req.Data.Attributes
form := &data.Form{
Nullifier: nullifier,
Status: data.AcceptedStatus,
Name: userData.Name,
Surname: userData.Surname,
IDNum: userData.IdNum,
Birthday: userData.Birthday,
Citizen: userData.Citizen,
Visited: userData.Visited,
Purpose: userData.Purpose,
Country: userData.Country,
City: userData.City,
Address: userData.Address,
Postal: userData.Postal,
Phone: userData.Phone,
Email: userData.Email,
Image: nil,
ImageURL: sql.NullString{String: userData.Image, Valid: true},
err = FormsQ(r).FilterByID(lastForm.ID).Update(map[string]interface{}{
data.ColStatus: data.AcceptedStatus,
data.ColName: userData.Name,
data.ColSurname: userData.Surname,
data.ColIDNum: userData.IdNum,
data.ColBirthday: userData.Birthday,
data.ColCitizen: userData.Citizen,
data.ColVisited: userData.Visited,
data.ColPurpose: userData.Purpose,
data.ColCountry: userData.Country,
data.ColCity: userData.City,
data.ColAddress: userData.Address,
data.ColPostal: userData.Postal,
data.ColPhone: userData.Phone,
data.ColEmail: userData.Email,
data.ColImageURL: sql.NullString{String: userData.Image, Valid: true},
})
if err != nil {
Log(r).WithError(err).Error("failed to insert form")
ape.RenderErr(w, problems.InternalError())
return
}

formStatus, err := FormsQ(r).Insert(form)
lastForm, err = FormsQ(r).Last(nullifier)
if err != nil {
Log(r).WithError(err).Error("failed to insert form")
Log(r).WithError(err).Errorf("Failed to get last user form for nullifier [%s]", nullifier)
ape.RenderErr(w, problems.InternalError())
return
}

formStatus.NextFormAt = formStatus.CreatedAt.Add(Forms(r).Cooldown)
lastForm.NextFormAt = lastForm.CreatedAt.Add(Forms(r).Cooldown)

ape.Render(w, newFormStatusResponse(formStatus))
ape.Render(w, newFormStatusResponse(lastForm))
}
Loading

0 comments on commit fa7df43

Please sign in to comment.