profile picture, birthday format and refresh token expiry fixes
This commit is contained in:
parent
839c8345a4
commit
7f1bf0e7f1
|
|
@ -216,6 +216,16 @@
|
|||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE devices (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
device_token TEXT NOT NULL,
|
||||
platform VARCHAR(20), -- web, android, ios
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
last_seen TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
|
|
|||
39
db/query/device.sql
Normal file
39
db/query/device.sql
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
-- name: CreateDevice :one
|
||||
INSERT INTO devices (
|
||||
user_id,
|
||||
device_token,
|
||||
platform
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3
|
||||
)
|
||||
ON CONFLICT (user_id, device_token)
|
||||
DO UPDATE SET
|
||||
is_active = true,
|
||||
last_seen = CURRENT_TIMESTAMP
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetUserDevices :many
|
||||
SELECT *
|
||||
FROM devices
|
||||
WHERE user_id = $1 AND is_active = true;
|
||||
|
||||
-- name: UpdateDeviceLastSeen :exec
|
||||
UPDATE devices
|
||||
SET last_seen = CURRENT_TIMESTAMP
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: DeactivateDevice :exec
|
||||
UPDATE devices
|
||||
SET is_active = false
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: DeactivateUserDevices :exec
|
||||
UPDATE devices
|
||||
SET is_active = false
|
||||
WHERE user_id = $1;
|
||||
|
||||
-- name: GetActiveDeviceTokens :many
|
||||
SELECT device_token
|
||||
FROM devices
|
||||
WHERE user_id = $1 AND is_active = true AND platform IN ('android', 'ios');
|
||||
57
docs/docs.go
57
docs/docs.go
|
|
@ -3078,6 +3078,62 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/user/{id}/profile-picture": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "Uploads a profile picture for the specified user",
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Upload profile picture",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"description": "Image file (jpg|png|webp)",
|
||||
"name": "file",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/user/{user_id}/is-pending": {
|
||||
"get": {
|
||||
"description": "Returns whether the specified user has a status of \"pending\"",
|
||||
|
|
@ -4162,6 +4218,7 @@ const docTemplate = `{
|
|||
"type": "string"
|
||||
},
|
||||
"birth_day": {
|
||||
"description": "formatted as YYYY-MM-DD",
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
|
|
|
|||
|
|
@ -3070,6 +3070,62 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/user/{id}/profile-picture": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "Uploads a profile picture for the specified user",
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Upload profile picture",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"description": "Image file (jpg|png|webp)",
|
||||
"name": "file",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/user/{user_id}/is-pending": {
|
||||
"get": {
|
||||
"description": "Returns whether the specified user has a status of \"pending\"",
|
||||
|
|
@ -4154,6 +4210,7 @@
|
|||
"type": "string"
|
||||
},
|
||||
"birth_day": {
|
||||
"description": "formatted as YYYY-MM-DD",
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ definitions:
|
|||
age_group:
|
||||
type: string
|
||||
birth_day:
|
||||
description: formatted as YYYY-MM-DD
|
||||
type: string
|
||||
country:
|
||||
type: string
|
||||
|
|
@ -2984,6 +2985,42 @@ paths:
|
|||
summary: Update user profile
|
||||
tags:
|
||||
- user
|
||||
/api/v1/user/{id}/profile-picture:
|
||||
post:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
description: Uploads a profile picture for the specified user
|
||||
parameters:
|
||||
- description: User ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
- description: Image file (jpg|png|webp)
|
||||
in: formData
|
||||
name: file
|
||||
required: true
|
||||
type: file
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Upload profile picture
|
||||
tags:
|
||||
- user
|
||||
/api/v1/user/{user_id}/is-pending:
|
||||
get:
|
||||
consumes:
|
||||
|
|
|
|||
142
gen/db/device.sql.go
Normal file
142
gen/db/device.sql.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: device.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const CreateDevice = `-- name: CreateDevice :one
|
||||
INSERT INTO devices (
|
||||
user_id,
|
||||
device_token,
|
||||
platform
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3
|
||||
)
|
||||
ON CONFLICT (user_id, device_token)
|
||||
DO UPDATE SET
|
||||
is_active = true,
|
||||
last_seen = CURRENT_TIMESTAMP
|
||||
RETURNING id, user_id, device_token, platform, is_active, last_seen, created_at
|
||||
`
|
||||
|
||||
type CreateDeviceParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
DeviceToken string `json:"device_token"`
|
||||
Platform pgtype.Text `json:"platform"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateDevice(ctx context.Context, arg CreateDeviceParams) (Device, error) {
|
||||
row := q.db.QueryRow(ctx, CreateDevice, arg.UserID, arg.DeviceToken, arg.Platform)
|
||||
var i Device
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.DeviceToken,
|
||||
&i.Platform,
|
||||
&i.IsActive,
|
||||
&i.LastSeen,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const DeactivateDevice = `-- name: DeactivateDevice :exec
|
||||
UPDATE devices
|
||||
SET is_active = false
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeactivateDevice(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, DeactivateDevice, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const DeactivateUserDevices = `-- name: DeactivateUserDevices :exec
|
||||
UPDATE devices
|
||||
SET is_active = false
|
||||
WHERE user_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeactivateUserDevices(ctx context.Context, userID int64) error {
|
||||
_, err := q.db.Exec(ctx, DeactivateUserDevices, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
const GetActiveDeviceTokens = `-- name: GetActiveDeviceTokens :many
|
||||
SELECT device_token
|
||||
FROM devices
|
||||
WHERE user_id = $1 AND is_active = true AND platform IN ('android', 'ios')
|
||||
`
|
||||
|
||||
func (q *Queries) GetActiveDeviceTokens(ctx context.Context, userID int64) ([]string, error) {
|
||||
rows, err := q.db.Query(ctx, GetActiveDeviceTokens, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []string
|
||||
for rows.Next() {
|
||||
var device_token string
|
||||
if err := rows.Scan(&device_token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, device_token)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetUserDevices = `-- name: GetUserDevices :many
|
||||
SELECT id, user_id, device_token, platform, is_active, last_seen, created_at
|
||||
FROM devices
|
||||
WHERE user_id = $1 AND is_active = true
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserDevices(ctx context.Context, userID int64) ([]Device, error) {
|
||||
rows, err := q.db.Query(ctx, GetUserDevices, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Device
|
||||
for rows.Next() {
|
||||
var i Device
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.DeviceToken,
|
||||
&i.Platform,
|
||||
&i.IsActive,
|
||||
&i.LastSeen,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const UpdateDeviceLastSeen = `-- name: UpdateDeviceLastSeen :exec
|
||||
UPDATE devices
|
||||
SET last_seen = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) UpdateDeviceLastSeen(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, UpdateDeviceLastSeen, id)
|
||||
return err
|
||||
}
|
||||
|
|
@ -86,6 +86,16 @@ type CourseCategory struct {
|
|||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
DeviceToken string `json:"device_token"`
|
||||
Platform pgtype.Text `json:"platform"`
|
||||
IsActive pgtype.Bool `json:"is_active"`
|
||||
LastSeen pgtype.Timestamptz `json:"last_seen"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type GlobalSetting struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
|
|
|
|||
31
go.mod
31
go.mod
|
|
@ -17,22 +17,53 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go v0.121.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/firestore v1.18.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.2 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
cloud.google.com/go/storage v1.53.0 // indirect
|
||||
firebase.google.com/go/v4 v4.19.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
|
||||
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/api v0.239.0 // indirect
|
||||
google.golang.org/appengine/v2 v2.0.6 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
|
|
|
|||
70
go.sum
70
go.sum
|
|
@ -1,10 +1,36 @@
|
|||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg=
|
||||
cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q=
|
||||
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s=
|
||||
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
|
||||
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
|
||||
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
|
||||
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
||||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
||||
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
|
||||
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
|
||||
cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHUAw=
|
||||
cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA=
|
||||
firebase.google.com/go/v4 v4.19.0 h1:f5NMlC2YHFsncz00c2+ecBr+ZYlRMhKIhj1z8Iz0lD8=
|
||||
firebase.google.com/go/v4 v4.19.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
|
||||
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
|
|
@ -26,16 +52,25 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
|||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
|
|
@ -62,13 +97,20 @@ github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2cc
|
|||
github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY=
|
||||
github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY=
|
||||
github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
|
|
@ -133,6 +175,8 @@ github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH
|
|||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/resend/resend-go/v2 v2.28.0 h1:ttM1/VZR4fApBv3xI1TneSKi1pbfFsVrq7fXFlHKtj4=
|
||||
|
|
@ -146,6 +190,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
|||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
|
@ -190,16 +236,26 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS
|
|||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
|
||||
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
|
||||
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
|
|
@ -265,6 +321,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
|
|
@ -275,11 +333,23 @@ golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
|||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
|
||||
google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
|
||||
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ type Config struct {
|
|||
TwilioSenderPhoneNumber string
|
||||
RedisAddr string
|
||||
KafkaBrokers []string
|
||||
FCMServiceAccountKey string
|
||||
}
|
||||
|
||||
func NewConfig() (*Config, error) {
|
||||
|
|
@ -511,6 +512,8 @@ func (c *Config) loadEnv() error {
|
|||
}
|
||||
c.TwilioSenderPhoneNumber = twilioSenderPhoneNumber
|
||||
|
||||
c.FCMServiceAccountKey = os.Getenv("FCM_SERVICE_ACCOUNT_KEY")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,20 @@ type ResponseWDataFactory[T any] struct {
|
|||
type Response struct {
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Success bool `json:"success" default:"true"`
|
||||
StatusCode int `json:"status_code" default:"200"`
|
||||
Success bool `json:"success"`
|
||||
StatusCode int `json:"status_code"`
|
||||
MetaData interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
func NewResponse(message string, data interface{}) Response {
|
||||
return Response{
|
||||
Message: message,
|
||||
Data: data,
|
||||
Success: true, // default true
|
||||
StatusCode: 200, // default 200
|
||||
}
|
||||
}
|
||||
|
||||
// type CallbackErrorResponse struct {
|
||||
// ErrorData interface
|
||||
// Error string `json:"error,omitempty"`
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ type UserProfileResponse struct {
|
|||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Gender string `json:"gender"`
|
||||
BirthDay time.Time `json:"birth_day"`
|
||||
BirthDay string `json:"birth_day,omitempty"` // formatted as YYYY-MM-DD
|
||||
// UserName string `json:"user_name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
PhoneNumber string `json:"phone_number,omitempty"`
|
||||
|
|
@ -113,7 +113,7 @@ type UserProfileResponse struct {
|
|||
|
||||
LastLogin *time.Time `json:"last_login,omitempty"`
|
||||
ProfileCompleted bool `json:"profile_completed"`
|
||||
ProfilePictureURL string `json:"profile_picture_url,omitempty"`
|
||||
ProfilePictureURL string `json:"profile_picture_url"`
|
||||
PreferredLanguage string `json:"preferred_language,omitempty"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ type UserStore interface {
|
|||
phone string,
|
||||
) (domain.User, error)
|
||||
UpdatePassword(ctx context.Context, password string, userID int64) error
|
||||
RegisterDevice(ctx context.Context, userID int64, deviceToken, platform string) error
|
||||
GetUserDeviceTokens(ctx context.Context, userID int64) ([]string, error)
|
||||
}
|
||||
type SmsGateway interface {
|
||||
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
|
||||
|
|
|
|||
|
|
@ -21,7 +21,14 @@ func NewTokenStore(s *Store) ports.TokenStore {
|
|||
|
||||
// CreateRefreshToken inserts a new refresh token into the database
|
||||
func (s *Store) CreateRefreshToken(ctx context.Context, rt domain.RefreshToken) error {
|
||||
// Use provided ExpiresAt and CreatedAt when available; otherwise fall back to sensible defaults.
|
||||
if rt.ExpiresAt.IsZero() {
|
||||
rt.ExpiresAt = time.Now().Add(10 * time.Minute)
|
||||
}
|
||||
createdAt := rt.CreatedAt
|
||||
if createdAt.IsZero() {
|
||||
createdAt = time.Now()
|
||||
}
|
||||
|
||||
return s.queries.CreateRefreshToken(ctx, dbgen.CreateRefreshTokenParams{
|
||||
UserID: rt.UserID,
|
||||
|
|
@ -31,7 +38,7 @@ func (s *Store) CreateRefreshToken(ctx context.Context, rt domain.RefreshToken)
|
|||
Valid: true,
|
||||
},
|
||||
CreatedAt: pgtype.Timestamptz{
|
||||
Time: time.Now(),
|
||||
Time: createdAt,
|
||||
Valid: true,
|
||||
},
|
||||
Revoked: rt.Revoked,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,19 @@ import (
|
|||
|
||||
func NewUserStore(s *Store) ports.UserStore { return s }
|
||||
|
||||
func (s *Store) GetUserDeviceTokens(ctx context.Context, userID int64) ([]string, error) {
|
||||
return s.queries.GetActiveDeviceTokens(ctx, userID)
|
||||
}
|
||||
|
||||
func (s *Store) RegisterDevice(ctx context.Context, userID int64, deviceToken, platform string) error {
|
||||
_, err := s.queries.CreateDevice(ctx, dbgen.CreateDeviceParams{
|
||||
UserID: userID,
|
||||
DeviceToken: deviceToken,
|
||||
Platform: pgtype.Text{String: platform},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) LinkGoogleAccount(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
|
|
@ -652,65 +665,6 @@ func (s *Store) CheckPhoneEmailExist(ctx context.Context, phone, email string) (
|
|||
return res.PhoneExists, res.EmailExists, nil
|
||||
}
|
||||
|
||||
// func (s *Store) GetUserByUserName(
|
||||
// ctx context.Context,
|
||||
// userName string,
|
||||
// ) (domain.User, error) {
|
||||
|
||||
// u, err := s.queries.GetUserByUserName(ctx, userName)
|
||||
// if err != nil {
|
||||
// if errors.Is(err, pgx.ErrNoRows) {
|
||||
// return domain.User{}, authentication.ErrUserNotFound
|
||||
// }
|
||||
// return domain.User{}, err
|
||||
// }
|
||||
|
||||
// var lastLogin *time.Time
|
||||
// if u.LastLogin.Valid {
|
||||
// lastLogin = &u.LastLogin.Time
|
||||
// }
|
||||
|
||||
// var updatedAt *time.Time
|
||||
// if u.UpdatedAt.Valid {
|
||||
// updatedAt = &u.UpdatedAt.Time
|
||||
// }
|
||||
|
||||
// return domain.User{
|
||||
// ID: u.ID,
|
||||
// FirstName: u.FirstName,
|
||||
// LastName: u.LastName,
|
||||
// UserName: u.UserName,
|
||||
// Email: u.Email.String,
|
||||
// PhoneNumber: u.PhoneNumber.String,
|
||||
// Password: u.Password,
|
||||
// Role: domain.Role(u.Role),
|
||||
|
||||
// Age: int(u.Age.Int32),
|
||||
// EducationLevel: u.EducationLevel.String,
|
||||
// Country: u.Country.String,
|
||||
// Region: u.Region.String,
|
||||
|
||||
// NickName: u.NickName.String,
|
||||
// Occupation: u.Occupation.String,
|
||||
// LearningGoal: u.LearningGoal.String,
|
||||
// LanguageGoal: u.LanguageGoal.String,
|
||||
// LanguageChallange: u.LanguageChallange.String,
|
||||
// FavouriteTopic: u.FavouriteTopic.String,
|
||||
|
||||
// EmailVerified: u.EmailVerified,
|
||||
// PhoneVerified: u.PhoneVerified,
|
||||
// Status: domain.UserStatus(u.Status),
|
||||
|
||||
// LastLogin: lastLogin,
|
||||
// ProfileCompleted: u.ProfileCompleted,
|
||||
// ProfilePictureURL: u.ProfilePictureUrl.String,
|
||||
// PreferredLanguage: u.PreferredLanguage.String,
|
||||
|
||||
// CreatedAt: u.CreatedAt.Time,
|
||||
// UpdatedAt: updatedAt,
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
// GetUserByEmail retrieves a user by email and organization
|
||||
func (s *Store) GetUserByEmailPhone(
|
||||
ctx context.Context,
|
||||
|
|
@ -869,6 +823,7 @@ func mapDBUser(
|
|||
PhoneVerified: userRes.PhoneVerified,
|
||||
Status: domain.UserStatus(userRes.Status),
|
||||
|
||||
ProfilePictureURL: userRes.ProfilePictureUrl.String,
|
||||
ProfileCompleted: userRes.ProfileCompleted.Bool,
|
||||
PreferredLanguage: userRes.PreferredLanguage.String,
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
// "errors"
|
||||
"log/slog"
|
||||
|
|
@ -23,6 +24,9 @@ import (
|
|||
// "github.com/segmentio/kafka-go"
|
||||
"go.uber.org/zap"
|
||||
// afro "github.com/amanuelabay/afrosms-go"
|
||||
firebase "firebase.google.com/go/v4"
|
||||
"firebase.google.com/go/v4/messaging"
|
||||
"google.golang.org/api/option"
|
||||
afro "github.com/amanuelabay/afrosms-go"
|
||||
"github.com/gorilla/websocket"
|
||||
// "github.com/redis/go-redis/v9"
|
||||
|
|
@ -39,6 +43,7 @@ type Service struct {
|
|||
messengerSvc *messenger.Service
|
||||
mongoLogger *zap.Logger
|
||||
logger *slog.Logger
|
||||
fcmClient *messaging.Client
|
||||
}
|
||||
|
||||
func New(
|
||||
|
|
@ -64,6 +69,13 @@ func New(
|
|||
config: cfg,
|
||||
}
|
||||
|
||||
// Initialize FCM client if service account key is provided
|
||||
if cfg.FCMServiceAccountKey != "" {
|
||||
if err := svc.initFCMClient(); err != nil {
|
||||
mongoLogger.Error("Failed to initialize FCM client", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
go hub.Run()
|
||||
go svc.startWorker()
|
||||
// go svc.startRetryWorker()
|
||||
|
|
@ -73,6 +85,31 @@ func New(
|
|||
return svc
|
||||
}
|
||||
|
||||
func (s *Service) initFCMClient() error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Prepare client options; if a service account JSON string is provided, use it.
|
||||
var opts []option.ClientOption
|
||||
if s.config.FCMServiceAccountKey != "" {
|
||||
opts = append(opts, option.WithCredentialsJSON([]byte(s.config.FCMServiceAccountKey)))
|
||||
}
|
||||
|
||||
// Initialize Firebase app
|
||||
app, err := firebase.NewApp(ctx, nil, opts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize Firebase app: %w", err)
|
||||
}
|
||||
|
||||
// Get messaging client
|
||||
client, err := app.Messaging(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get FCM client: %w", err)
|
||||
}
|
||||
|
||||
s.fcmClient = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) SendAfroMessageSMS(ctx context.Context, receiverPhone, message string) error {
|
||||
apiKey := s.config.AFRO_SMS_API_KEY
|
||||
senderName := s.config.AFRO_SMS_SENDER_NAME
|
||||
|
|
@ -411,6 +448,15 @@ func (s *Service) handleNotification(notification *domain.Notification) {
|
|||
} else {
|
||||
notification.DeliveryStatus = domain.DeliveryStatusSent
|
||||
}
|
||||
|
||||
case domain.DeliveryChannelPush:
|
||||
err := s.SendPushNotification(ctx, notification)
|
||||
if err != nil {
|
||||
notification.DeliveryStatus = domain.DeliveryStatusFailed
|
||||
} else {
|
||||
notification.DeliveryStatus = domain.DeliveryStatusSent
|
||||
}
|
||||
|
||||
default:
|
||||
if notification.DeliveryChannel != domain.DeliveryChannelInApp {
|
||||
s.mongoLogger.Warn("[NotificationSvc.HandleNotification] Unsupported delivery channel",
|
||||
|
|
@ -490,6 +536,64 @@ func (s *Service) SendNotificationEmail(ctx context.Context, recipientID int64,
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) SendPushNotification(ctx context.Context, notification *domain.Notification) error {
|
||||
if s.fcmClient == nil {
|
||||
return fmt.Errorf("FCM client not initialized")
|
||||
}
|
||||
|
||||
// Get user device tokens
|
||||
tokens, err := s.userSvc.GetUserDeviceTokens(ctx, notification.RecipientID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user device tokens: %w", err)
|
||||
}
|
||||
|
||||
if len(tokens) == 0 {
|
||||
s.mongoLogger.Info("[NotificationSvc.SendPushNotification] No device tokens found for user",
|
||||
zap.Int64("userID", notification.RecipientID),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return nil // Not an error, just no devices to send to
|
||||
}
|
||||
|
||||
// Create FCM message
|
||||
message := &messaging.Message{
|
||||
Notification: &messaging.Notification{
|
||||
Title: notification.Payload.Headline,
|
||||
Body: notification.Payload.Message,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"type": string(notification.Type),
|
||||
"recipient_id": strconv.FormatInt(notification.RecipientID, 10),
|
||||
"notification_id": notification.ID,
|
||||
},
|
||||
}
|
||||
|
||||
// Send to all user devices
|
||||
for _, token := range tokens {
|
||||
message.Token = token
|
||||
|
||||
response, err := s.fcmClient.Send(ctx, message)
|
||||
if err != nil {
|
||||
s.mongoLogger.Error("[NotificationSvc.SendPushNotification] Failed to send FCM message",
|
||||
zap.String("token", token),
|
||||
zap.Int64("userID", notification.RecipientID),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
continue // Continue with other tokens
|
||||
}
|
||||
|
||||
s.mongoLogger.Info("[NotificationSvc.SendPushNotification] FCM message sent successfully",
|
||||
zap.String("response", response),
|
||||
zap.String("token", token),
|
||||
zap.Int64("userID", notification.RecipientID),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// func (s *Service) startRetryWorker() {
|
||||
// ticker := time.NewTicker(1 * time.Minute)
|
||||
// defer ticker.Stop()
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ type UserStore interface {
|
|||
GetCustomerDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerDetail, error)
|
||||
GetBranchCustomerCounts(ctx context.Context, filter domain.ReportFilter) (map[int64]int64, error)
|
||||
GetRoleCounts(ctx context.Context, role string, filter domain.ReportFilter) (total, active, inactive int64, err error)
|
||||
RegisterDevice(ctx context.Context, userID int64, deviceToken, platform string) error
|
||||
GetUserDeviceTokens(ctx context.Context, userID int64) ([]string, error)
|
||||
}
|
||||
type SmsGateway interface {
|
||||
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ func (s *Service) IsProfileCompleted(ctx context.Context, userId int64) (bool, e
|
|||
return s.userStore.IsProfileCompleted(ctx, userId)
|
||||
}
|
||||
|
||||
// func (s *Service) IsUserNameUnique(ctx context.Context, userID int64) (bool, error) {
|
||||
// return s.userStore.IsUserNameUnique(ctx, userID)
|
||||
// }
|
||||
|
||||
// func (s *Service) GetUserByUserName(
|
||||
// ctx context.Context,
|
||||
// userName string,
|
||||
// ) (domain.User, error) {
|
||||
// return s.userStore.GetUserByUserName(ctx, userName)
|
||||
// }
|
||||
|
||||
func (s *Service) SearchUserByNameOrPhone(ctx context.Context, searchString string, role *int64) ([]domain.User, error) {
|
||||
// Search user
|
||||
var roleStr *string
|
||||
|
|
@ -49,10 +38,6 @@ func (s *Service) UpdateUser(ctx context.Context, req domain.UpdateUserReq) erro
|
|||
return s.userStore.UpdateUser(ctx, req)
|
||||
}
|
||||
|
||||
// func (s *Service) UpdateUserSuspend(ctx context.Context, id int64, status bool) error {
|
||||
// // update user
|
||||
// return s.userStore.UpdateUserSuspend(ctx, id, status)
|
||||
// }
|
||||
func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
|
||||
return s.userStore.GetUserByID(ctx, id)
|
||||
}
|
||||
|
|
@ -92,4 +77,12 @@ func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error
|
|||
// }
|
||||
|
||||
// return s.userStore.GetAllUsers(ctx, role, query, createdBefore, createdAfter, limit, offset)
|
||||
// }
|
||||
|
||||
// Device management methods
|
||||
func (s *Service) RegisterDevice(ctx context.Context, userID int64, deviceToken, platform string) error {
|
||||
return s.userStore.RegisterDevice(ctx, userID, deviceToken, platform)
|
||||
}
|
||||
|
||||
func (s *Service) GetUserDeviceTokens(ctx context.Context, userID int64) ([]string, error) {
|
||||
return s.userStore.GetUserDeviceTokens(ctx, userID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -523,3 +523,52 @@ func (h *Handler) SendSingleAfroSMS(c *fiber.Ctx) error {
|
|||
Data: req,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterDeviceToken(c *fiber.Ctx) error {
|
||||
type Request struct {
|
||||
DeviceToken string `json:"device_token" validate:"required"`
|
||||
Platform string `json:"platform" validate:"required,oneof=android ios web"`
|
||||
}
|
||||
|
||||
var req Request
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
h.mongoLoggerSvc.Info("[NotificationHandler.RegisterDeviceToken] Failed to parse request body",
|
||||
zap.Int("status_code", fiber.StatusBadRequest),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
userID, ok := c.Locals("user_id").(int64)
|
||||
if !ok || userID == 0 {
|
||||
h.mongoLoggerSvc.Error("[NotificationHandler.RegisterDeviceToken] Invalid user ID in context",
|
||||
zap.Int("status_code", fiber.StatusUnauthorized),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
|
||||
}
|
||||
|
||||
if err := h.userSvc.RegisterDevice(c.Context(), userID, req.DeviceToken, req.Platform); err != nil {
|
||||
h.mongoLoggerSvc.Error("[NotificationHandler.RegisterDeviceToken] Failed to register device token",
|
||||
zap.Int64("userID", userID),
|
||||
zap.String("platform", req.Platform),
|
||||
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to register device token: "+err.Error())
|
||||
}
|
||||
|
||||
h.mongoLoggerSvc.Info("[NotificationHandler.RegisterDeviceToken] Device token registered successfully",
|
||||
zap.Int64("userID", userID),
|
||||
zap.String("platform", req.Platform),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Device token registered successfully",
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusCreated,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,15 @@ import (
|
|||
"Yimaru-Backend/internal/web_server/response"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
|
@ -484,7 +489,46 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
|
|||
// }
|
||||
// }
|
||||
|
||||
return response.WriteJSON(c, fiber.StatusOK, "Users fetched successfully", map[string]interface{}{"users": users, "total": total}, nil)
|
||||
// Map domain.User -> domain.UserProfileResponse to control fields and formatting
|
||||
mapped := make([]domain.UserProfileResponse, 0, len(users))
|
||||
for _, u := range users {
|
||||
var bd string
|
||||
if !u.BirthDay.IsZero() {
|
||||
bd = u.BirthDay.Format("2006-01-02")
|
||||
}
|
||||
mapped = append(mapped, domain.UserProfileResponse{
|
||||
ID: u.ID,
|
||||
FirstName: u.FirstName,
|
||||
LastName: u.LastName,
|
||||
Gender: u.Gender,
|
||||
BirthDay: bd,
|
||||
Email: u.Email,
|
||||
PhoneNumber: u.PhoneNumber,
|
||||
Role: u.Role,
|
||||
AgeGroup: u.AgeGroup,
|
||||
EducationLevel: u.EducationLevel,
|
||||
Country: u.Country,
|
||||
Region: u.Region,
|
||||
InitialAssessmentCompleted: u.InitialAssessmentCompleted,
|
||||
NickName: u.NickName,
|
||||
Occupation: u.Occupation,
|
||||
LearningGoal: u.LearningGoal,
|
||||
LanguageGoal: u.LanguageGoal,
|
||||
LanguageChallange: u.LanguageChallange,
|
||||
FavouriteTopic: u.FavouriteTopic,
|
||||
EmailVerified: u.EmailVerified,
|
||||
PhoneVerified: u.PhoneVerified,
|
||||
Status: u.Status,
|
||||
LastLogin: u.LastLogin,
|
||||
ProfileCompleted: u.ProfileCompleted,
|
||||
ProfilePictureURL: u.ProfilePictureURL,
|
||||
PreferredLanguage: u.PreferredLanguage,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return response.WriteJSON(c, fiber.StatusOK, "Users fetched successfully", map[string]interface{}{"users": mapped, "total": total}, nil)
|
||||
}
|
||||
|
||||
// VerifyOtp godoc
|
||||
|
|
@ -1241,6 +1285,12 @@ func (h *Handler) GetUserProfile(c *fiber.Ctx) error {
|
|||
PhoneNumber: user.PhoneNumber,
|
||||
Role: user.Role,
|
||||
AgeGroup: user.AgeGroup,
|
||||
BirthDay: func() string {
|
||||
if !user.BirthDay.IsZero() {
|
||||
return user.BirthDay.Format("2006-01-02")
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
EducationLevel: user.EducationLevel,
|
||||
Country: user.Country,
|
||||
Region: user.Region,
|
||||
|
|
@ -1343,6 +1393,10 @@ func (h *Handler) AdminProfile(c *fiber.Ctx) error {
|
|||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
// Ensure birthday is included and formatted
|
||||
if !user.BirthDay.IsZero() {
|
||||
res.BirthDay = user.BirthDay.Format("2006-01-02")
|
||||
}
|
||||
|
||||
return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil)
|
||||
}
|
||||
|
|
@ -1449,6 +1503,12 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
|
|||
LastName: user.LastName,
|
||||
// UserName: user.UserName,
|
||||
Email: user.Email,
|
||||
BirthDay: func() string {
|
||||
if !user.BirthDay.IsZero() {
|
||||
return user.BirthDay.Format("2006-01-02")
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Role: user.Role,
|
||||
AgeGroup: user.AgeGroup,
|
||||
|
|
@ -1529,6 +1589,12 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error {
|
|||
ID: user.ID,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
BirthDay: func() string {
|
||||
if !user.BirthDay.IsZero() {
|
||||
return user.BirthDay.Format("2006-01-02")
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
LearningGoal: user.LearningGoal,
|
||||
LanguageGoal: user.LanguageGoal,
|
||||
LanguageChallange: user.LanguageChallange,
|
||||
|
|
@ -1605,3 +1671,133 @@ type UpdateUserSuspendRes struct {
|
|||
UserID int64 `json:"user_id"`
|
||||
Suspended bool `json:"suspended"`
|
||||
}
|
||||
|
||||
// UploadProfilePicture godoc
|
||||
// @Summary Upload profile picture
|
||||
// @Description Uploads a profile picture for the specified user
|
||||
// @Tags user
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path int true "User ID"
|
||||
// @Param file formData file true "Image file (jpg|png|webp)"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Security Bearer
|
||||
// @Router /api/v1/user/{id}/profile-picture [post]
|
||||
func (h *Handler) UploadProfilePicture(c *fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
userID, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil || userID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid user id",
|
||||
Error: "User id must be a positive integer",
|
||||
})
|
||||
}
|
||||
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid file",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
const maxSize = 5 * 1024 * 1024 // 5 MB
|
||||
if fileHeader.Size > maxSize {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "File too large",
|
||||
Error: "Profile picture must be <= 5MB",
|
||||
})
|
||||
}
|
||||
|
||||
fh, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to read file",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
// Read up to first 512 bytes for content sniffing, then read the rest
|
||||
head := make([]byte, 512)
|
||||
n, _ := fh.Read(head)
|
||||
contentType := http.DetectContentType(head[:n])
|
||||
if contentType != "image/jpeg" && contentType != "image/png" && contentType != "image/webp" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid file type",
|
||||
Error: "Only jpg, png and webp images are allowed",
|
||||
})
|
||||
}
|
||||
|
||||
// Read remaining bytes
|
||||
rest, err := io.ReadAll(fh)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to read file",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Combine head + rest
|
||||
data := append(head[:n], rest...)
|
||||
|
||||
ext := ".jpg"
|
||||
switch contentType {
|
||||
case "image/png":
|
||||
ext = ".png"
|
||||
case "image/webp":
|
||||
ext = ".webp"
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
dir := filepath.Join(".", "static", "profile_pictures")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to create storage directory",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
filename := uuid.New().String() + ext
|
||||
fullpath := filepath.Join(dir, filename)
|
||||
|
||||
if err := os.WriteFile(fullpath, data, 0o644); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to save file",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
publicPath := "/static/profile_pictures/" + filename
|
||||
|
||||
// Update user profile picture URL
|
||||
req := domain.UpdateUserReq{
|
||||
UserID: userID,
|
||||
ProfilePictureURL: publicPath,
|
||||
}
|
||||
|
||||
if err := h.userSvc.UpdateUser(c.Context(), req); err != nil {
|
||||
// Attempt to remove file on failure
|
||||
_ = os.Remove(fullpath)
|
||||
h.mongoLoggerSvc.Error("Failed to update user with profile picture",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update user",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Profile picture uploaded successfully",
|
||||
Data: map[string]string{"profile_picture_url": publicPath},
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
|
||||
// return response.WriteJSON(c, fiber.StatusOK, "Profile picture uploaded successfully", map[string]string{"profile_picture_url": publicPath}, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ func (a *App) initAppRoutes() {
|
|||
// })
|
||||
// })
|
||||
|
||||
// Serve static files (profile pictures, etc.)
|
||||
a.fiber.Static("/static", "./static")
|
||||
|
||||
// Get S
|
||||
groupV1.Get("/tenant", a.authMiddleware, h.GetTenantSlugByToken)
|
||||
|
||||
|
|
@ -196,6 +199,8 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Post("/auth/admin-login", h.LoginAdmin)
|
||||
groupV1.Post("/auth/super-login", h.LoginSuper)
|
||||
groupV1.Post("/auth/refresh", h.RefreshToken)
|
||||
// Upload profile picture
|
||||
groupV1.Post("/user/:id/profile-picture", a.authMiddleware, h.UploadProfilePicture)
|
||||
groupV1.Post("/auth/logout", a.authMiddleware, h.LogOutuser)
|
||||
groupV1.Get("/auth/test", a.authMiddleware, func(c *fiber.Ctx) error {
|
||||
userID, ok := c.Locals("user_id").(int64)
|
||||
|
|
@ -293,6 +298,9 @@ func (a *App) initAppRoutes() {
|
|||
// groupV1.Patch("/issues/:issue_id/status", a.authMiddleware, a.OnlyAdminAndAbove, h.UpdateIssueStatus)
|
||||
// groupV1.Delete("/issues/:issue_id", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteIssue)
|
||||
|
||||
// Device Token Registration
|
||||
groupV1.Post("/devices/register", a.authMiddleware, h.RegisterDeviceToken)
|
||||
|
||||
// Settings
|
||||
groupV1.Get("/settings", a.authMiddleware, a.SuperAdminOnly, h.GetGlobalSettingList)
|
||||
groupV1.Get("/settings/:key", a.authMiddleware, a.SuperAdminOnly, h.GetGlobalSettingByKey)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user