model_driven_api 3.6.4 → 3.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +311 -0
- data/app/controllers/api/v2/application_controller.rb +18 -2
- data/app/controllers/api/v3/application_controller.rb +1 -1
- data/app/models/endpoints/push_subscriber.rb +110 -0
- data/lib/concerns/api_exception_management.rb +4 -1
- data/lib/model_driven_api/version.rb +1 -1
- data/lib/non_crud_endpoints.rb +18 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ad2abbdb4e4fd783d5e4fadde50593be43e265aa857a0077655513b4e73f522e
|
|
4
|
+
data.tar.gz: 323ffab731877ae6c4f8adb6b75bb358e12319a7b2d55ccc77aadd5e6e52d41d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 17322346aef3972a55db2fce8703abfe9b2148076509c3a14aa465c0d34e4d5ec41294366496a77fc7dc44c5dd324fe9513f77961338591ae035ac5d03e8575d
|
|
7
|
+
data.tar.gz: f11a3a215784201aa30fb20b45b29a1df75ef8d1e8f045d2d15c6b8f45425487d40a9ba6315db679a827dabd2e54846ef78ee9950f5533861befb6342d59b150
|
data/README.md
CHANGED
|
@@ -528,6 +528,317 @@ fetch(`/api/v2/products/${id}`, { method: 'PATCH', body: formData });
|
|
|
528
528
|
|
|
529
529
|
---
|
|
530
530
|
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Web Push (VAPID) from a React client
|
|
534
|
+
|
|
535
|
+
This section is the complete integration guide for React apps that want to receive browser push notifications from a Thecore backend. The server-side setup (models, service, ActionCable channel) is documented in the [`thecore_backend_commons` README](../thecore_backend_commons/README.md#web-push-notifications-vapid).
|
|
536
|
+
|
|
537
|
+
### Prerequisites
|
|
538
|
+
|
|
539
|
+
1. Run `rails db:seed` on the backend — this generates the VAPID key pair automatically.
|
|
540
|
+
2. Set `vapid.contact_email` in RailsAdmin → Settings (e.g. `admin@yourapp.com`).
|
|
541
|
+
3. Your site must be served over **HTTPS** (required by the Push API in all browsers). `localhost` is exempt for development.
|
|
542
|
+
|
|
543
|
+
### Endpoint reference
|
|
544
|
+
|
|
545
|
+
All endpoints live under `/api/v2/push_subscribers/custom_action/`.
|
|
546
|
+
|
|
547
|
+
| Method | Path | Auth | Description |
|
|
548
|
+
|--------|------|------|-------------|
|
|
549
|
+
| `GET` | `vapid_public_key` | **No** | Returns the VAPID public key for `PushManager.subscribe` |
|
|
550
|
+
| `POST` | `subscribe` | Yes (JWT) | Registers or updates a push subscription for the current user |
|
|
551
|
+
| `POST` | `send_push` | Yes (JWT) | Creates a `PushMessage` and dispatches it to an active subscriber |
|
|
552
|
+
| `POST` | `acknowledge` | Yes (JWT) | Marks a message as `received` and/or `read` |
|
|
553
|
+
|
|
554
|
+
### Step 1 — Register a service worker
|
|
555
|
+
|
|
556
|
+
Create `public/sw.js` in your React app:
|
|
557
|
+
|
|
558
|
+
```javascript
|
|
559
|
+
// public/sw.js
|
|
560
|
+
|
|
561
|
+
self.addEventListener('push', event => {
|
|
562
|
+
const data = event.data?.json() ?? {};
|
|
563
|
+
event.waitUntil(
|
|
564
|
+
self.registration.showNotification(data.title ?? 'Notification', {
|
|
565
|
+
body: data.body,
|
|
566
|
+
icon: data.icon ?? '/favicon.ico',
|
|
567
|
+
data: { url: data.url },
|
|
568
|
+
})
|
|
569
|
+
);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
self.addEventListener('notificationclick', event => {
|
|
573
|
+
event.notification.close();
|
|
574
|
+
const url = event.notification.data?.url;
|
|
575
|
+
if (url) {
|
|
576
|
+
event.waitUntil(clients.openWindow(url));
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Step 2 — Subscribe to push notifications
|
|
582
|
+
|
|
583
|
+
```javascript
|
|
584
|
+
// src/usePushSubscription.js
|
|
585
|
+
const API_BASE = process.env.REACT_APP_API_URL ?? '/api/v2';
|
|
586
|
+
|
|
587
|
+
// Convert a base64url string to a Uint8Array (required by PushManager)
|
|
588
|
+
function urlBase64ToUint8Array(base64String) {
|
|
589
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
590
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
591
|
+
const raw = atob(base64);
|
|
592
|
+
return Uint8Array.from([...raw].map(c => c.charCodeAt(0)));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Convert a PushSubscription key to a base64url string (required by the backend)
|
|
596
|
+
function keyToBase64(subscription, key) {
|
|
597
|
+
return btoa(String.fromCharCode(...new Uint8Array(subscription.getKey(key))));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export async function subscribeToPush(jwtToken) {
|
|
601
|
+
// 1. Register service worker
|
|
602
|
+
const registration = await navigator.serviceWorker.register('/sw.js');
|
|
603
|
+
|
|
604
|
+
// 2. Fetch the VAPID public key (no auth needed)
|
|
605
|
+
const res = await fetch(`${API_BASE}/push_subscribers/custom_action/vapid_public_key`);
|
|
606
|
+
const { vapid_public_key } = await res.json();
|
|
607
|
+
|
|
608
|
+
// 3. Subscribe via the Push API
|
|
609
|
+
const subscription = await registration.pushManager.subscribe({
|
|
610
|
+
userVisibleOnly: true,
|
|
611
|
+
applicationServerKey: urlBase64ToUint8Array(vapid_public_key),
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// 4. Register the subscription with the backend
|
|
615
|
+
const registerRes = await fetch(
|
|
616
|
+
`${API_BASE}/push_subscribers/custom_action/subscribe`,
|
|
617
|
+
{
|
|
618
|
+
method: 'POST',
|
|
619
|
+
headers: {
|
|
620
|
+
'Content-Type': 'application/json',
|
|
621
|
+
Authorization: `Bearer ${jwtToken}`,
|
|
622
|
+
},
|
|
623
|
+
body: JSON.stringify({
|
|
624
|
+
endpoint: subscription.endpoint,
|
|
625
|
+
p256dh: keyToBase64(subscription, 'p256dh'),
|
|
626
|
+
auth: keyToBase64(subscription, 'auth'),
|
|
627
|
+
user_agent: navigator.userAgent,
|
|
628
|
+
}),
|
|
629
|
+
}
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
const subscriber = await registerRes.json();
|
|
633
|
+
// subscriber.id is the push_subscriber_id to use with ActionCable and send_push
|
|
634
|
+
return subscriber;
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
Call this after the user logs in (a JWT is required for `subscribe`):
|
|
639
|
+
|
|
640
|
+
```javascript
|
|
641
|
+
import { subscribeToPush } from './usePushSubscription';
|
|
642
|
+
|
|
643
|
+
const subscriber = await subscribeToPush(jwtToken);
|
|
644
|
+
localStorage.setItem('push_subscriber_id', subscriber.id);
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Step 3 — Listen via ActionCable
|
|
648
|
+
|
|
649
|
+
Install the ActionCable client if you haven't already:
|
|
650
|
+
|
|
651
|
+
```bash
|
|
652
|
+
npm install @rails/actioncable
|
|
653
|
+
# or
|
|
654
|
+
yarn add @rails/actioncable
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
```javascript
|
|
658
|
+
// src/usePushChannel.js
|
|
659
|
+
import { createConsumer } from '@rails/actioncable';
|
|
660
|
+
|
|
661
|
+
const WS_URL = process.env.REACT_APP_WS_URL ?? 'ws://localhost:3000/cable';
|
|
662
|
+
|
|
663
|
+
export function connectPushChannel(jwtToken, subscriberId, onMessage) {
|
|
664
|
+
// The JWT token is passed as a query parameter so the ActionCable
|
|
665
|
+
// connection.rb can authenticate the WebSocket handshake.
|
|
666
|
+
const consumer = createConsumer(`${WS_URL}?token=${jwtToken}`);
|
|
667
|
+
|
|
668
|
+
const subscription = consumer.subscriptions.create(
|
|
669
|
+
{ channel: 'PushNotificationChannel', subscriber_id: subscriberId },
|
|
670
|
+
{
|
|
671
|
+
received(data) {
|
|
672
|
+
// data is a PushMessage serialised as JSON:
|
|
673
|
+
// { id, title, body, url, icon, sent_at, received_at, read_at }
|
|
674
|
+
onMessage(data);
|
|
675
|
+
},
|
|
676
|
+
connected() {
|
|
677
|
+
console.log('[PushNotificationChannel] connected');
|
|
678
|
+
},
|
|
679
|
+
disconnected() {
|
|
680
|
+
console.log('[PushNotificationChannel] disconnected');
|
|
681
|
+
},
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
// Return a cleanup function
|
|
686
|
+
return () => {
|
|
687
|
+
subscription.unsubscribe();
|
|
688
|
+
consumer.disconnect();
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Use it in a React component or hook:
|
|
694
|
+
|
|
695
|
+
```javascript
|
|
696
|
+
import { useEffect } from 'react';
|
|
697
|
+
import { connectPushChannel } from './usePushChannel';
|
|
698
|
+
|
|
699
|
+
function App() {
|
|
700
|
+
const jwtToken = localStorage.getItem('token');
|
|
701
|
+
const subscriberId = localStorage.getItem('push_subscriber_id');
|
|
702
|
+
|
|
703
|
+
useEffect(() => {
|
|
704
|
+
if (!jwtToken || !subscriberId) return;
|
|
705
|
+
|
|
706
|
+
const disconnect = connectPushChannel(jwtToken, subscriberId, message => {
|
|
707
|
+
console.log('New push message via ActionCable:', message);
|
|
708
|
+
// Optionally acknowledge receipt immediately
|
|
709
|
+
acknowledgeMessage(jwtToken, message.id, { received: true });
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
return disconnect; // cleanup on unmount
|
|
713
|
+
}, [jwtToken, subscriberId]);
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
> **Tip:** use `user_id` instead of `subscriber_id` if you want to receive messages across all active subscriptions for the current user (e.g. multiple tabs):
|
|
718
|
+
> ```javascript
|
|
719
|
+
> { channel: 'PushNotificationChannel', user_id: currentUserId }
|
|
720
|
+
> ```
|
|
721
|
+
|
|
722
|
+
### Step 4 — Acknowledge receipt and read
|
|
723
|
+
|
|
724
|
+
```javascript
|
|
725
|
+
// src/pushApi.js
|
|
726
|
+
const API_BASE = process.env.REACT_APP_API_URL ?? '/api/v2';
|
|
727
|
+
|
|
728
|
+
export async function acknowledgeMessage(jwtToken, messageId, { received = false, read = false } = {}) {
|
|
729
|
+
const res = await fetch(
|
|
730
|
+
`${API_BASE}/push_subscribers/custom_action/acknowledge`,
|
|
731
|
+
{
|
|
732
|
+
method: 'POST',
|
|
733
|
+
headers: {
|
|
734
|
+
'Content-Type': 'application/json',
|
|
735
|
+
Authorization: `Bearer ${jwtToken}`,
|
|
736
|
+
},
|
|
737
|
+
body: JSON.stringify({
|
|
738
|
+
push_message_id: messageId,
|
|
739
|
+
received,
|
|
740
|
+
read,
|
|
741
|
+
}),
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
return res.json();
|
|
745
|
+
// Response: { id, title, body, url, icon, sent_at, received_at, read_at, ... }
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
Call `received: true` as soon as the message arrives via ActionCable. Call `read: true` when the user opens or dismisses it. Fields are set only once — a second call with the same flag is a no-op (idempotent).
|
|
750
|
+
|
|
751
|
+
### Step 5 — Send a push from the backend (optional)
|
|
752
|
+
|
|
753
|
+
Normally the backend triggers pushes from jobs or model callbacks. If you need to trigger a push from a privileged frontend (e.g. an admin panel), use `send_push`:
|
|
754
|
+
|
|
755
|
+
```javascript
|
|
756
|
+
export async function sendPush(jwtToken, { subscriberId, title, body, url, icon }) {
|
|
757
|
+
const res = await fetch(
|
|
758
|
+
`${API_BASE}/push_subscribers/custom_action/send_push`,
|
|
759
|
+
{
|
|
760
|
+
method: 'POST',
|
|
761
|
+
headers: {
|
|
762
|
+
'Content-Type': 'application/json',
|
|
763
|
+
Authorization: `Bearer ${jwtToken}`,
|
|
764
|
+
},
|
|
765
|
+
body: JSON.stringify({
|
|
766
|
+
push_subscriber_id: subscriberId,
|
|
767
|
+
title,
|
|
768
|
+
body,
|
|
769
|
+
url, // optional
|
|
770
|
+
icon, // optional
|
|
771
|
+
}),
|
|
772
|
+
}
|
|
773
|
+
);
|
|
774
|
+
if (!res.ok) {
|
|
775
|
+
const err = await res.json();
|
|
776
|
+
throw new Error(err.error ?? `HTTP ${res.status}`);
|
|
777
|
+
}
|
|
778
|
+
return res.json(); // PushMessage record
|
|
779
|
+
}
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
### Full flow summary
|
|
783
|
+
|
|
784
|
+
```
|
|
785
|
+
React app Thecore backend
|
|
786
|
+
│ │
|
|
787
|
+
│── GET vapid_public_key ──────────►│ (no auth)
|
|
788
|
+
│◄── { vapid_public_key: "..." } ───│
|
|
789
|
+
│ │
|
|
790
|
+
│ navigator.serviceWorker.register('/sw.js')
|
|
791
|
+
│ registration.pushManager.subscribe({ applicationServerKey })
|
|
792
|
+
│ │
|
|
793
|
+
│── POST subscribe ────────────────►│ creates/updates PushSubscriber
|
|
794
|
+
│◄── { id: 42, endpoint, ... } ─────│
|
|
795
|
+
│ │
|
|
796
|
+
│ createConsumer(wsUrl?token=jwt) │
|
|
797
|
+
│── WS upgrade ────────────────────►│ PushNotificationChannel#subscribed
|
|
798
|
+
│◄── stream: push_notifications_subscriber_42
|
|
799
|
+
│ │
|
|
800
|
+
│ [backend dispatches push] │
|
|
801
|
+
│◄── Web Push payload (sw.js) ──────│ Webpush.payload_send via VAPID
|
|
802
|
+
│ showNotification(title, body) │
|
|
803
|
+
│ │
|
|
804
|
+
│◄── ActionCable data ──────────────│ PushNotificationChannel.broadcast_to
|
|
805
|
+
│ onMessage(data) │
|
|
806
|
+
│ │
|
|
807
|
+
│── POST acknowledge (received) ───►│ message.received_at = now
|
|
808
|
+
│── POST acknowledge (read) ────────►│ message.read_at = now
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### Handling subscription expiry
|
|
812
|
+
|
|
813
|
+
Push service endpoints expire (the push provider returns HTTP 410). The backend automatically calls `subscriber.expire!` when this happens, but the client needs to re-subscribe on the next boot:
|
|
814
|
+
|
|
815
|
+
```javascript
|
|
816
|
+
export async function ensureSubscription(jwtToken) {
|
|
817
|
+
// Re-subscribe unconditionally — subscribe_for upserts on endpoint,
|
|
818
|
+
// so re-registering the same browser is always safe and clears expired_at.
|
|
819
|
+
const sub = await subscribeToPush(jwtToken);
|
|
820
|
+
localStorage.setItem('push_subscriber_id', sub.id);
|
|
821
|
+
return sub;
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
### Permissions check
|
|
826
|
+
|
|
827
|
+
Before calling `subscribeToPush`, check that the browser supports push and the user has granted permission:
|
|
828
|
+
|
|
829
|
+
```javascript
|
|
830
|
+
export async function requestPushPermission() {
|
|
831
|
+
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
|
832
|
+
console.warn('Web Push not supported in this browser');
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
const permission = await Notification.requestPermission();
|
|
836
|
+
return permission === 'granted';
|
|
837
|
+
}
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
---
|
|
841
|
+
|
|
531
842
|
## License
|
|
532
843
|
|
|
533
844
|
MIT
|
|
@@ -13,7 +13,7 @@ class Api::V2::ApplicationController < ActionController::API
|
|
|
13
13
|
|
|
14
14
|
# GET :controller/
|
|
15
15
|
def index
|
|
16
|
-
authorize! :index, @model
|
|
16
|
+
authorize! :index, @model unless public_custom_action?
|
|
17
17
|
|
|
18
18
|
# Custom Action
|
|
19
19
|
status, result, status_number = check_for_custom_action
|
|
@@ -67,7 +67,7 @@ class Api::V2::ApplicationController < ActionController::API
|
|
|
67
67
|
def create
|
|
68
68
|
# Normal Create Action
|
|
69
69
|
Rails.logger.debug("Creating a new record #{@record}")
|
|
70
|
-
authorize! :create, @record.presence || @model
|
|
70
|
+
authorize! :create, @record.presence || @model unless public_custom_action?
|
|
71
71
|
# Custom Action
|
|
72
72
|
status, result, status_number = check_for_custom_action
|
|
73
73
|
return render json: result, status: (status_number.presence || 200) if status == true
|
|
@@ -127,6 +127,19 @@ class Api::V2::ApplicationController < ActionController::API
|
|
|
127
127
|
|
|
128
128
|
private
|
|
129
129
|
|
|
130
|
+
# Returns true if the current request is for a NonCrudEndpoints custom action
|
|
131
|
+
# that has been declared as public (no authentication required).
|
|
132
|
+
# Forces autoloading of the Endpoints::<Model> class so the public_action_registry
|
|
133
|
+
# is populated before authenticate_request checks it.
|
|
134
|
+
def public_custom_action?
|
|
135
|
+
return false unless request.url.include?("/custom_action/")
|
|
136
|
+
model_name = params[:ctrl].to_s.classify
|
|
137
|
+
action_name = params[:action_name].to_s
|
|
138
|
+
# Ensure the endpoint class is loaded so its public_action declarations are registered.
|
|
139
|
+
("Endpoints::#{model_name}".constantize rescue nil)
|
|
140
|
+
NonCrudEndpoints.public_action?(model_name, action_name)
|
|
141
|
+
end
|
|
142
|
+
|
|
130
143
|
## CUSTOM ACTION
|
|
131
144
|
# [GET|PUT|POST|DELETE] :controller?do=:custom_action
|
|
132
145
|
# or
|
|
@@ -159,6 +172,9 @@ class Api::V2::ApplicationController < ActionController::API
|
|
|
159
172
|
end
|
|
160
173
|
|
|
161
174
|
def authenticate_request
|
|
175
|
+
# Skip auth for public NonCrudEndpoints actions (e.g. vapid_public_key).
|
|
176
|
+
return if public_custom_action?
|
|
177
|
+
|
|
162
178
|
@current_user = nil
|
|
163
179
|
Settings.ns(:security).allowed_authorization_headers.split(",").each do |header|
|
|
164
180
|
# puts "Found header #{header}: #{request.headers[header]}"
|
|
@@ -2,7 +2,7 @@ class Api::V3::ApplicationController < Api::V2::ApplicationController
|
|
|
2
2
|
include Pagy::Backend
|
|
3
3
|
|
|
4
4
|
def index
|
|
5
|
-
authorize! :index, @model
|
|
5
|
+
authorize! :index, @model unless public_custom_action?
|
|
6
6
|
|
|
7
7
|
status, result, status_number = check_for_custom_action
|
|
8
8
|
return render json: result, status: (status_number.presence || 200) if status == true
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
class Endpoints::PushSubscriber < NonCrudEndpoints
|
|
2
|
+
# vapid_public_key is public — no authentication required.
|
|
3
|
+
public_action :vapid_public_key
|
|
4
|
+
|
|
5
|
+
self.desc 'PushSubscriber', :vapid_public_key, {
|
|
6
|
+
get: {
|
|
7
|
+
summary: 'Returns the VAPID public key for browser push subscription',
|
|
8
|
+
parameters: [],
|
|
9
|
+
responses: {
|
|
10
|
+
'200' => {
|
|
11
|
+
description: 'VAPID public key',
|
|
12
|
+
content: {
|
|
13
|
+
'application/json' => {
|
|
14
|
+
schema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
vapid_public_key: { type: 'string' }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def vapid_public_key(params)
|
|
28
|
+
key = ThecoreSettings::Setting.where(ns: :vapid, key: :public_key).pluck(:raw).first
|
|
29
|
+
[{ vapid_public_key: key }, 200]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
self.desc 'PushSubscriber', :subscribe, {
|
|
33
|
+
post: {
|
|
34
|
+
summary: 'Register or update a push subscription for the current user',
|
|
35
|
+
parameters: [],
|
|
36
|
+
responses: {
|
|
37
|
+
'200' => { description: 'Updated subscriber' },
|
|
38
|
+
'201' => { description: 'Created subscriber' }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def subscribe(params)
|
|
44
|
+
user = User.find(params[:current_user_id])
|
|
45
|
+
subscriber = PushSubscriber.subscribe_for(
|
|
46
|
+
user,
|
|
47
|
+
endpoint: params[:endpoint],
|
|
48
|
+
p256dh: params[:p256dh],
|
|
49
|
+
auth: params[:auth],
|
|
50
|
+
user_agent: params[:user_agent]
|
|
51
|
+
)
|
|
52
|
+
status = subscriber.previously_new_record? ? 201 : 200
|
|
53
|
+
[subscriber, status]
|
|
54
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
55
|
+
[{ error: e.message }, 422]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
self.desc 'PushSubscriber', :send_push, {
|
|
59
|
+
post: {
|
|
60
|
+
summary: 'Send a Web Push notification to a specific subscriber',
|
|
61
|
+
parameters: [],
|
|
62
|
+
responses: {
|
|
63
|
+
'201' => { description: 'Message created and dispatched' },
|
|
64
|
+
'422' => { description: 'Validation error' },
|
|
65
|
+
'404' => { description: 'Subscriber not found' }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def send_push(params)
|
|
71
|
+
subscriber = PushSubscriber.active.find_by(id: params[:push_subscriber_id])
|
|
72
|
+
return [{ error: 'Subscriber not found' }, 404] unless subscriber
|
|
73
|
+
message = subscriber.push_messages.build(
|
|
74
|
+
title: params[:title],
|
|
75
|
+
body: params[:body],
|
|
76
|
+
url: params[:url],
|
|
77
|
+
icon: params[:icon]
|
|
78
|
+
)
|
|
79
|
+
unless message.save
|
|
80
|
+
return [{ error: message.errors.full_messages.join(', ') }, 422]
|
|
81
|
+
end
|
|
82
|
+
ThecoreBackendCommons::PushNotificationService.dispatch(subscriber, message)
|
|
83
|
+
[message, 201]
|
|
84
|
+
rescue => e
|
|
85
|
+
[{ error: e.message }, 500]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
self.desc 'PushSubscriber', :acknowledge, {
|
|
89
|
+
post: {
|
|
90
|
+
summary: 'Mark a push message as received or read',
|
|
91
|
+
parameters: [],
|
|
92
|
+
responses: {
|
|
93
|
+
'200' => { description: 'Message updated' },
|
|
94
|
+
'404' => { description: 'Message not found' }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def acknowledge(params)
|
|
100
|
+
message = PushMessage.find_by(id: params[:push_message_id])
|
|
101
|
+
return [{ error: 'Message not found' }, 404] unless message
|
|
102
|
+
|
|
103
|
+
now = Time.current
|
|
104
|
+
message.update!(received_at: now) if params[:received] && message.received_at.nil?
|
|
105
|
+
message.update!(read_at: now) if params[:read] && message.read_at.nil?
|
|
106
|
+
[message, 200]
|
|
107
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
108
|
+
[{ error: e.message }, 422]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -44,7 +44,10 @@ module ApiExceptionManagement
|
|
|
44
44
|
|
|
45
45
|
def api_error(status: 501, errors: [])
|
|
46
46
|
# puts errors.full_messages if !Rails.env.production? && errors.respond_to?(:full_messages)
|
|
47
|
-
|
|
47
|
+
if errors.blank?
|
|
48
|
+
head status
|
|
49
|
+
return
|
|
50
|
+
end
|
|
48
51
|
|
|
49
52
|
# For retrocompatibility, I try to send back only strings, as errors
|
|
50
53
|
errors_response = if errors.respond_to?(:full_messages)
|
data/lib/non_crud_endpoints.rb
CHANGED
|
@@ -2,6 +2,24 @@ class NonCrudEndpoints
|
|
|
2
2
|
attr_accessor :result
|
|
3
3
|
cattr_accessor :definitions
|
|
4
4
|
self.definitions = {}
|
|
5
|
+
|
|
6
|
+
# Registry of actions that do not require authentication.
|
|
7
|
+
# Populated by subclasses via `self.public_action :action_name`.
|
|
8
|
+
# Used by CustomActionDispatcher and ApplicationController to skip
|
|
9
|
+
# authenticate_request for these endpoints.
|
|
10
|
+
cattr_accessor :public_action_registry
|
|
11
|
+
self.public_action_registry = {}
|
|
12
|
+
|
|
13
|
+
def self.public_action(action_name)
|
|
14
|
+
self.public_action_registry[name] ||= []
|
|
15
|
+
self.public_action_registry[name] << action_name.to_sym
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.public_action?(model_name, action_name)
|
|
19
|
+
registry = public_action_registry["Endpoints::#{model_name}"] || []
|
|
20
|
+
registry.include?(action_name.to_sym)
|
|
21
|
+
end
|
|
22
|
+
|
|
5
23
|
# Add a validation method which will be inherited by all the instances, and automatically run before any method call
|
|
6
24
|
def initialize(m, params)
|
|
7
25
|
# Rails.logger.debug "Initializing NonCrudEndpoints"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: model_driven_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriele Tassoni
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '3.0'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
39
|
+
version: '3.0'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: simple_command
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -188,6 +188,7 @@ files:
|
|
|
188
188
|
- app/controllers/api/v3/info_controller.rb
|
|
189
189
|
- app/controllers/api/v3/raw_controller.rb
|
|
190
190
|
- app/controllers/api/v3/users_controller.rb
|
|
191
|
+
- app/models/endpoints/push_subscriber.rb
|
|
191
192
|
- app/models/endpoints/test_api.rb
|
|
192
193
|
- app/models/test_api.rb
|
|
193
194
|
- app/models/used_token.rb
|