diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eba4387..475814e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: --health-timeout 5s --health-retries 5 smtp: - image: mailhog/mailhog + image: marlonb/mailcrab:latest ports: - 1025:1025 steps: diff --git a/.gitignore b/.gitignore index 81e61d1..53ee585 100644 --- a/.gitignore +++ b/.gitignore @@ -410,4 +410,5 @@ cython_debug/ ./backend/*.sh -!./backend/blast-email.sh \ No newline at end of file +!./backend/blast-email.sh +./backend/configuration.yml \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..012449a --- /dev/null +++ b/backend/README.md @@ -0,0 +1,77 @@ +# Backend + +## Developing + +To develop the backend, just create integration test and make sure it's running on CI. + +But, if you want to run it locally, copy the `configuration.example.yml` into `configuration.yml`, +then start the Docker Compose file on the root directory using `docker compose up -d postgres mailcrab`. + +Your `configuration.yml` file should be similar to: + +```yaml +feature_flags: + registration_closed: false + +environment: local + +database: + host: localhost + port: 5432 + user: conference + password: VeryStrongPassword + database: conference + +port: 8080 + +mailer: + hostname: localhost + port: 1025 + from: administrator@localhost + password: + +blob_url: file:///tmp/teknologi-umum-conference + +signature: + public_key: 2bb6b9b9e1d9e12bfdd4196bfba6a081156ac... + private_key: 48d0ca64011fec1cb23b21820e9f7e880843e71f236b7f8decfe3568f... + +validate_payment_key: 24326124313024514d56324d782f4a7342446f36363653784b324175657341... +``` + +Generate the `signature.public_key` and `signature.private_key` using this simple Go script: + +```go +package main + +import ( + "crypto/ed25519" + "encoding/hex" + "fmt" +) + +func main() { + pub, priv, _ := ed25519.GenerateKey(nil) + fmt.Println(hex.EncodeToString(pub)) + fmt.Println(hex.EncodeToString(priv)) +} +``` + +Generate the `validate_payment_key` using this simple Go script: + +```go +package main + +import ( + "fmt" + + "golang.org/x/crypto/bcrypt" +) + +func main() { + passphrase := "test" + value, _ := bcrypt.GenerateFromPassword([]byte(passphrase), 10) + fmt.Printf("%x", value) + +} +``` \ No newline at end of file diff --git a/backend/mailer.go b/backend/mailer.go index b83182b..51b63e0 100644 --- a/backend/mailer.go +++ b/backend/mailer.go @@ -88,7 +88,7 @@ func (m *Mailer) messageBuilder(ctx context.Context, mail *Mail) []byte { msg.WriteString("Content-Transfer-Encoding: 8bit\n") msg.WriteString("\n") msg.WriteString(mail.PlainTextBody) - msg.WriteString("\n") + msg.WriteString("\n\n") msg.WriteString("--" + alternateBoundary + "\n") msg.WriteString("Content-Type: text/html; charset=\"utf-8\"\n") msg.WriteString("Content-Transfer-Encoding: 8bit\n") diff --git a/backend/server.go b/backend/server.go index 03559eb..7427a77 100644 --- a/backend/server.go +++ b/backend/server.go @@ -1,6 +1,7 @@ package main import ( + "encoding/hex" "errors" "mime" "net/http" @@ -248,7 +249,17 @@ func (s *ServerDependency) DayTicketScan(c echo.Context) error { } // Validate key - if err := bcrypt.CompareHashAndPassword([]byte(s.validateTicketKey), []byte(requestBody.Key)); err != nil { + decodedPassphrase, err := hex.DecodeString(s.validateTicketKey) + if err != nil { + sentryHub.CaptureException(err) + return c.JSON(http.StatusInternalServerError, echo.Map{ + "message": "Internal server error", + "errors": "Internal server error", + "request_id": requestId, + }) + } + + if err := bcrypt.CompareHashAndPassword(decodedPassphrase, []byte(requestBody.Key)); err != nil { if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { return c.JSON(http.StatusForbidden, echo.Map{ "message": "Wrong passphrase", diff --git a/backend/ticketing.go b/backend/ticketing.go index b2821c9..8657830 100644 --- a/backend/ticketing.go +++ b/backend/ticketing.go @@ -350,7 +350,7 @@ func (t *TicketDomain) VerifyTicket(ctx context.Context, payload []byte) (email return "", "", false, ErrInvalidTicket } - ticketId, err := uuid.FromBytes(rawTicketId) + ticketId, err := uuid.ParseBytes(rawTicketId) if err != nil { return "", "", false, ErrInvalidTicket } @@ -380,20 +380,20 @@ func (t *TicketDomain) VerifyTicket(ctx context.Context, payload []byte) (email tx, err := conn.BeginTx(ctx, pgx.TxOptions{ IsoLevel: pgx.RepeatableRead, - AccessMode: pgx.ReadOnly, + AccessMode: pgx.ReadWrite, }) if err != nil { return "", "", false, fmt.Errorf("creating transaction: %w", err) } - err = tx.QueryRow(ctx, "SELECT email, student FROM ticketing WHERE id = $1", ticketId).Scan(&email, &student) + err = tx.QueryRow(ctx, "SELECT email, student FROM ticketing WHERE id = $1 AND used = FALSE", ticketId).Scan(&email, &student) if err != nil { if e := tx.Rollback(ctx); e != nil { return "", "", false, fmt.Errorf("rolling back transaction: %w (%s)", e, err.Error()) } if errors.Is(err, pgx.ErrNoRows) { - return "", "", false, ErrInvalidTicket + return "", "", false, fmt.Errorf("%w (id not exists, or ticket has been used)", ErrInvalidTicket) } return "", "", false, fmt.Errorf("acquiring data from table: %w", err) diff --git a/docker-compose.yml b/docker-compose.yml index 4a87925..75019c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,11 +39,11 @@ services: options: max-size: 10M - mailhog: - image: mailhog/mailhog:latest + mailcrab: + image: marlonb/mailcrab:latest ports: - 127.0.0.1:1025:1025 - - 127.0.0.1:8025:8025 + - 127.0.0.1:8025:1080 deploy: mode: replicated replicas: 1 @@ -94,7 +94,7 @@ services: condition: service_completed_successfully postgres: condition: service_healthy - mailhog: + mailcrab: condition: service_started logging: driver: local diff --git a/frontend/pages/verify.vue b/frontend/pages/verify.vue index 6ab828d..5b1c1de 100644 --- a/frontend/pages/verify.vue +++ b/frontend/pages/verify.vue @@ -10,9 +10,15 @@ const key = ref([]) const config = useRuntimeConfig(); const alertClass = ref(null); const paused = ref(false) +const invalidTicketReason = ref(""); +interface ScanResponse { + message: string + student: boolean + name: string + email: string +} const onDetect = async (a: any) => { - - const response = await useFetch(`${config.public.backendBaseUrl}/scan-tiket`, { + const response = await useFetch(`${config.public.backendBaseUrl}/scan-tiket`, { method: "POST", body: { code: a[0].rawValue, @@ -23,12 +29,14 @@ const onDetect = async (a: any) => { if (response.error.value?.statusCode && [406, 403].includes(response.error.value?.statusCode)) { alertClass.value = false + invalidTicketReason.value = response.error.value?.data.errors; } else { + const body = response.data.value; alertClass.value = true detectedUser.value = { - name: 'Aji', - student: true, - institution: "PT Mencari Cinta Sejati" + name: body?.name, + student: body?.student, + email: body?.email, } } setTimeout(() => { @@ -45,7 +53,7 @@ const scanNext = () => {
- {{ alertClass ? 'User verified!' : 'Invalid ticket' }} + {{ alertClass ? 'User verified!' : invalidTicketReason }}