Skip to content

Commit

Permalink
feat: traceable transaction, wishlist get like info (#63)
Browse files Browse the repository at this point in the history
Co-authored-by: Yuwang Cai <[email protected]>
  • Loading branch information
MrPhotato and mrcaidev authored Oct 26, 2024
1 parent 745500c commit ef09900
Show file tree
Hide file tree
Showing 28 changed files with 698 additions and 150 deletions.
2 changes: 1 addition & 1 deletion docker-compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ services:
env_file:
- envs/wishlist-mongo.env
volumes:
- ./services/wishlist/database:/docker-entrypoint-initdb.d
- ./services/wishlist/database/dev:/docker-entrypoint-initdb.d
- wishlist-mongo-data:/data/db
networks:
- bridge
Expand Down
12 changes: 6 additions & 6 deletions services/account/database/2-seed.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ insert into department (acronym, name) values
('BIZ', 'Business School'),
('ECE', 'Electrical and Computer Engineering');

insert into account (email, password_hash, password_salt, nickname, avatar_url) values
('[email protected]', '$2a$10$/UwR7JKUFzyPfLLs9.DlyOXtWMLgPEob0jfF0I6sEK85t7z0vNLlK', '/+uNyqPxTo0OAecGgo3Oog==', 'Johnny', 'https://avatars.githubusercontent.com/u/78269445?v=4'),
('[email protected]', '$2a$10$j4WHKfF8BRc3Vd5riEZ/8OrnU6dXIIx3vT/qf9y38lRDWy764lYQG', 'Vq3iUtK70eav3Rd0VjYFog==', 'JaneS', 'https://avatars.githubusercontent.com/u/69978374?v=4'),
('[email protected]', '$2a$10$.QM6HzIPoJOllcbOdDhMxOSM6eGPs9ACV08WA.eBMEjuVAnObsqUi', 'ypJaYX91LwNjtmjjf4H8YQ==', 'AlexL', 'https://avatars.githubusercontent.com/u/13389461?v=4'),
('[email protected]', '$2a$10$04CYtCiQ/.tP4wTz5rmH.ev9t2wYxfjOHx64Cp2YiaDidiIPj9MXa', 'taHXrSQ0f9HJeT/N5OgeuQ==', 'MikeB', 'https://avatars.githubusercontent.com/u/60336739?v=4'),
('[email protected]', '$2a$10$OeA4WtLAccv12uylxYnqneVcAGsj0IgSerUGPXX41vRGKRlGLpf3K', 'pix8BBby+8fpK7NnUT07iA==', 'EmJ', 'https://avatars.githubusercontent.com/u/83934144?v=4');
insert into account (email, password_hash, password_salt, nickname, avatar_url) values
('[email protected]', '$2a$10$/UwR7JKUFzyPfLLs9.DlyOXtWMLgPEob0jfF0I6sEK85t7z0vNLlK', '/+uNyqPxTo0OAecGgo3Oog==', 'Johnny', 'https://avatars.githubusercontent.com/u/78269445?v=4'),
('[email protected]', '$2a$10$j4WHKfF8BRc3Vd5riEZ/8OrnU6dXIIx3vT/qf9y38lRDWy764lYQG', 'Vq3iUtK70eav3Rd0VjYFog==', 'JaneS', 'https://avatars.githubusercontent.com/u/69978374?v=4'),
('[email protected]', '$2a$10$.QM6HzIPoJOllcbOdDhMxOSM6eGPs9ACV08WA.eBMEjuVAnObsqUi', 'ypJaYX91LwNjtmjjf4H8YQ==', 'AlexL', 'https://avatars.githubusercontent.com/u/13389461?v=4'),
('[email protected]', '$2a$10$04CYtCiQ/.tP4wTz5rmH.ev9t2wYxfjOHx64Cp2YiaDidiIPj9MXa', 'taHXrSQ0f9HJeT/N5OgeuQ==', 'MikeB', 'https://avatars.githubusercontent.com/u/60336739?v=4'),
('[email protected]', '$2a$10$OeA4WtLAccv12uylxYnqneVcAGsj0IgSerUGPXX41vRGKRlGLpf3K', 'pix8BBby+8fpK7NnUT07iA==', 'EmJ', 'https://avatars.githubusercontent.com/u/83934144?v=4');
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ public class JWTPayload {

String avatarUrl;

String email;

// all args constructor, input an account object, and convert to this rspAccount object
public JWTPayload(Account account) {
this.id = account.getId();
this.nickname = account.getNickname();
this.avatarUrl = account.getAvatarUrl();
this.email = account.getEmail();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static String generateAccessToken(JWTPayload jwtPayload){
.claim("id", jwtPayload.getId())
.claim("nickname", jwtPayload.getNickname())
.claim("avatar_url", jwtPayload.getAvatarUrl())
.claim("email", jwtPayload.getEmail())

.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, secretKey)
Expand Down Expand Up @@ -75,7 +76,7 @@ public static JWTPayload decodeAccessToken(String token) {
.getBody();

JWTPayload jwtPayload = new JWTPayload((int) claims.get("id"), (String)claims.get("nickname"),
(String)claims.get("avatar_url"));
(String)claims.get("avatar_url"), (String)claims.get("email"));

return jwtPayload;

Expand Down
2 changes: 2 additions & 0 deletions services/item/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ RUN apk add --no-cache curl

COPY --from=build /app/dist/index.js .

ENV NODE_ENV=production

CMD ["bun", "run", "index.js"]
2 changes: 2 additions & 0 deletions services/item/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ COPY package.json bun.lockb ./

RUN bun install --frozen-lockfile

ENV NODE_ENV=development

CMD ["bun", "run", "dev"]
2 changes: 2 additions & 0 deletions services/item/Dockerfile.test
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ RUN bun install --frozen-lockfile

COPY . .

ENV NODE_ENV=test

CMD ["sleep", "infinity"]
4 changes: 4 additions & 0 deletions services/item/database/dev/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const items = [
seller: simplifiedAccounts[2],
name: "Give back to the community",
description: "A bonus pack of my items for sale. Get a 20% discount!",
price: (120.5 + 45.0) * 0.8,
discount: 0.2,
status: 0,
children: [
Expand Down Expand Up @@ -264,6 +265,7 @@ const items = [
seller: simplifiedAccounts[2],
name: "Home Office Essentials",
description: "A pack of essential items for a home office setup.",
price: (30.0 + 40.0) * 0.85,
discount: 0.15,
status: 0,
children: [
Expand Down Expand Up @@ -361,6 +363,7 @@ const items = [
seller: simplifiedAccounts[3],
name: "Kitchen Essentials",
description: "A pack of essential kitchen appliances and tools.",
price: (60.0 + 40.0) * 0.9,
discount: 0.1,
status: 0,
children: [
Expand Down Expand Up @@ -458,6 +461,7 @@ const items = [
seller: simplifiedAccounts[4],
name: "Travel Essentials",
description: "A pack of essential items for travel.",
price: (25.0 + 150.0) * 0.9,
discount: 0.1,
status: 0,
children: [
Expand Down
4 changes: 4 additions & 0 deletions services/item/database/test/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const items = [
seller: simplifiedAccounts[2],
name: "Give back to the community",
description: "A bonus pack of my items for sale. Get a 20% discount!",
price: (120.5 + 45.0) * 0.8,
discount: 0.2,
status: 0,
children: [
Expand Down Expand Up @@ -264,6 +265,7 @@ const items = [
seller: simplifiedAccounts[2],
name: "Home Office Essentials",
description: "A pack of essential items for a home office setup.",
price: (30.0 + 40.0) * 0.85,
discount: 0.15,
status: 0,
children: [
Expand Down Expand Up @@ -361,6 +363,7 @@ const items = [
seller: simplifiedAccounts[3],
name: "Kitchen Essentials",
description: "A pack of essential kitchen appliances and tools.",
price: (60.0 + 40.0) * 0.9,
discount: 0.1,
status: 0,
children: [
Expand Down Expand Up @@ -458,6 +461,7 @@ const items = [
seller: simplifiedAccounts[4],
name: "Travel Essentials",
description: "A pack of essential items for travel.",
price: (25.0 + 150.0) * 0.9,
discount: 0.1,
status: 0,
children: [
Expand Down
46 changes: 25 additions & 21 deletions services/notification/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,13 @@ Each notification is guaranteed to be processed **exactly once**.

## Exchange

| Property | Value |
|-------------|--------------|
| name | notification |
| type | topic |
| durable | true |
| auto delete | false |
| internal | false |
| no wait | false |
| args | (none) |
- Name: notification
- Type: topic
- Durable: true

## Topic
## Topic: email

The topic name indicates the eventual notification channel.

| Topic name | Notification channel |
|------------|----------------------|
| email | Email |

## Message

It is recommended to mark all messages as `persistent`.

For email topic:
Send an email to one user.

```json
{
Expand All @@ -45,3 +29,23 @@ For email topic:
"content": "<p>Email content in HTML or plain text</p>"
}
```

## Topic: batch-email

Send multiple emails to multiple users at once, in batch.

```json
{
"emails": [
{
"to": "[email protected]",
"title": "Email subject 1",
"content": "<p>Email content in HTML or plain text</p>"
},
{
"to": "[email protected]",
"title": "Email subject 2",
"content": "<p>Email content in HTML or plain text</p>"
}
]
}
5 changes: 5 additions & 0 deletions services/notification/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ func main() {
log.Panic(err)
}

err = startQueue[processors.BatchEmailPayload](channel, "batch-email")
if err != nil {
log.Panic(err)
}

startHealthCheck()

log.Print("service started")
Expand Down
55 changes: 55 additions & 0 deletions services/notification/processors/batch_email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package processors

import (
"context"
"log"
"os"
"time"

"github.com/resend/resend-go/v2"
"nshm.shop/notification/utils"
)

type BatchEmailPayload struct {
Emails []EmailPayload `json:"emails" validate:"required,gt=0,dive,required"`
}

func (payload BatchEmailPayload) Process() error {
err := utils.Validate(payload)
if err != nil {
return err
}

emails := payload.Emails

if os.Getenv("GO_ENV") != "production" {
for _, email := range emails {
log.Printf("sent emails:\n[to]\n%s\n[title]\n%s\n[content]\n%s\n", email.To, email.Title, email.Content)
}

return nil
}

client := resend.NewClient(os.Getenv("RESEND_API_KEY"))

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

sendEmailRequests := make([]*resend.SendEmailRequest, 0)
for _, email := range emails {
sendEmailRequests = append(sendEmailRequests, &resend.SendEmailRequest{
From: "NUS Second-Hand Market <[email protected]>",
To: []string{email.To},
Subject: email.Title,
Html: email.Content,
})
}

sent, err := client.Batch.SendWithContext(ctx, sendEmailRequests)
if err != nil {
return err
}

log.Printf("sent emails: %s\n", sent.Data)
return nil
}
116 changes: 116 additions & 0 deletions services/notification/processors/batch_email_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package processors

import "testing"

// TestProcessBatchEmailValidPayload tests Process with a valid email payload.
// It should not return an error.
func TestProcessBatchEmailValidPayload(t *testing.T) {
payload := BatchEmailPayload{
Emails: []EmailPayload{
{
To: "[email protected]",
Title: "test1",
Content: "test1",
},
{
To: "[email protected]",
Title: "test2",
Content: "test2",
},
},
}

err := payload.Process()
if err != nil {
t.Errorf("expected no error, got %v", err)
}
}

// TestProcessBatchEmailEmptyList tests Process with an empty 'Emails' field.
// It should return an error.
func TestProcessBatchEmailEmptyList(t *testing.T) {
payload := BatchEmailPayload{
Emails: []EmailPayload{},
}

err := payload.Process()
if err == nil {
t.Errorf("expected error, got nil")
}
}

// TestProcessBatchEmailEmptyTo tests Process with an empty 'To' field.
// It should return an error.
func TestProcessBatchEmailEmptyTo(t *testing.T) {
payload := BatchEmailPayload{
Emails: []EmailPayload{
{
To: "",
Title: "test",
Content: "test",
},
},
}

err := payload.Process()
if err == nil {
t.Errorf("expected error, got nil")
}
}

// TestProcessBatchEmailInvalidTo tests Process with an invalid 'To' field.
// It should return an error.
func TestProcessBatchEmailInvalidTo(t *testing.T) {
payload := BatchEmailPayload{
Emails: []EmailPayload{
{
To: "test",
Title: "test",
Content: "test",
},
},
}

err := payload.Process()
if err == nil {
t.Errorf("expected error, got nil")
}
}

// TestProcessBatchEmailEmptyTitle tests Process with an empty 'Title' field.
// It should return an error.
func TestProcessBatchEmailEmptyTitle(t *testing.T) {
payload := BatchEmailPayload{
Emails: []EmailPayload{
{
To: "[email protected]",
Title: "",
Content: "test",
},
},
}

err := payload.Process()
if err == nil {
t.Errorf("expected error, got nil")
}
}

// TestProcessBatchEmailEmptyContent tests Process with an empty 'Content' field.
// It should return an error.
func TestProcessBatchEmailEmptyContent(t *testing.T) {
payload := BatchEmailPayload{
Emails: []EmailPayload{
{
To: "[email protected]",
Title: "test",
Content: "",
},
},
}

err := payload.Process()
if err == nil {
t.Errorf("expected error, got nil")
}
}
6 changes: 6 additions & 0 deletions services/web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ const config: NextConfig = {
{
hostname: "picsum.photos",
},
{
hostname: "s2.loli.net",
},
{
hostname: "nshm-public.s3.ap-southeast-1.amazonaws.com",
},
],
},
};
Expand Down
Loading

0 comments on commit ef09900

Please sign in to comment.