mirror of
https://github.com/makayabou/asg-server.git
synced 2026-05-02 17:43:36 +02:00
Merge pull request #69 from capcom6/feature/webhooks
Add webhooks support
This commit is contained in:
commit
86178005f0
@ -57,3 +57,23 @@ Authorization: Basic {{localCredentials}}
|
||||
###
|
||||
GET {{localUrl}}/message/8GN2Pz-fzu73NL3398ROE HTTP/1.1
|
||||
Authorization: Basic {{localCredentials}}
|
||||
|
||||
###
|
||||
GET {{localUrl}}/webhooks HTTP/1.1
|
||||
Authorization: Basic {{localCredentials}}
|
||||
|
||||
###
|
||||
POST {{localUrl}}/webhooks HTTP/1.1
|
||||
Authorization: Basic {{localCredentials}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "LreFUt-Z3sSq0JufY9uWB",
|
||||
"url": "https://webhook.site/280a6655-eb68-40b9-b857-af5be37c5303",
|
||||
"event": "sms:received"
|
||||
}
|
||||
|
||||
###
|
||||
DELETE {{localUrl}}/webhooks/LreFUt-Z3sSq0JufY9uWB HTTP/1.1
|
||||
Authorization: Basic {{localCredentials}}
|
||||
|
||||
|
||||
43
api/mobile.http
Normal file
43
api/mobile.http
Normal file
@ -0,0 +1,43 @@
|
||||
@baseUrl={{$dotenv CLOUD__URL}}/api/mobile/v1
|
||||
@mobileToken={{$dotenv MOBILE__TOKEN}}
|
||||
@phone={{$dotenv PHONE}}
|
||||
|
||||
###
|
||||
POST {{baseUrl}}/device HTTP/1.1
|
||||
Authorization: Bearer 123456789
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Android Phone",
|
||||
"pushToken": "eTxx88nfSla87gZuJcW5mS:APA91bHGxVgSqqRtxwFHD1q9em5Oa6xSP4gO_OZRrqOoP1wjf_7UMfXKsc4uws6rWkqn73jYCc1owyATB1v61mqak4ntpqtmRkNtTey7NQXa0Wz3uQZBWY-Ecbn2rWG2VJRihOzXRId-"
|
||||
}
|
||||
|
||||
###
|
||||
GET {{baseUrl}}/message HTTP/1.1
|
||||
Authorization: Bearer {{mobileToken}}
|
||||
|
||||
###
|
||||
PATCH {{baseUrl}}/message HTTP/1.1
|
||||
Authorization: Bearer {{mobileToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"id": "2dcIAhcLg81cez7GE_Pdp",
|
||||
"state": "Failed",
|
||||
"recipients": [
|
||||
{
|
||||
"phoneNumber": "{{phone}}",
|
||||
"state": "Failed"
|
||||
}
|
||||
],
|
||||
"states": {
|
||||
"Processed": "2024-05-13T16:49:17.357+07:00",
|
||||
"Failed": "2024-05-13T16:49:17.357+07:00"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
###
|
||||
GET {{baseUrl}}/webhooks HTTP/1.1
|
||||
Authorization: Bearer {{mobileToken}}
|
||||
@ -9,16 +9,6 @@ GET {{baseUrl}}/health HTTP/1.1
|
||||
###
|
||||
GET {{baseUrl}}/api/3rdparty/v1/health HTTP/1.1
|
||||
|
||||
###
|
||||
POST {{baseUrl}}/api/mobile/v1/device HTTP/1.1
|
||||
Authorization: Bearer 123456789
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Android Phone",
|
||||
"pushToken": "eTxx88nfSla87gZuJcW5mS:APA91bHGxVgSqqRtxwFHD1q9em5Oa6xSP4gO_OZRrqOoP1wjf_7UMfXKsc4uws6rWkqn73jYCc1owyATB1v61mqak4ntpqtmRkNtTey7NQXa0Wz3uQZBWY-Ecbn2rWG2VJRihOzXRId-"
|
||||
}
|
||||
|
||||
###
|
||||
POST {{baseUrl}}/api/3rdparty/v1/message?skipPhoneValidation=false HTTP/1.1
|
||||
Content-Type: application/json
|
||||
@ -58,32 +48,6 @@ Authorization: Basic {{credentials}}
|
||||
GET {{baseUrl}}/api/3rdparty/v1/device HTTP/1.1
|
||||
Authorization: Basic {{credentials}}
|
||||
|
||||
###
|
||||
GET {{baseUrl}}/api/mobile/v1/message HTTP/1.1
|
||||
Authorization: Bearer {{mobileToken}}
|
||||
|
||||
###
|
||||
PATCH {{baseUrl}}/api/mobile/v1/message HTTP/1.1
|
||||
Authorization: Bearer {{mobileToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"id": "2dcIAhcLg81cez7GE_Pdp",
|
||||
"state": "Failed",
|
||||
"recipients": [
|
||||
{
|
||||
"phoneNumber": "{{phone}}",
|
||||
"state": "Failed"
|
||||
}
|
||||
],
|
||||
"states": {
|
||||
"Processed": "2024-05-13T16:49:17.357+07:00",
|
||||
"Failed": "2024-05-13T16:49:17.357+07:00"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
###
|
||||
POST {{baseUrl}}/api/upstream/v1/push HTTP/1.1
|
||||
Content-Type: application/json
|
||||
@ -92,4 +56,24 @@ Content-Type: application/json
|
||||
{
|
||||
"token": "eTxx88nfSla87gZuJcW5mS:APA91bHGxVgSqqRtxwFHD1q9em5Oa6xSP4gO_OZRrqOoP1wjf_7UMfXKsc4uws6rWkqn73jYCc1owyATB1v61mqak4ntpqtmRkNtTey7NQXa0Wz3uQZBWY-Ecbn2rWG2VJRihOzXRId-"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
###
|
||||
GET {{baseUrl}}/api/3rdparty/v1/webhooks HTTP/1.1
|
||||
Authorization: Basic {{credentials}}
|
||||
|
||||
###
|
||||
POST {{baseUrl}}/api/3rdparty/v1/webhooks HTTP/1.1
|
||||
Authorization: Basic {{credentials}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "MYofX8bTd5Bov0wWFZLRP",
|
||||
"url": "https://webhook.site/280a6655-eb68-40b9-b857-af5be37c5303",
|
||||
"event": "sms:received"
|
||||
}
|
||||
|
||||
###
|
||||
DELETE {{baseUrl}}/api/3rdparty/v1/webhooks/MYofX8bTd5Bov0wWFZLRP HTTP/1.1
|
||||
Authorization: Basic {{credentials}}
|
||||
|
||||
|
||||
257
api/swagger.json
257
api/swagger.json
@ -208,6 +208,150 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/3rdparty/webhooks": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns list of registered webhooks",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User",
|
||||
"Webhooks"
|
||||
],
|
||||
"summary": "List webhooks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Webhook list",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/smsgateway.Webhook"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Registers webhook. If webhook with same ID already exists, it will be replaced",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User",
|
||||
"Webhooks"
|
||||
],
|
||||
"summary": "Register webhook",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Webhook",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.Webhook"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.Webhook"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/3rdparty/webhooks/{id}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"ApiAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Deletes webhook",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User",
|
||||
"Webhooks"
|
||||
],
|
||||
"summary": "Delete webhook",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Webhook ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Webhook deleted",
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/health": {
|
||||
"get": {
|
||||
"description": "Checks if service is healthy",
|
||||
@ -429,6 +573,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mobile/v1/webhooks": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"MobileToken": []
|
||||
}
|
||||
],
|
||||
"description": "Returns list of registered webhooks for device",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Device",
|
||||
"Webhooks"
|
||||
],
|
||||
"summary": "List webhooks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Webhook list",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/smsgateway.Webhook"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/upstream/v1/push": {
|
||||
"post": {
|
||||
"description": "Enqueues notifications for sending to devices",
|
||||
@ -799,14 +984,46 @@
|
||||
"ProcessingStateFailed"
|
||||
]
|
||||
},
|
||||
"smsgateway.PushEventType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"MessageEnqueued",
|
||||
"WebhooksUpdated"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"PushMessageEnqueued",
|
||||
"PushWebhooksUpdated"
|
||||
]
|
||||
},
|
||||
"smsgateway.PushNotification": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"token"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"description": "The additional data associated with the event.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"description": "The type of event.",
|
||||
"default": "MessageEnqueued",
|
||||
"enum": [
|
||||
"MessageEnqueued",
|
||||
"WebhooksUpdated"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/smsgateway.PushEventType"
|
||||
}
|
||||
],
|
||||
"example": "MessageEnqueued"
|
||||
},
|
||||
"token": {
|
||||
"description": "Device FCM token",
|
||||
"description": "The token of the device that receives the notification.",
|
||||
"type": "string",
|
||||
"example": "PyDmBQZZXYmyxMwED8Fzy"
|
||||
}
|
||||
@ -841,6 +1058,44 @@
|
||||
"example": "Pending"
|
||||
}
|
||||
}
|
||||
},
|
||||
"smsgateway.Webhook": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"event",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"event": {
|
||||
"description": "The type of event the webhook is triggered for.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/smsgateway.WebhookEvent"
|
||||
}
|
||||
],
|
||||
"example": "sms:received"
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique identifier of the webhook.",
|
||||
"type": "string",
|
||||
"maxLength": 36,
|
||||
"example": "123e4567-e89b-12d3-a456-426614174000"
|
||||
},
|
||||
"url": {
|
||||
"description": "The URL the webhook will be sent to.",
|
||||
"type": "string",
|
||||
"example": "https://example.com/webhook"
|
||||
}
|
||||
}
|
||||
},
|
||||
"smsgateway.WebhookEvent": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sms:received"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"WebhookEventSmsReceived"
|
||||
]
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
|
||||
169
api/swagger.yaml
169
api/swagger.yaml
@ -238,10 +238,32 @@ definitions:
|
||||
- ProcessingStateSent
|
||||
- ProcessingStateDelivered
|
||||
- ProcessingStateFailed
|
||||
smsgateway.PushEventType:
|
||||
enum:
|
||||
- MessageEnqueued
|
||||
- WebhooksUpdated
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- PushMessageEnqueued
|
||||
- PushWebhooksUpdated
|
||||
smsgateway.PushNotification:
|
||||
properties:
|
||||
data:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: The additional data associated with the event.
|
||||
type: object
|
||||
event:
|
||||
allOf:
|
||||
- $ref: '#/definitions/smsgateway.PushEventType'
|
||||
default: MessageEnqueued
|
||||
description: The type of event.
|
||||
enum:
|
||||
- MessageEnqueued
|
||||
- WebhooksUpdated
|
||||
example: MessageEnqueued
|
||||
token:
|
||||
description: Device FCM token
|
||||
description: The token of the device that receives the notification.
|
||||
example: PyDmBQZZXYmyxMwED8Fzy
|
||||
type: string
|
||||
required:
|
||||
@ -268,6 +290,32 @@ definitions:
|
||||
- phoneNumber
|
||||
- state
|
||||
type: object
|
||||
smsgateway.Webhook:
|
||||
properties:
|
||||
event:
|
||||
allOf:
|
||||
- $ref: '#/definitions/smsgateway.WebhookEvent'
|
||||
description: The type of event the webhook is triggered for.
|
||||
example: sms:received
|
||||
id:
|
||||
description: The unique identifier of the webhook.
|
||||
example: 123e4567-e89b-12d3-a456-426614174000
|
||||
maxLength: 36
|
||||
type: string
|
||||
url:
|
||||
description: The URL the webhook will be sent to.
|
||||
example: https://example.com/webhook
|
||||
type: string
|
||||
required:
|
||||
- event
|
||||
- url
|
||||
type: object
|
||||
smsgateway.WebhookEvent:
|
||||
enum:
|
||||
- sms:received
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- WebhookEventSmsReceived
|
||||
host: sms.capcom.me
|
||||
info:
|
||||
contact:
|
||||
@ -402,6 +450,99 @@ paths:
|
||||
summary: Health check
|
||||
tags:
|
||||
- System
|
||||
/api/v1/3rdparty/webhooks:
|
||||
get:
|
||||
description: Returns list of registered webhooks
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Webhook list
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/smsgateway.Webhook'
|
||||
type: array
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
security:
|
||||
- ApiAuth: []
|
||||
summary: List webhooks
|
||||
tags:
|
||||
- User
|
||||
- Webhooks
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Registers webhook. If webhook with same ID already exists, it will
|
||||
be replaced
|
||||
parameters:
|
||||
- description: Webhook
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.Webhook'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.Webhook'
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
security:
|
||||
- ApiAuth: []
|
||||
summary: Register webhook
|
||||
tags:
|
||||
- User
|
||||
- Webhooks
|
||||
/api/v1/3rdparty/webhooks/{id}:
|
||||
delete:
|
||||
description: Deletes webhook
|
||||
parameters:
|
||||
- description: Webhook ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: Webhook deleted
|
||||
schema:
|
||||
type: object
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
security:
|
||||
- ApiAuth: []
|
||||
summary: Delete webhook
|
||||
tags:
|
||||
- User
|
||||
- Webhooks
|
||||
/health:
|
||||
get:
|
||||
description: Checks if service is healthy
|
||||
@ -544,6 +685,32 @@ paths:
|
||||
tags:
|
||||
- Device
|
||||
- Messages
|
||||
/mobile/v1/webhooks:
|
||||
get:
|
||||
description: Returns list of registered webhooks for device
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Webhook list
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/smsgateway.Webhook'
|
||||
type: array
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
security:
|
||||
- MobileToken: []
|
||||
summary: List webhooks
|
||||
tags:
|
||||
- Device
|
||||
- Webhooks
|
||||
/upstream/v1/push:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
2
go.mod
2
go.mod
@ -4,7 +4,7 @@ go 1.22.0
|
||||
|
||||
require (
|
||||
firebase.google.com/go/v4 v4.12.1
|
||||
github.com/android-sms-gateway/client-go v1.0.0
|
||||
github.com/android-sms-gateway/client-go v1.0.1-0.20240610222412-894fc9370287
|
||||
github.com/capcom6/go-helpers v0.0.0-20240521035030-5f57bddeecee
|
||||
github.com/capcom6/go-infra-fx v0.0.2
|
||||
github.com/go-playground/validator/v10 v10.16.0
|
||||
|
||||
4
go.sum
4
go.sum
@ -32,6 +32,10 @@ github.com/android-sms-gateway/client-go v0.0.0-20240530135354-8d1ce85b9734 h1:d
|
||||
github.com/android-sms-gateway/client-go v0.0.0-20240530135354-8d1ce85b9734/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
|
||||
github.com/android-sms-gateway/client-go v1.0.0 h1:TPRNHlgcEW6jThsx0y4AG1J7wH5Iry+c6h+ailrSQW4=
|
||||
github.com/android-sms-gateway/client-go v1.0.0/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
|
||||
github.com/android-sms-gateway/client-go v1.0.1-0.20240610220902-94dc5641aa00 h1:GUtH5Pw57cxhpQ3y8EYQFTqbpjQ2dZR6G7BjcHK6lAM=
|
||||
github.com/android-sms-gateway/client-go v1.0.1-0.20240610220902-94dc5641aa00/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
|
||||
github.com/android-sms-gateway/client-go v1.0.1-0.20240610222412-894fc9370287 h1:4Q6TuWQcTrKb+nyMrdBTBIV0b4R/xQgmOJhOygHWIkg=
|
||||
github.com/android-sms-gateway/client-go v1.0.1-0.20240610222412-894fc9370287/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
|
||||
@ -12,11 +12,13 @@ import (
|
||||
appconfig "github.com/capcom6/sms-gateway/internal/config"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/auth"
|
||||
appdb "github.com/capcom6/sms-gateway/internal/sms-gateway/modules/db"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/devices"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/health"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/messages"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/push"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/webhooks"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/repositories"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/services"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/fx/fxevent"
|
||||
"go.uber.org/zap"
|
||||
@ -27,16 +29,18 @@ var Module = fx.Module(
|
||||
"server",
|
||||
logger.Module,
|
||||
appconfig.Module,
|
||||
appdb.Module,
|
||||
http.Module,
|
||||
validator.Module,
|
||||
handlers.Module,
|
||||
services.Module,
|
||||
auth.Module,
|
||||
push.Module,
|
||||
repositories.Module,
|
||||
db.Module,
|
||||
messages.Module,
|
||||
health.Module,
|
||||
webhooks.Module,
|
||||
devices.Module,
|
||||
)
|
||||
|
||||
func Run() {
|
||||
|
||||
@ -5,11 +5,13 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/android-sms-gateway/client-go/smsgateway"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/base"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/webhooks"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/auth"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/devices"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/messages"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/repositories"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/services"
|
||||
"github.com/capcom6/sms-gateway/pkg/types"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@ -25,24 +27,26 @@ const (
|
||||
type ThirdPartyHandlerParams struct {
|
||||
fx.In
|
||||
|
||||
HealthHandler *healthHandler
|
||||
HealthHandler *healthHandler
|
||||
WebhooksHandler *webhooks.ThirdPartyController
|
||||
|
||||
AuthSvc *auth.Service
|
||||
MessagesSvc *messages.Service
|
||||
DevicesSvc *services.DevicesService
|
||||
DevicesSvc *devices.Service
|
||||
|
||||
Logger *zap.Logger
|
||||
Validator *validator.Validate
|
||||
}
|
||||
|
||||
type thirdPartyHandler struct {
|
||||
Handler
|
||||
base.Handler
|
||||
|
||||
healthHandler *healthHandler
|
||||
healthHandler *healthHandler
|
||||
webhooksHandler *webhooks.ThirdPartyController
|
||||
|
||||
authSvc *auth.Service
|
||||
messagesSvc *messages.Service
|
||||
devicesSvc *services.DevicesService
|
||||
devicesSvc *devices.Service
|
||||
}
|
||||
|
||||
// @Summary List devices
|
||||
@ -58,7 +62,7 @@ type thirdPartyHandler struct {
|
||||
//
|
||||
// List devices
|
||||
func (h *thirdPartyHandler) getDevice(user models.User, c *fiber.Ctx) error {
|
||||
devices, err := h.devicesSvc.Select(user)
|
||||
devices, err := h.devicesSvc.Select(devices.WithUserID(user.ID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't select devices: %w", err)
|
||||
}
|
||||
@ -106,7 +110,7 @@ func (h *thirdPartyHandler) postMessage(user models.User, c *fiber.Ctx) error {
|
||||
|
||||
skipPhoneValidation := c.QueryBool("skipPhoneValidation", false)
|
||||
|
||||
devices, err := h.devicesSvc.Select(user)
|
||||
devices, err := h.devicesSvc.Select(devices.WithUserID(user.ID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't select devices: %w", err)
|
||||
}
|
||||
@ -166,8 +170,16 @@ func (h *thirdPartyHandler) getMessage(user models.User, c *fiber.Ctx) error {
|
||||
return c.JSON(state)
|
||||
}
|
||||
|
||||
func (h *thirdPartyHandler) authorize(handler func(models.User, *fiber.Ctx) error) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
func (h *thirdPartyHandler) Register(router fiber.Router) {
|
||||
router = router.Group("/3rdparty/v1")
|
||||
|
||||
h.healthHandler.Register(router)
|
||||
|
||||
router.Use(basicauth.New(basicauth.Config{
|
||||
Authorizer: func(username string, password string) bool {
|
||||
return len(username) > 0 && len(password) > 0
|
||||
},
|
||||
}), func(c *fiber.Ctx) error {
|
||||
username := c.Locals("username").(string)
|
||||
password := c.Locals("password").(string)
|
||||
|
||||
@ -179,33 +191,24 @@ func (h *thirdPartyHandler) authorize(handler func(models.User, *fiber.Ctx) erro
|
||||
|
||||
c.Locals("user", user)
|
||||
|
||||
return handler(user, c)
|
||||
}
|
||||
}
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
func (h *thirdPartyHandler) Register(router fiber.Router) {
|
||||
router = router.Group("/3rdparty/v1")
|
||||
router.Get("/device", auth.WithUser(h.getDevice))
|
||||
|
||||
h.healthHandler.Register(router)
|
||||
router.Post("/message", auth.WithUser(h.postMessage))
|
||||
router.Get("/message/:id", auth.WithUser(h.getMessage)).Name(route3rdPartyGetMessage)
|
||||
|
||||
router.Use(basicauth.New(basicauth.Config{
|
||||
Authorizer: func(username string, password string) bool {
|
||||
return len(username) > 0 && len(password) > 0
|
||||
},
|
||||
}))
|
||||
|
||||
router.Get("/device", h.authorize(h.getDevice))
|
||||
|
||||
router.Post("/message", h.authorize(h.postMessage))
|
||||
router.Get("/message/:id", h.authorize(h.getMessage)).Name(route3rdPartyGetMessage)
|
||||
h.webhooksHandler.Register(router.Group("/webhooks"))
|
||||
}
|
||||
|
||||
func newThirdPartyHandler(params ThirdPartyHandlerParams) *thirdPartyHandler {
|
||||
return &thirdPartyHandler{
|
||||
Handler: Handler{Logger: params.Logger.Named("ThirdPartyHandler"), Validator: params.Validator},
|
||||
healthHandler: params.HealthHandler,
|
||||
authSvc: params.AuthSvc,
|
||||
messagesSvc: params.MessagesSvc,
|
||||
devicesSvc: params.DevicesSvc,
|
||||
Handler: base.Handler{Logger: params.Logger.Named("ThirdPartyHandler"), Validator: params.Validator},
|
||||
healthHandler: params.HealthHandler,
|
||||
webhooksHandler: params.WebhooksHandler,
|
||||
authSvc: params.AuthSvc,
|
||||
messagesSvc: params.MessagesSvc,
|
||||
devicesSvc: params.DevicesSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -22,7 +22,7 @@ func (h *Handler) BodyParserValidator(c *fiber.Ctx, out any) error {
|
||||
return fmt.Errorf("can't parse body: %w", err)
|
||||
}
|
||||
|
||||
return h.validateStruct(out)
|
||||
return h.ValidateStruct(out)
|
||||
}
|
||||
|
||||
func (h *Handler) QueryParserValidator(c *fiber.Ctx, out any) error {
|
||||
@ -30,7 +30,7 @@ func (h *Handler) QueryParserValidator(c *fiber.Ctx, out any) error {
|
||||
return fmt.Errorf("can't parse query: %w", err)
|
||||
}
|
||||
|
||||
return h.validateStruct(out)
|
||||
return h.ValidateStruct(out)
|
||||
}
|
||||
|
||||
func (h *Handler) ParamsParserValidator(c *fiber.Ctx, out any) error {
|
||||
@ -38,10 +38,10 @@ func (h *Handler) ParamsParserValidator(c *fiber.Ctx, out any) error {
|
||||
return fmt.Errorf("can't parse params: %w", err)
|
||||
}
|
||||
|
||||
return h.validateStruct(out)
|
||||
return h.ValidateStruct(out)
|
||||
}
|
||||
|
||||
func (h *Handler) validateStruct(out any) error {
|
||||
func (h *Handler) ValidateStruct(out any) error {
|
||||
if h.Validator != nil {
|
||||
if err := h.Validator.Struct(out); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package base
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -197,7 +197,7 @@ func TestHandler_validateStruct(t *testing.T) {
|
||||
Logger: tt.fields.Logger,
|
||||
Validator: tt.fields.Validator,
|
||||
}
|
||||
if err := h.validateStruct(tt.args.out); (err != nil) != tt.wantErr {
|
||||
if err := h.ValidateStruct(tt.args.out); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Handler.validateStruct() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"github.com/android-sms-gateway/client-go/smsgateway"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/base"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/health"
|
||||
"github.com/capcom6/sms-gateway/internal/version"
|
||||
"github.com/capcom6/sms-gateway/pkg/maps"
|
||||
@ -19,7 +20,7 @@ type healthHanlderParams struct {
|
||||
}
|
||||
|
||||
type healthHandler struct {
|
||||
Handler
|
||||
base.Handler
|
||||
|
||||
healthSvc *health.Service
|
||||
|
||||
@ -72,7 +73,7 @@ func (h *healthHandler) Register(router fiber.Router) {
|
||||
|
||||
func newHealthHandler(params healthHanlderParams) *healthHandler {
|
||||
return &healthHandler{
|
||||
Handler: Handler{Logger: params.Logger.Named("HealthHandler"), Validator: nil},
|
||||
Handler: base.Handler{Logger: params.Logger.Named("HealthHandler"), Validator: nil},
|
||||
healthSvc: params.HealthSvc,
|
||||
logger: params.Logger,
|
||||
}
|
||||
|
||||
@ -7,6 +7,8 @@ import (
|
||||
|
||||
"github.com/android-sms-gateway/client-go/smsgateway"
|
||||
"github.com/capcom6/go-infra-fx/http/apikey"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/base"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/webhooks"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/auth"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/messages"
|
||||
@ -20,11 +22,13 @@ import (
|
||||
)
|
||||
|
||||
type mobileHandler struct {
|
||||
Handler
|
||||
base.Handler
|
||||
|
||||
authSvc *auth.Service
|
||||
messagesSvc *messages.Service
|
||||
|
||||
webhooksCtrl *webhooks.MobileController
|
||||
|
||||
idGen func() string
|
||||
}
|
||||
|
||||
@ -58,7 +62,7 @@ func (h *mobileHandler) postDevice(c *fiber.Ctx) error {
|
||||
return fmt.Errorf("can't create user: %w", err)
|
||||
}
|
||||
|
||||
device, err := h.authSvc.RegisterDevice(user.ID, req.Name, req.PushToken)
|
||||
device, err := h.authSvc.RegisterDevice(user, req.Name, req.PushToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't register device: %w", err)
|
||||
}
|
||||
@ -142,7 +146,7 @@ func (h *mobileHandler) patchMessage(device models.Device, c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
for _, v := range req {
|
||||
if err := h.validateStruct(v); err != nil {
|
||||
if err := h.ValidateStruct(v); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
@ -155,20 +159,6 @@ func (h *mobileHandler) patchMessage(device models.Device, c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *mobileHandler) authorize(handler func(models.Device, *fiber.Ctx) error) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
token := c.Locals("token").(string)
|
||||
|
||||
device, err := h.authSvc.AuthorizeDevice(token)
|
||||
if err != nil {
|
||||
h.Logger.Error("Can't authorize device", zap.Error(err))
|
||||
return fiber.ErrUnauthorized
|
||||
}
|
||||
|
||||
return handler(device, c)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *mobileHandler) Register(router fiber.Router) {
|
||||
router = router.Group("/mobile/v1")
|
||||
|
||||
@ -183,15 +173,29 @@ func (h *mobileHandler) Register(router fiber.Router) {
|
||||
Authorizer: func(token string) bool {
|
||||
return len(token) > 0
|
||||
},
|
||||
}))
|
||||
}), func(c *fiber.Ctx) error {
|
||||
token := c.Locals("token").(string)
|
||||
|
||||
router.Patch("/device", h.authorize(h.patchDevice))
|
||||
device, err := h.authSvc.AuthorizeDevice(token)
|
||||
if err != nil {
|
||||
h.Logger.Error("Can't authorize device", zap.Error(err))
|
||||
return fiber.ErrUnauthorized
|
||||
}
|
||||
|
||||
router.Get("/message", h.authorize(h.getMessage))
|
||||
router.Patch("/message", h.authorize(h.patchMessage))
|
||||
c.Locals("device", device)
|
||||
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
router.Patch("/device", auth.WithDevice(h.patchDevice))
|
||||
|
||||
router.Get("/message", auth.WithDevice(h.getMessage))
|
||||
router.Patch("/message", auth.WithDevice(h.patchMessage))
|
||||
|
||||
h.webhooksCtrl.Register(router.Group("/webhooks"))
|
||||
}
|
||||
|
||||
type MobileHandlerParams struct {
|
||||
type mobileHandlerParams struct {
|
||||
fx.In
|
||||
|
||||
Logger *zap.Logger
|
||||
@ -199,15 +203,18 @@ type MobileHandlerParams struct {
|
||||
|
||||
AuthSvc *auth.Service
|
||||
MessagesSvc *messages.Service
|
||||
|
||||
WebhooksCtrl *webhooks.MobileController
|
||||
}
|
||||
|
||||
func newMobileHandler(params MobileHandlerParams) *mobileHandler {
|
||||
func newMobileHandler(params mobileHandlerParams) *mobileHandler {
|
||||
idGen, _ := nanoid.Standard(21)
|
||||
|
||||
return &mobileHandler{
|
||||
Handler: Handler{Logger: params.Logger, Validator: params.Validator},
|
||||
authSvc: params.AuthSvc,
|
||||
messagesSvc: params.MessagesSvc,
|
||||
idGen: idGen,
|
||||
Handler: base.Handler{Logger: params.Logger, Validator: params.Validator},
|
||||
authSvc: params.AuthSvc,
|
||||
messagesSvc: params.MessagesSvc,
|
||||
webhooksCtrl: params.WebhooksCtrl,
|
||||
idGen: idGen,
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"github.com/capcom6/go-infra-fx/http"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/webhooks"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@ -19,6 +20,8 @@ var Module = fx.Module(
|
||||
),
|
||||
fx.Provide(
|
||||
newHealthHandler,
|
||||
webhooks.NewThirdPartyController,
|
||||
webhooks.NewMobileController,
|
||||
fx.Private,
|
||||
),
|
||||
)
|
||||
|
||||
@ -4,7 +4,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/android-sms-gateway/client-go/smsgateway"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/base"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/push"
|
||||
"github.com/capcom6/sms-gateway/pkg/types"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||
@ -13,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
type upstreamHandler struct {
|
||||
Handler
|
||||
base.Handler
|
||||
|
||||
config Config
|
||||
pushSvc *push.Service
|
||||
@ -31,7 +33,7 @@ type upstreamHandlerParams struct {
|
||||
|
||||
func newUpstreamHandler(params upstreamHandlerParams) *upstreamHandler {
|
||||
return &upstreamHandler{
|
||||
Handler: Handler{Logger: params.Logger, Validator: params.Validator},
|
||||
Handler: base.Handler{Logger: params.Logger, Validator: params.Validator},
|
||||
config: params.Config,
|
||||
pushSvc: params.PushSvc,
|
||||
}
|
||||
@ -62,11 +64,16 @@ func (h *upstreamHandler) postPush(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
for _, v := range req {
|
||||
if err := h.validateStruct(v); err != nil {
|
||||
if err := h.ValidateStruct(v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.pushSvc.Enqueue(c.Context(), v.Token, map[string]string{}); err != nil {
|
||||
event := push.Event{
|
||||
Event: types.ZeroDefault(v.Event, smsgateway.PushMessageEnqueued),
|
||||
Data: v.Data,
|
||||
}
|
||||
|
||||
if err := h.pushSvc.Enqueue(v.Token, &event); err != nil {
|
||||
h.Logger.Error("Can't push message", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
116
internal/sms-gateway/handlers/webhooks/3rdparty.go
Normal file
116
internal/sms-gateway/handlers/webhooks/3rdparty.go
Normal file
@ -0,0 +1,116 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/android-sms-gateway/client-go/smsgateway"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/base"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/auth"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/webhooks"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type thirdPartyControllerParams struct {
|
||||
fx.In
|
||||
|
||||
WebhooksSvc *webhooks.Service
|
||||
|
||||
Validator *validator.Validate
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
type ThirdPartyController struct {
|
||||
base.Handler
|
||||
|
||||
webhooksSvc *webhooks.Service
|
||||
}
|
||||
|
||||
// @Summary List webhooks
|
||||
// @Description Returns list of registered webhooks
|
||||
// @Security ApiAuth
|
||||
// @Tags User, Webhooks
|
||||
// @Produce json
|
||||
// @Success 200 {object} []smsgateway.Webhook "Webhook list"
|
||||
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
|
||||
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
|
||||
// @Router /api/v1/3rdparty/webhooks [get]
|
||||
//
|
||||
// List webhooks
|
||||
func (h *ThirdPartyController) get(user models.User, c *fiber.Ctx) error {
|
||||
items, err := h.webhooksSvc.Select(user.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't select webhooks: %w", err)
|
||||
}
|
||||
|
||||
return c.JSON(items)
|
||||
}
|
||||
|
||||
// @Summary Register webhook
|
||||
// @Description Registers webhook. If webhook with same ID already exists, it will be replaced
|
||||
// @Security ApiAuth
|
||||
// @Tags User, Webhooks
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body smsgateway.Webhook true "Webhook"
|
||||
// @Success 201 {object} smsgateway.Webhook "Created"
|
||||
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
|
||||
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
|
||||
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
|
||||
// @Router /api/v1/3rdparty/webhooks [post]
|
||||
//
|
||||
// Register webhook
|
||||
func (h *ThirdPartyController) post(user models.User, c *fiber.Ctx) error {
|
||||
dto := &smsgateway.Webhook{}
|
||||
|
||||
if err := h.BodyParserValidator(c, dto); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.webhooksSvc.Replace(user.ID, dto); err != nil {
|
||||
return fmt.Errorf("can't write webhook: %w", err)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(dto)
|
||||
}
|
||||
|
||||
// @Summary Delete webhook
|
||||
// @Description Deletes webhook
|
||||
// @Security ApiAuth
|
||||
// @Tags User, Webhooks
|
||||
// @Produce json
|
||||
// @Param id path string true "Webhook ID"
|
||||
// @Success 204 {object} object "Webhook deleted"
|
||||
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
|
||||
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
|
||||
// @Router /api/v1/3rdparty/webhooks/{id} [delete]
|
||||
//
|
||||
// Delete webhook
|
||||
func (h *ThirdPartyController) delete(user models.User, c *fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
|
||||
if err := h.webhooksSvc.Delete(user.ID, webhooks.WithExtID(id)); err != nil {
|
||||
return fmt.Errorf("can't delete webhook: %w", err)
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *ThirdPartyController) Register(router fiber.Router) {
|
||||
router.Get("/", auth.WithUser(h.get))
|
||||
router.Post("/", auth.WithUser(h.post))
|
||||
router.Delete("/:id", auth.WithUser(h.delete))
|
||||
}
|
||||
|
||||
func NewThirdPartyController(params thirdPartyControllerParams) *ThirdPartyController {
|
||||
return &ThirdPartyController{
|
||||
Handler: base.Handler{
|
||||
Logger: params.Logger.Named("controller"),
|
||||
Validator: params.Validator,
|
||||
},
|
||||
webhooksSvc: params.WebhooksSvc,
|
||||
}
|
||||
}
|
||||
60
internal/sms-gateway/handlers/webhooks/mobile.go
Normal file
60
internal/sms-gateway/handlers/webhooks/mobile.go
Normal file
@ -0,0 +1,60 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers/base"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/auth"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/webhooks"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type mobileControllerParams struct {
|
||||
fx.In
|
||||
|
||||
WebhooksServices *webhooks.Service
|
||||
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
type MobileController struct {
|
||||
base.Handler
|
||||
|
||||
webhooksSvc *webhooks.Service
|
||||
}
|
||||
|
||||
// @Summary List webhooks
|
||||
// @Description Returns list of registered webhooks for device
|
||||
// @Security MobileToken
|
||||
// @Tags Device, Webhooks
|
||||
// @Produce json
|
||||
// @Success 200 {object} []smsgateway.Webhook "Webhook list"
|
||||
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
|
||||
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
|
||||
// @Router /mobile/v1/webhooks [get]
|
||||
//
|
||||
// List webhooks
|
||||
func (h *MobileController) get(device models.Device, c *fiber.Ctx) error {
|
||||
items, err := h.webhooksSvc.Select(device.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't select webhooks: %w", err)
|
||||
}
|
||||
|
||||
return c.JSON(items)
|
||||
}
|
||||
|
||||
func (h *MobileController) Register(router fiber.Router) {
|
||||
router.Get("/", auth.WithDevice(h.get))
|
||||
}
|
||||
|
||||
func NewMobileController(params mobileControllerParams) *MobileController {
|
||||
return &MobileController{
|
||||
Handler: base.Handler{
|
||||
Logger: params.Logger.Named("mobile"),
|
||||
},
|
||||
webhooksSvc: params.WebhooksServices,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE `webhooks` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT,
|
||||
`ext_id` varchar(36) NOT NULL,
|
||||
`user_id` varchar(32) NOT NULL,
|
||||
`url` varchar(256) NOT NULL,
|
||||
`event` varchar(32) NOT NULL,
|
||||
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
`deleted_at` datetime(3) NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE INDEX `unq_webhooks_user_extid` (`user_id`, `ext_id`),
|
||||
CONSTRAINT `fk_webhooks_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
---
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE `webhooks`;
|
||||
-- +goose StatementEnd
|
||||
@ -15,9 +15,9 @@ const (
|
||||
)
|
||||
|
||||
type TimedModel struct {
|
||||
CreatedAt time.Time `gorm:"not null;autocreatetime:false;default:CURRENT_TIMESTAMP(3)"`
|
||||
UpdatedAt time.Time `gorm:"not null;autoupdatetime:false;default:CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"`
|
||||
DeletedAt *time.Time
|
||||
CreatedAt time.Time `gorm:"->;not null;autocreatetime:false;default:CURRENT_TIMESTAMP(3)"`
|
||||
UpdatedAt time.Time `gorm:"->;not null;autoupdatetime:false;default:CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"`
|
||||
DeletedAt *time.Time `gorm:"<-:update"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
|
||||
18
internal/sms-gateway/modules/auth/decorators.go
Normal file
18
internal/sms-gateway/modules/auth/decorators.go
Normal file
@ -0,0 +1,18 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func WithUser(handler func(models.User, *fiber.Ctx) error) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
return handler(c.Locals("user").(models.User), c)
|
||||
}
|
||||
}
|
||||
|
||||
func WithDevice(handler func(models.Device, *fiber.Ctx) error) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
return handler(c.Locals("device").(models.Device), c)
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/devices"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/repositories"
|
||||
"github.com/capcom6/sms-gateway/pkg/crypto"
|
||||
"github.com/jaevor/go-nanoid"
|
||||
@ -21,8 +22,8 @@ type Params struct {
|
||||
|
||||
Config Config
|
||||
|
||||
Users *repositories.UsersRepository
|
||||
Devices *repositories.DevicesRepository
|
||||
Users *repositories.UsersRepository
|
||||
DevicesSvc *devices.Service
|
||||
|
||||
Logger *zap.Logger
|
||||
}
|
||||
@ -30,8 +31,8 @@ type Params struct {
|
||||
type Service struct {
|
||||
config Config
|
||||
|
||||
users *repositories.UsersRepository
|
||||
devices *repositories.DevicesRepository
|
||||
users *repositories.UsersRepository
|
||||
devicesSvc *devices.Service
|
||||
|
||||
logger *zap.Logger
|
||||
|
||||
@ -42,11 +43,11 @@ func New(params Params) *Service {
|
||||
idgen, _ := nanoid.Standard(21)
|
||||
|
||||
return &Service{
|
||||
config: params.Config,
|
||||
users: params.Users,
|
||||
devices: params.Devices,
|
||||
logger: params.Logger.Named("Service"),
|
||||
idgen: idgen,
|
||||
config: params.Config,
|
||||
users: params.Users,
|
||||
devicesSvc: params.DevicesSvc,
|
||||
logger: params.Logger.Named("Service"),
|
||||
idgen: idgen,
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,20 +68,17 @@ func (s *Service) RegisterUser(login, password string) (models.User, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Service) RegisterDevice(userID string, name, pushToken *string) (models.Device, error) {
|
||||
func (s *Service) RegisterDevice(user models.User, name, pushToken *string) (models.Device, error) {
|
||||
device := models.Device{
|
||||
ID: s.idgen(),
|
||||
Name: name,
|
||||
AuthToken: s.idgen(),
|
||||
PushToken: pushToken,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
return device, s.devices.Insert(&device)
|
||||
return device, s.devicesSvc.Insert(user, &device)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateDevice(id, pushToken string) error {
|
||||
return s.devices.UpdateToken(id, pushToken)
|
||||
return s.devicesSvc.UpdateToken(id, pushToken)
|
||||
}
|
||||
|
||||
func (s *Service) IsPublic() bool {
|
||||
@ -100,12 +98,12 @@ func (s *Service) AuthorizeRegistration(token string) error {
|
||||
}
|
||||
|
||||
func (s *Service) AuthorizeDevice(token string) (models.Device, error) {
|
||||
device, err := s.devices.GetByToken(token)
|
||||
device, err := s.devicesSvc.Get(devices.WithToken(token))
|
||||
if err != nil {
|
||||
return device, err
|
||||
}
|
||||
|
||||
if err := s.devices.UpdateLastSeen(device.ID); err != nil {
|
||||
if err := s.devicesSvc.UpdateLastSeen(device.ID); err != nil {
|
||||
s.logger.Error("can't update last seen", zap.Error(err))
|
||||
}
|
||||
|
||||
|
||||
15
internal/sms-gateway/modules/db/module.go
Normal file
15
internal/sms-gateway/modules/db/module.go
Normal file
@ -0,0 +1,15 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/jaevor/go-nanoid"
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
type IDGen func() string
|
||||
|
||||
var Module = fx.Module(
|
||||
"db",
|
||||
fx.Provide(func() (IDGen, error) {
|
||||
return nanoid.Standard(21)
|
||||
}),
|
||||
)
|
||||
@ -1,4 +1,4 @@
|
||||
package services
|
||||
package devices
|
||||
|
||||
import (
|
||||
"go.uber.org/fx"
|
||||
@ -6,11 +6,15 @@ import (
|
||||
)
|
||||
|
||||
var Module = fx.Module(
|
||||
"services",
|
||||
"devices",
|
||||
fx.Decorate(func(log *zap.Logger) *zap.Logger {
|
||||
return log.Named("services")
|
||||
return log.Named("devices")
|
||||
}),
|
||||
fx.Provide(
|
||||
NewDevicesService,
|
||||
newDevicesRepository,
|
||||
fx.Private,
|
||||
),
|
||||
fx.Provide(
|
||||
NewService,
|
||||
),
|
||||
)
|
||||
65
internal/sms-gateway/modules/devices/repository.go
Normal file
65
internal/sms-gateway/modules/devices/repository.go
Normal file
@ -0,0 +1,65 @@
|
||||
package devices
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = gorm.ErrRecordNotFound
|
||||
ErrInvalidFilter = errors.New("invalid filter")
|
||||
ErrMoreThanOne = errors.New("more than one record")
|
||||
)
|
||||
|
||||
type repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *repository) Select(filter ...SelectFilter) ([]models.Device, error) {
|
||||
if len(filter) == 0 {
|
||||
return nil, ErrInvalidFilter
|
||||
}
|
||||
|
||||
f := newFilter(filter...)
|
||||
devices := []models.Device{}
|
||||
|
||||
return devices, f.apply(r.db).Find(&devices).Error
|
||||
}
|
||||
|
||||
func (r *repository) Get(filter ...SelectFilter) (models.Device, error) {
|
||||
devices, err := r.Select(filter...)
|
||||
if err != nil {
|
||||
return models.Device{}, err
|
||||
}
|
||||
|
||||
if len(devices) == 0 {
|
||||
return models.Device{}, ErrNotFound
|
||||
}
|
||||
|
||||
if len(devices) > 1 {
|
||||
return models.Device{}, ErrMoreThanOne
|
||||
}
|
||||
|
||||
return devices[0], nil
|
||||
}
|
||||
|
||||
func (r *repository) Insert(device *models.Device) error {
|
||||
return r.db.Create(device).Error
|
||||
}
|
||||
|
||||
func (r *repository) UpdateToken(id, token string) error {
|
||||
return r.db.Model(&models.Device{}).Where("id", id).Update("push_token", token).Error
|
||||
}
|
||||
|
||||
func (r *repository) UpdateLastSeen(id string) error {
|
||||
return r.db.Model(&models.Device{}).Where("id", id).Update("last_seen", time.Now()).Error
|
||||
}
|
||||
|
||||
func newDevicesRepository(db *gorm.DB) *repository {
|
||||
return &repository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
54
internal/sms-gateway/modules/devices/repository_filter.go
Normal file
54
internal/sms-gateway/modules/devices/repository_filter.go
Normal file
@ -0,0 +1,54 @@
|
||||
package devices
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type SelectFilter func(*selectFilter)
|
||||
|
||||
func WithID(id string) SelectFilter {
|
||||
return func(f *selectFilter) {
|
||||
f.id = &id
|
||||
}
|
||||
}
|
||||
|
||||
func WithToken(token string) SelectFilter {
|
||||
return func(f *selectFilter) {
|
||||
f.token = &token
|
||||
}
|
||||
}
|
||||
|
||||
func WithUserID(userID string) SelectFilter {
|
||||
return func(f *selectFilter) {
|
||||
f.userID = &userID
|
||||
}
|
||||
}
|
||||
|
||||
type selectFilter struct {
|
||||
id *string
|
||||
userID *string
|
||||
token *string
|
||||
}
|
||||
|
||||
func newFilter(filters ...SelectFilter) *selectFilter {
|
||||
f := &selectFilter{}
|
||||
f.merge(filters...)
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *selectFilter) merge(filters ...SelectFilter) {
|
||||
for _, filter := range filters {
|
||||
filter(f)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *selectFilter) apply(query *gorm.DB) *gorm.DB {
|
||||
if f.id != nil {
|
||||
query = query.Where("id = ?", *f.id)
|
||||
}
|
||||
if f.token != nil {
|
||||
query = query.Where("auth_token = ?", *f.token)
|
||||
}
|
||||
if f.userID != nil {
|
||||
query = query.Where("user_id = ?", *f.userID)
|
||||
}
|
||||
return query
|
||||
}
|
||||
58
internal/sms-gateway/modules/devices/service.go
Normal file
58
internal/sms-gateway/modules/devices/service.go
Normal file
@ -0,0 +1,58 @@
|
||||
package devices
|
||||
|
||||
import (
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/db"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ServiceParams struct {
|
||||
fx.In
|
||||
|
||||
Devices *repository
|
||||
|
||||
IDGen db.IDGen
|
||||
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
devices *repository
|
||||
|
||||
idGen db.IDGen
|
||||
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func (s *Service) Insert(user models.User, device *models.Device) error {
|
||||
device.ID = s.idGen()
|
||||
device.AuthToken = s.idGen()
|
||||
device.UserID = user.ID
|
||||
|
||||
return s.devices.Insert(device)
|
||||
}
|
||||
|
||||
func (s *Service) Select(filter ...SelectFilter) ([]models.Device, error) {
|
||||
return s.devices.Select(filter...)
|
||||
}
|
||||
|
||||
func (s *Service) Get(filter ...SelectFilter) (models.Device, error) {
|
||||
return s.devices.Get(filter...)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateToken(deviceId string, token string) error {
|
||||
return s.devices.UpdateToken(deviceId, token)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateLastSeen(deviceId string) error {
|
||||
return s.devices.UpdateLastSeen(deviceId)
|
||||
}
|
||||
|
||||
func NewService(params ServiceParams) *Service {
|
||||
return &Service{
|
||||
devices: params.Devices,
|
||||
idGen: params.IDGen,
|
||||
logger: params.Logger.Named("service"),
|
||||
}
|
||||
}
|
||||
@ -10,10 +10,10 @@ import (
|
||||
"github.com/android-sms-gateway/client-go/smsgateway"
|
||||
"github.com/capcom6/go-helpers/slices"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/db"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/push"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/repositories"
|
||||
"github.com/capcom6/sms-gateway/pkg/types"
|
||||
"github.com/jaevor/go-nanoid"
|
||||
"github.com/nyaruka/phonenumbers"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
@ -37,6 +37,8 @@ type EnqueueOptions struct {
|
||||
type ServiceParams struct {
|
||||
fx.In
|
||||
|
||||
IDGen db.IDGen
|
||||
|
||||
Messages *repositories.MessagesRepository
|
||||
HashingTask *HashingTask
|
||||
|
||||
@ -55,8 +57,6 @@ type Service struct {
|
||||
}
|
||||
|
||||
func NewService(params ServiceParams) *Service {
|
||||
idgen, _ := nanoid.Standard(21)
|
||||
|
||||
return &Service{
|
||||
Messages: params.Messages,
|
||||
HashingTask: params.HashingTask,
|
||||
@ -64,7 +64,7 @@ func NewService(params ServiceParams) *Service {
|
||||
PushSvc: params.PushSvc,
|
||||
Logger: params.Logger.Named("Service"),
|
||||
|
||||
idgen: idgen,
|
||||
idgen: params.IDGen,
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,10 +214,7 @@ func (s *Service) Enqeue(device models.Device, message smsgateway.Message, opts
|
||||
}
|
||||
|
||||
go func(token string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := s.PushSvc.Enqueue(ctx, token, map[string]string{}); err != nil {
|
||||
if err := s.PushSvc.Enqueue(token, push.NewMessageEnqueuedEvent()); err != nil {
|
||||
s.Logger.Error("Can't enqueue message", zap.String("token", token), zap.Error(err))
|
||||
}
|
||||
}(*device.PushToken)
|
||||
|
||||
36
internal/sms-gateway/modules/push/events.go
Normal file
36
internal/sms-gateway/modules/push/events.go
Normal file
@ -0,0 +1,36 @@
|
||||
package push
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/android-sms-gateway/client-go/smsgateway"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Event smsgateway.PushEventType
|
||||
Data any
|
||||
}
|
||||
|
||||
func (e *Event) Map() map[string]string {
|
||||
json, _ := json.Marshal(e.Data)
|
||||
|
||||
return map[string]string{
|
||||
"event": string(e.Event),
|
||||
"data": string(json),
|
||||
}
|
||||
}
|
||||
|
||||
func NewEvent(event smsgateway.PushEventType, data any) *Event {
|
||||
return &Event{
|
||||
Event: event,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func NewMessageEnqueuedEvent() *Event {
|
||||
return NewEvent(smsgateway.PushMessageEnqueued, nil)
|
||||
}
|
||||
|
||||
func NewWebhooksUpdatedEvent() *Event {
|
||||
return NewEvent(smsgateway.PushWebhooksUpdated, nil)
|
||||
}
|
||||
@ -70,8 +70,8 @@ func (s *Service) Run(ctx context.Context) {
|
||||
}
|
||||
|
||||
// Enqueue adds the data to the cache and immediately sends all messages if the debounce is 0.
|
||||
func (s *Service) Enqueue(ctx context.Context, token string, data map[string]string) error {
|
||||
s.cache.Set(token, data)
|
||||
func (s *Service) Enqueue(token string, event *Event) error {
|
||||
s.cache.Set(token, event.Map())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
11
internal/sms-gateway/modules/webhooks/converters.go
Normal file
11
internal/sms-gateway/modules/webhooks/converters.go
Normal file
@ -0,0 +1,11 @@
|
||||
package webhooks
|
||||
|
||||
import "github.com/android-sms-gateway/client-go/smsgateway"
|
||||
|
||||
func webhookToDTO(model *Webhook) smsgateway.Webhook {
|
||||
return smsgateway.Webhook{
|
||||
ID: model.ExtID,
|
||||
URL: model.URL,
|
||||
Event: model.Event,
|
||||
}
|
||||
}
|
||||
24
internal/sms-gateway/modules/webhooks/models.go
Normal file
24
internal/sms-gateway/modules/webhooks/models.go
Normal file
@ -0,0 +1,24 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"github.com/android-sms-gateway/client-go/smsgateway"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Webhook struct {
|
||||
ID uint64 `json:"-" gorm:"->;primaryKey;type:BIGINT UNSIGNED;autoIncrement"`
|
||||
ExtID string `json:"id" gorm:"not null;type:varchar(36);uniqueIndex:unq_webhooks_user_extid,priority:2"`
|
||||
UserID string `json:"-" gorm:"<-:create;not null;type:varchar(32);uniqueIndex:unq_webhooks_user_extid,priority:1"`
|
||||
|
||||
URL string `json:"url" validate:"required,http_url" gorm:"not null;type:varchar(256)"`
|
||||
Event smsgateway.WebhookEvent `json:"event" gorm:"not null;type:varchar(32)"`
|
||||
|
||||
User models.User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
|
||||
|
||||
models.TimedModel
|
||||
}
|
||||
|
||||
func Migrate(db *gorm.DB) error {
|
||||
return db.AutoMigrate(&Webhook{})
|
||||
}
|
||||
22
internal/sms-gateway/modules/webhooks/module.go
Normal file
22
internal/sms-gateway/modules/webhooks/module.go
Normal file
@ -0,0 +1,22 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"github.com/capcom6/go-infra-fx/db"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var Module = fx.Module(
|
||||
"webhooks",
|
||||
fx.Decorate(func(log *zap.Logger) *zap.Logger {
|
||||
return log.Named("webhooks")
|
||||
}),
|
||||
fx.Provide(NewRepository, fx.Private),
|
||||
fx.Provide(
|
||||
NewService,
|
||||
),
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterMigration(Migrate)
|
||||
}
|
||||
35
internal/sms-gateway/modules/webhooks/repository.go
Normal file
35
internal/sms-gateway/modules/webhooks/repository.go
Normal file
@ -0,0 +1,35 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *Repository) Select(filters ...SelectFilter) ([]*Webhook, error) {
|
||||
webhooks := []*Webhook{}
|
||||
if err := newFilter(filters...).apply(r.db).Find(&webhooks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return webhooks, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Replace(webhook *Webhook) error {
|
||||
return r.db.
|
||||
Clauses(clause.OnConflict{UpdateAll: true}).
|
||||
Save(webhook).
|
||||
Error
|
||||
}
|
||||
|
||||
func (r *Repository) Delete(filters ...SelectFilter) error {
|
||||
return newFilter(filters...).apply(r.db).Delete(&Webhook{}).Error
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
42
internal/sms-gateway/modules/webhooks/repository_filter.go
Normal file
42
internal/sms-gateway/modules/webhooks/repository_filter.go
Normal file
@ -0,0 +1,42 @@
|
||||
package webhooks
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type SelectFilter func(*selectFilter)
|
||||
|
||||
func WithExtID(extID string) SelectFilter {
|
||||
return func(f *selectFilter) {
|
||||
f.extID = &extID
|
||||
}
|
||||
}
|
||||
|
||||
func WithUserID(userID string) SelectFilter {
|
||||
return func(f *selectFilter) {
|
||||
f.userID = userID
|
||||
}
|
||||
}
|
||||
|
||||
type selectFilter struct {
|
||||
userID string
|
||||
extID *string
|
||||
}
|
||||
|
||||
func newFilter(filters ...SelectFilter) *selectFilter {
|
||||
f := &selectFilter{}
|
||||
f.merge(filters...)
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *selectFilter) merge(filters ...SelectFilter) {
|
||||
for _, filter := range filters {
|
||||
filter(f)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *selectFilter) apply(query *gorm.DB) *gorm.DB {
|
||||
query = query.Where("user_id = ?", f.userID)
|
||||
if f.extID != nil {
|
||||
query = query.Where("ext_id = ?", *f.extID)
|
||||
}
|
||||
return query
|
||||
}
|
||||
112
internal/sms-gateway/modules/webhooks/service.go
Normal file
112
internal/sms-gateway/modules/webhooks/service.go
Normal file
@ -0,0 +1,112 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/android-sms-gateway/client-go/smsgateway"
|
||||
"github.com/capcom6/go-helpers/slices"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/db"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/devices"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/push"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ServiceParams struct {
|
||||
fx.In
|
||||
|
||||
IDGen db.IDGen
|
||||
|
||||
Webhooks *Repository
|
||||
|
||||
DevicesSvc *devices.Service
|
||||
PushSvc *push.Service
|
||||
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
idgen db.IDGen
|
||||
|
||||
webhooks *Repository
|
||||
|
||||
devicesSvc *devices.Service
|
||||
pushSvc *push.Service
|
||||
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewService(params ServiceParams) *Service {
|
||||
return &Service{
|
||||
idgen: params.IDGen,
|
||||
webhooks: params.Webhooks,
|
||||
devicesSvc: params.DevicesSvc,
|
||||
pushSvc: params.PushSvc,
|
||||
logger: params.Logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Select(userID string, filters ...SelectFilter) ([]smsgateway.Webhook, error) {
|
||||
filters = append(filters, WithUserID(userID))
|
||||
|
||||
items, err := s.webhooks.Select(filters...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't select webhooks: %w", err)
|
||||
}
|
||||
|
||||
return slices.Map(items, webhookToDTO), nil
|
||||
}
|
||||
|
||||
func (s *Service) Replace(userID string, webhook *smsgateway.Webhook) error {
|
||||
if webhook.ID == "" {
|
||||
webhook.ID = s.idgen()
|
||||
}
|
||||
|
||||
model := Webhook{
|
||||
ExtID: webhook.ID,
|
||||
UserID: userID,
|
||||
URL: webhook.URL,
|
||||
Event: webhook.Event,
|
||||
}
|
||||
|
||||
if err := s.webhooks.Replace(&model); err != nil {
|
||||
return fmt.Errorf("can't replace webhook: %w", err)
|
||||
}
|
||||
|
||||
go s.notifyDevices(userID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Delete(userID string, filters ...SelectFilter) error {
|
||||
filters = append(filters, WithUserID(userID))
|
||||
if err := s.webhooks.Delete(filters...); err != nil {
|
||||
return fmt.Errorf("can't delete webhooks: %w", err)
|
||||
}
|
||||
|
||||
go s.notifyDevices(userID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) notifyDevices(userID string) {
|
||||
s.logger.Info("Notifying devices", zap.String("user_id", userID))
|
||||
|
||||
devices, err := s.devicesSvc.Select(devices.WithUserID(userID))
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to select devices", zap.String("user_id", userID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, device := range devices {
|
||||
if device.PushToken == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := s.pushSvc.Enqueue(*device.PushToken, push.NewWebhooksUpdatedEvent()); err != nil {
|
||||
s.logger.Error("Failed to send push notification", zap.String("user_id", userID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("Notified devices", zap.String("user_id", userID), zap.Int("count", len(devices)))
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDeviceNotFound = gorm.ErrRecordNotFound
|
||||
)
|
||||
|
||||
type SelectDevicesFilter struct {
|
||||
UserId *string
|
||||
}
|
||||
|
||||
type DevicesRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *DevicesRepository) Select(filter SelectDevicesFilter) ([]models.Device, error) {
|
||||
devices := []models.Device{}
|
||||
|
||||
return devices, r.db.Where(filter).Find(&devices).Error
|
||||
}
|
||||
|
||||
func (r *DevicesRepository) Get(id string) (models.Device, error) {
|
||||
device := models.Device{}
|
||||
|
||||
return device, r.db.Where("id = ?", id).Take(&device).Error
|
||||
}
|
||||
|
||||
func (r *DevicesRepository) GetByToken(token string) (models.Device, error) {
|
||||
device := models.Device{}
|
||||
|
||||
return device, r.db.Where("auth_token = ?", token).Take(&device).Error
|
||||
}
|
||||
|
||||
func (r *DevicesRepository) Insert(device *models.Device) error {
|
||||
return r.db.Create(device).Error
|
||||
}
|
||||
|
||||
func (r *DevicesRepository) UpdateToken(id, token string) error {
|
||||
return r.db.Model(&models.Device{}).Where("id", id).Update("push_token", token).Error
|
||||
}
|
||||
|
||||
func (r *DevicesRepository) UpdateLastSeen(id string) error {
|
||||
return r.db.Model(&models.Device{}).Where("id", id).Update("last_seen", time.Now()).Error
|
||||
}
|
||||
|
||||
func NewDevicesRepository(db *gorm.DB) *DevicesRepository {
|
||||
return &DevicesRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,6 @@ var Module = fx.Module(
|
||||
return log.Named("repositories")
|
||||
}),
|
||||
fx.Provide(
|
||||
NewDevicesRepository,
|
||||
NewMessagesRepository,
|
||||
NewUsersRepository,
|
||||
),
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/repositories"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type DevicesServiceParams struct {
|
||||
fx.In
|
||||
|
||||
Devices *repositories.DevicesRepository
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
type DevicesService struct {
|
||||
Devices *repositories.DevicesRepository
|
||||
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
func (s *DevicesService) Select(user models.User) ([]models.Device, error) {
|
||||
return s.Devices.Select(repositories.SelectDevicesFilter{UserId: &user.ID})
|
||||
}
|
||||
|
||||
func NewDevicesService(params DevicesServiceParams) *DevicesService {
|
||||
return &DevicesService{
|
||||
Devices: params.Devices,
|
||||
Logger: params.Logger.Named("DevicesService"),
|
||||
}
|
||||
}
|
||||
@ -10,3 +10,19 @@ func OrDefault[T any](v *T, def T) T {
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
// ZeroDefault returns the default value if the given value is zero, otherwise it returns the value.
|
||||
//
|
||||
// Parameters:
|
||||
// - v: The value to check.
|
||||
// - def: The default value to return if v is zero.
|
||||
//
|
||||
// Returns:
|
||||
// - The default value if v is zero, otherwise the value.
|
||||
func ZeroDefault[T comparable](v T, def T) T {
|
||||
zero := new(T)
|
||||
if v == *zero {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
36
pkg/types/types_test.go
Normal file
36
pkg/types/types_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestZeroDefault(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
def string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "String zero value",
|
||||
value: "",
|
||||
def: "default",
|
||||
want: "default",
|
||||
},
|
||||
{
|
||||
name: "String non-zero value",
|
||||
value: "value",
|
||||
def: "default",
|
||||
want: "value",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ZeroDefault(tt.value, tt.def)
|
||||
if got != tt.want {
|
||||
t.Errorf("ZeroDefault() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user