Skip to content

Commit

Permalink
Merge pull request #66 from teknologi-umum/ref/backend/qrscan-endpoint
Browse files Browse the repository at this point in the history
ref(backend): verify ticket endpoint
  • Loading branch information
WahidinAji authored Oct 2, 2023
2 parents f3bf5e5 + e4cd6a7 commit b712ba0
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 21 deletions.
68 changes: 66 additions & 2 deletions backend/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"path"
"slices"

"github.com/getsentry/sentry-go"
sentryecho "github.com/getsentry/sentry-go/echo"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
Expand Down Expand Up @@ -68,6 +69,7 @@ func NewServer(config *ServerConfig) *echo.Echo {

e.POST("/users", dependencies.RegisterUser)
e.POST("/bukti-transfer", dependencies.UploadBuktiTransfer)
e.POST("/scan-tiket", dependencies.DayTicketScan)
return e
}

Expand All @@ -81,6 +83,9 @@ func (s *ServerDependency) RegisterUser(c echo.Context) error {
sentryHub := sentryecho.GetHubFromContext(c)
sentryHub.Scope().SetTag("request-id", requestId)

span := sentry.StartSpan(c.Request().Context(), "http.server", sentry.WithTransactionName("POST /users"), sentry.WithTransactionSource(sentry.SourceRoute))
defer span.Finish()

if s.registrationClosed {
return c.JSON(http.StatusNotAcceptable, echo.Map{
"message": "Registration is closed",
Expand All @@ -98,7 +103,7 @@ func (s *ServerDependency) RegisterUser(c echo.Context) error {
}

err := s.userDomain.CreateParticipant(
c.Request().Context(),
span.Context(),
CreateParticipantRequest{
Name: p.Name,
Email: p.Email,
Expand Down Expand Up @@ -130,6 +135,9 @@ func (s *ServerDependency) UploadBuktiTransfer(c echo.Context) error {
sentryHub := sentryecho.GetHubFromContext(c)
sentryHub.Scope().SetTag("request-id", requestId)

span := sentry.StartSpan(c.Request().Context(), "http.server", sentry.WithTransactionName("POST /bukti-transfer"), sentry.WithTransactionSource(sentry.SourceRoute))
defer span.Finish()

if err := c.Request().ParseMultipartForm(32 << 10); err != nil {
return c.JSON(http.StatusBadRequest, echo.Map{
"message": "Parsing error",
Expand Down Expand Up @@ -183,7 +191,7 @@ func (s *ServerDependency) UploadBuktiTransfer(c echo.Context) error {

photoContentType := mime.TypeByExtension(photoExtension)

err = s.ticketDomain.StorePaymentReceipt(c.Request().Context(), email, photoFile, photoContentType)
err = s.ticketDomain.StorePaymentReceipt(span.Context(), email, photoFile, photoContentType)
if err != nil {
var validationError *ValidationError
if errors.As(err, &validationError) {
Expand All @@ -204,3 +212,59 @@ func (s *ServerDependency) UploadBuktiTransfer(c echo.Context) error {

return c.NoContent(http.StatusCreated)
}

type DayTicketScanRequest struct {
Code string `json:"code"`
}

func (s *ServerDependency) DayTicketScan(c echo.Context) error {
requestId := c.Response().Header().Get(echo.HeaderXRequestID)
sentryHub := sentryecho.GetHubFromContext(c)
sentryHub.Scope().SetTag("request-id", requestId)

span := sentry.StartSpan(c.Request().Context(), "http.server", sentry.WithTransactionName("POST /scan-tiket"), sentry.WithTransactionSource(sentry.SourceRoute))
defer span.Finish()

var requestBody DayTicketScanRequest
if err := c.Bind(&requestBody); err != nil {
return c.JSON(http.StatusBadRequest, echo.Map{
"message": "Invalid request body",
"errors": err.Error(),
"request_id": requestId,
})
}

email, name, student, err := s.ticketDomain.VerifyTicket(span.Context(), []byte(requestBody.Code))
if err != nil {
var validationError *ValidationError
if errors.As(err, &validationError) {
return c.JSON(http.StatusBadRequest, echo.Map{
"message": "Validation error",
"errors": validationError.Error(),
"request_id": requestId,
})
}

if errors.Is(err, ErrInvalidTicket) {
return c.JSON(http.StatusNotAcceptable, echo.Map{
"message": "Invalid ticket",
"errors": err.Error(),
"request_id": requestId,
})
}

sentryHub.CaptureException(err)
return c.JSON(http.StatusInternalServerError, echo.Map{
"message": "Internal server error",
"errors": "Internal server error",
"request_id": requestId,
})
}

return c.JSON(http.StatusOK, echo.Map{
"message": "Ticket confirmed",
"student": student,
"name": name,
"email": email,
})
}
50 changes: 31 additions & 19 deletions backend/ticketing.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,50 +312,50 @@ harap abaikan email ini. Terima kasih!`,
// the signature and mark the ticket as used. Each ticket can only be used once.
//
// If the signature is invalid or the ticket is used, it will return ErrInvalidTicket error.
func (t *TicketDomain) VerifyTicket(ctx context.Context, payload []byte) (ok bool, student bool, err error) {
func (t *TicketDomain) VerifyTicket(ctx context.Context, payload []byte) (email string, name string, student bool, err error) {
span := sentry.StartSpan(ctx, "ticket.verify_ticket")
defer span.Finish()

if len(payload) == 0 {
return false, false, ValidationError{Errors: []string{"payload is empty"}}
return "", "", false, ValidationError{Errors: []string{"payload is empty"}}
}

// Separate the payload into the signature + email + random id that's generated from ValidatePaymentReceipt
rawSignature, payloadAfter, found := bytes.Cut(payload, []byte(";"))
if !found {
return false, false, ErrInvalidTicket
return "", "", false, ErrInvalidTicket
}

rawTicketId, rawHashedEmail, found := bytes.Cut(payloadAfter, []byte(":"))
if !found {
return false, false, ErrInvalidTicket
return "", "", false, ErrInvalidTicket
}

ticketId, err := uuid.FromBytes(rawTicketId)
if err != nil {
return false, false, ErrInvalidTicket
return "", "", false, ErrInvalidTicket
}

userHashedEmail, err := base64.StdEncoding.DecodeString(string(rawHashedEmail))
if err != nil {
return false, false, fmt.Errorf("decoding base64 string for email: %w", err)
return "", "", false, fmt.Errorf("decoding base64 string for email: %w", err)
}

signature, err := hex.DecodeString(string(rawSignature))
if err != nil {
return false, false, fmt.Errorf("decoding hex string for signature: %w", err)
return "", "", false, fmt.Errorf("decoding hex string for signature: %w", err)
}

// Validate the signature and its message using ed25519. If it's invalid, return ErrInvalidTicket
signatureValidated := ed25519.Verify(*t.publicKey, payloadAfter, signature)
if !signatureValidated {
return false, false, fmt.Errorf("%w (verifying signature)", ErrInvalidTicket)
return "", "", false, fmt.Errorf("%w (verifying signature)", ErrInvalidTicket)
}

// Check the ticket if it's been used before. If it is, return ErrInvalidTicket. Decorate it a bit.
conn, err := t.db.Acquire(ctx)
if err != nil {
return false, false, fmt.Errorf("acquiring connection from pool: %w", err)
return "", "", false, fmt.Errorf("acquiring connection from pool: %w", err)
}
defer conn.Release()

Expand All @@ -364,46 +364,58 @@ func (t *TicketDomain) VerifyTicket(ctx context.Context, payload []byte) (ok boo
AccessMode: pgx.ReadOnly,
})
if err != nil {
return false, false, fmt.Errorf("creating transaction: %w", err)
return "", "", false, fmt.Errorf("creating transaction: %w", err)
}

var email string
err = tx.QueryRow(ctx, "SELECT email, student FROM ticketing WHERE id = $1", ticketId).Scan(&email, &student)
if err != nil {
if e := tx.Rollback(ctx); e != nil {
return false, false, fmt.Errorf("rolling back transaction: %w (%s)", e, err.Error())
return "", "", false, fmt.Errorf("rolling back transaction: %w (%s)", e, err.Error())
}

if errors.Is(err, pgx.ErrNoRows) {
return false, false, ErrInvalidTicket
return "", "", false, ErrInvalidTicket
}

return false, false, fmt.Errorf("acquiring data from table: %w", err)
return "", "", false, fmt.Errorf("acquiring data from table: %w", err)
}

err = tx.QueryRow(ctx, "SELECT name FROM users WHERE email = $1", email).Scan(&name)
if err != nil {
if e := tx.Rollback(ctx); e != nil {
return "", "", false, fmt.Errorf("rolling back transaction: %w (%s)", e, err.Error())
}

// Do not return error if something's wrong with name.
// Instead, just report to Sentry.
if !errors.Is(err, pgx.ErrNoRows) {
sentry.GetHubFromContext(ctx).CaptureException(err)
}
}

// Validate email
sha384Hasher := sha512.New384()
sha384Hasher.Write([]byte(email))
hashedEmail := sha384Hasher.Sum(nil)
if !bytes.Equal(hashedEmail, userHashedEmail) {
return false, false, fmt.Errorf("%w (mismatched email)", ErrInvalidTicket)
return "", "", false, fmt.Errorf("%w (mismatched email)", ErrInvalidTicket)
}

// Mark the ticket as used
_, err = tx.Exec(ctx, "UPDATE ticketing SET used = TRUE WHERE id = $1", ticketId)
if err != nil {
if e := tx.Rollback(ctx); e != nil {
return false, false, fmt.Errorf("rolling back transaction: %w (%s)", e, err.Error())
return "", "", false, fmt.Errorf("rolling back transaction: %w (%s)", e, err.Error())
}

return false, false, fmt.Errorf("acquiring data from table: %w", err)
return "", "", false, fmt.Errorf("acquiring data from table: %w", err)
}

if err := tx.Commit(ctx); err != nil {
return false, false, fmt.Errorf("commiting transaction: %w", err)
return "", "", false, fmt.Errorf("commiting transaction: %w", err)
}

return true, student, nil
return email, name, student, nil
}

func (t *TicketDomain) VerifyIsStudent(ctx context.Context, email string) (err error) {
Expand Down

0 comments on commit b712ba0

Please sign in to comment.