summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.sqlx/query-2262a6d3af5b470a397f3371bbd9e0da6f99ea037bc5c4cb8c468fbd977b9543.json44
-rw-r--r--.sqlx/query-81023ce5ac1032bc853ea5ec03e903a30d10a1cc4c09f993c91f636e0793e225.json12
-rw-r--r--.sqlx/query-8bd26c3ccd7ae16380758ba7061943a09f933a996e8e74373047c45f6d5ad63e.json12
-rw-r--r--migrations/20250724_subscription.sql17
-rw-r--r--migrations/20250725023155_push_notification.sql15
-rw-r--r--src/app.rs19
-rw-r--r--src/cli.rs22
-rw-r--r--src/push/app.rs138
-rw-r--r--src/push/handlers/echo.rs20
-rw-r--r--src/push/handlers/mod.rs79
-rw-r--r--src/push/handlers/register.rs40
-rw-r--r--src/push/handlers/unregister.rs16
-rw-r--r--src/push/id.rs27
-rw-r--r--src/push/mod.rs6
-rw-r--r--src/push/repo.rs81
-rw-r--r--src/push/vapid.rs28
-rw-r--r--src/routes.rs9
17 files changed, 444 insertions, 141 deletions
diff --git a/.sqlx/query-2262a6d3af5b470a397f3371bbd9e0da6f99ea037bc5c4cb8c468fbd977b9543.json b/.sqlx/query-2262a6d3af5b470a397f3371bbd9e0da6f99ea037bc5c4cb8c468fbd977b9543.json
new file mode 100644
index 0000000..f83a179
--- /dev/null
+++ b/.sqlx/query-2262a6d3af5b470a397f3371bbd9e0da6f99ea037bc5c4cb8c468fbd977b9543.json
@@ -0,0 +1,44 @@
+{
+ "db_name": "SQLite",
+ "query": "\n select\n id as \"id: Id\",\n user as \"user: user::Id\",\n endpoint,\n key_p256dh,\n key_auth\n from subscription\n where id = $1\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "id: Id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "user: user::Id",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "endpoint",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "key_p256dh",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "key_auth",
+ "ordinal": 4,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "2262a6d3af5b470a397f3371bbd9e0da6f99ea037bc5c4cb8c468fbd977b9543"
+}
diff --git a/.sqlx/query-81023ce5ac1032bc853ea5ec03e903a30d10a1cc4c09f993c91f636e0793e225.json b/.sqlx/query-81023ce5ac1032bc853ea5ec03e903a30d10a1cc4c09f993c91f636e0793e225.json
new file mode 100644
index 0000000..1dad656
--- /dev/null
+++ b/.sqlx/query-81023ce5ac1032bc853ea5ec03e903a30d10a1cc4c09f993c91f636e0793e225.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n insert into subscription (id, user, endpoint, key_p256dh, key_auth)\n values ($1, $2, $3, $4, $5)\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 5
+ },
+ "nullable": []
+ },
+ "hash": "81023ce5ac1032bc853ea5ec03e903a30d10a1cc4c09f993c91f636e0793e225"
+}
diff --git a/.sqlx/query-8bd26c3ccd7ae16380758ba7061943a09f933a996e8e74373047c45f6d5ad63e.json b/.sqlx/query-8bd26c3ccd7ae16380758ba7061943a09f933a996e8e74373047c45f6d5ad63e.json
new file mode 100644
index 0000000..811689f
--- /dev/null
+++ b/.sqlx/query-8bd26c3ccd7ae16380758ba7061943a09f933a996e8e74373047c45f6d5ad63e.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n delete from subscription\n where id = $1\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": []
+ },
+ "hash": "8bd26c3ccd7ae16380758ba7061943a09f933a996e8e74373047c45f6d5ad63e"
+}
diff --git a/migrations/20250724_subscription.sql b/migrations/20250724_subscription.sql
deleted file mode 100644
index 368557a..0000000
--- a/migrations/20250724_subscription.sql
+++ /dev/null
@@ -1,17 +0,0 @@
-create table subscription (
- id text
- not null
- primary key,
- user text
- not null
- references user (id),
- endpoint text
- unique
- not null,
- key_p256dh text
- not null,
- key_auth text
- not null,
- expiration_time text,
-);
-
diff --git a/migrations/20250725023155_push_notification.sql b/migrations/20250725023155_push_notification.sql
new file mode 100644
index 0000000..374e152
--- /dev/null
+++ b/migrations/20250725023155_push_notification.sql
@@ -0,0 +1,15 @@
+create table subscription (
+ id text
+ not null
+ primary key,
+ user text
+ not null
+ references user (id),
+ endpoint text
+ unique
+ not null,
+ key_p256dh text
+ not null,
+ key_auth text
+ not null
+);
diff --git a/src/app.rs b/src/app.rs
index 133ee04..73d4621 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,14 +1,15 @@
-use sqlx::sqlite::SqlitePool;
-
use crate::{
boot::app::Boot,
conversation::app::Conversations,
event::{self, app::Events},
invite::app::Invites,
message::app::Messages,
+ push::app::Push,
setup::app::Setup,
token::{self, app::Tokens},
};
+use sqlx::sqlite::SqlitePool;
+use web_push::PartialVapidSignatureBuilder;
#[cfg(test)]
use crate::user::app::Users;
@@ -18,16 +19,24 @@ pub struct App {
db: SqlitePool,
events: event::Broadcaster,
token_events: token::Broadcaster,
+ vapid_public_key: String,
+ vapid_signer: PartialVapidSignatureBuilder,
}
impl App {
- pub fn from(db: SqlitePool) -> Self {
+ pub fn from(
+ db: SqlitePool,
+ vapid_public_key: String,
+ vapid_signer: PartialVapidSignatureBuilder,
+ ) -> Self {
let events = event::Broadcaster::default();
let token_events = token::Broadcaster::default();
Self {
db,
events,
token_events,
+ vapid_public_key,
+ vapid_signer,
}
}
}
@@ -58,6 +67,10 @@ impl App {
Messages::new(&self.db, &self.events)
}
+ pub fn push(&self) -> Push {
+ Push::new(&self.db, &self.vapid_public_key, &self.vapid_signer)
+ }
+
pub const fn setup(&self) -> Setup {
Setup::new(&self.db, &self.events)
}
diff --git a/src/cli.rs b/src/cli.rs
index 703bf19..fe43c56 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -16,7 +16,9 @@ use tokio::net;
use crate::{
app::App,
- clock, db, routes,
+ clock, db,
+ push::vapid,
+ routes,
umask::{self, Umask},
};
@@ -26,10 +28,8 @@ use crate::{
/// arguments for the `pilcrow` server:
///
/// ```no_run
-/// # use pilcrow::cli::Error;
-/// #
/// # #[tokio::main]
-/// # async fn main() -> Result<(), Error> {
+/// # async fn main() -> Result<(), impl std::error::Error> {
/// use clap::Parser;
/// use pilcrow::cli::Args;
///
@@ -67,6 +67,14 @@ pub struct Args {
/// upgrades
#[arg(short = 'D', long, env, default_value = "sqlite://pilcrow.db.backup")]
backup_database_url: String,
+
+ /// Base64-encoded VAPID public key for web push notifications.
+ #[arg(long, env)]
+ vapid_public_key: String,
+
+ /// Base64-encoded VAPID public key for web push notifications.
+ #[arg(long, env)]
+ vapid_private_key: vapid::PrivateKey,
}
impl Args {
@@ -90,7 +98,10 @@ impl Args {
self.umask.set();
let pool = self.pool().await?;
- let app = App::from(pool);
+ let vapid_public_key = self.vapid_public_key.clone();
+ let vapid_signer = self.vapid_private_key.as_signature_builder()?;
+
+ let app = App::from(pool, vapid_public_key, vapid_signer);
let app = routes::routes(&app)
.route_layer(middleware::from_fn(clock::middleware))
.route_layer(middleware::map_response(Self::server_info()))
@@ -141,6 +152,7 @@ fn started_msg(listener: &net::TcpListener) -> io::Result<String> {
#[error(transparent)]
enum Error {
Io(#[from] io::Error),
+ WebPush(#[from] web_push::WebPushError),
Database(#[from] db::Error),
Umask(#[from] umask::Error),
}
diff --git a/src/push/app.rs b/src/push/app.rs
new file mode 100644
index 0000000..2d6e15c
--- /dev/null
+++ b/src/push/app.rs
@@ -0,0 +1,138 @@
+use sqlx::SqlitePool;
+use web_push::{
+ ContentEncoding, IsahcWebPushClient, PartialVapidSignatureBuilder, SubscriptionInfo,
+ WebPushClient, WebPushMessageBuilder,
+};
+
+use super::{Id, repo::Provider as _};
+
+use crate::{
+ db::NotFound as _,
+ user::{self, User},
+};
+
+pub struct Push<'a> {
+ db: &'a SqlitePool,
+ vapid_public_key: &'a str,
+ vapid_signer: &'a PartialVapidSignatureBuilder,
+}
+
+impl<'a> Push<'a> {
+ pub const fn new(
+ db: &'a SqlitePool,
+ vapid_public_key: &'a str,
+ vapid_signer: &'a PartialVapidSignatureBuilder,
+ ) -> Self {
+ Self {
+ db,
+ vapid_public_key,
+ vapid_signer,
+ }
+ }
+
+ pub fn public_key(&self) -> &str {
+ self.vapid_public_key
+ }
+
+ pub async fn register(
+ &self,
+ user: &User,
+ subscription: &SubscriptionInfo,
+ ) -> Result<Id, RegisterError> {
+ let mut tx = self.db.begin().await?;
+ let id = tx.subscriptions().create(user, subscription).await?;
+ tx.commit().await?;
+
+ Ok(id)
+ }
+
+ pub async fn echo(
+ &self,
+ user: &User,
+ subscription: &Id,
+ message: &str,
+ ) -> Result<(), EchoError> {
+ let mut tx = self.db.begin().await?;
+ let subscription = tx
+ .subscriptions()
+ .by_id(subscription)
+ .await
+ .not_found(|| EchoError::NotFound(subscription.clone()))?;
+ if subscription.user != user.id {
+ return Err(EchoError::NotSubscriber(subscription.id, user.id.clone()));
+ }
+
+ tx.commit().await?;
+
+ self.send(&subscription.info, message).await?;
+
+ Ok(())
+ }
+
+ async fn send(&self, subscription: &SubscriptionInfo, message: &str) -> Result<(), EchoError> {
+ let sig_builder = self
+ .vapid_signer
+ .clone()
+ .add_sub_info(subscription)
+ .build()?;
+
+ let payload = message.as_bytes();
+
+ let mut message_builder = WebPushMessageBuilder::new(subscription);
+ message_builder.set_payload(ContentEncoding::Aes128Gcm, payload);
+ message_builder.set_vapid_signature(sig_builder);
+ let message = message_builder.build()?;
+
+ let client = IsahcWebPushClient::new()?;
+ client.send(message).await?;
+
+ Ok(())
+ }
+
+ pub async fn unregister(&self, user: &User, subscription: &Id) -> Result<(), UnregisterError> {
+ let mut tx = self.db.begin().await?;
+ let subscription = tx
+ .subscriptions()
+ .by_id(subscription)
+ .await
+ .not_found(|| UnregisterError::NotFound(subscription.clone()))?;
+ if subscription.user != user.id {
+ return Err(UnregisterError::NotSubscriber(
+ subscription.id,
+ user.id.clone(),
+ ));
+ }
+ tx.subscriptions().delete(&subscription).await?;
+ tx.commit().await?;
+
+ Ok(())
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum RegisterError {
+ #[error(transparent)]
+ Database(#[from] sqlx::Error),
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum EchoError {
+ #[error("subscription {0} not found")]
+ NotFound(Id),
+ #[error("user {1} is not the subscriber for subscription {0}")]
+ NotSubscriber(Id, user::Id),
+ #[error(transparent)]
+ WebPush(#[from] web_push::WebPushError),
+ #[error(transparent)]
+ Database(#[from] sqlx::Error),
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum UnregisterError {
+ #[error("subscription {0} not found")]
+ NotFound(Id),
+ #[error("user {1} is not the subscriber for subscription {0}")]
+ NotSubscriber(Id, user::Id),
+ #[error(transparent)]
+ Database(#[from] sqlx::Error),
+}
diff --git a/src/push/handlers/echo.rs b/src/push/handlers/echo.rs
new file mode 100644
index 0000000..4b4de57
--- /dev/null
+++ b/src/push/handlers/echo.rs
@@ -0,0 +1,20 @@
+use axum::extract::{Json, State};
+
+use crate::{app::App, push::Id, token::extract::Identity};
+
+#[derive(serde::Deserialize)]
+pub struct Request {
+ subscription: Id,
+ msg: String,
+}
+
+pub async fn handler(
+ State(app): State<App>,
+ identity: Identity,
+ Json(request): Json<Request>,
+) -> Result<(), crate::error::Internal> {
+ let Request { subscription, msg } = request;
+ app.push().echo(&identity.user, &subscription, &msg).await?;
+
+ Ok(())
+}
diff --git a/src/push/handlers/mod.rs b/src/push/handlers/mod.rs
index e4a531b..90edaa7 100644
--- a/src/push/handlers/mod.rs
+++ b/src/push/handlers/mod.rs
@@ -1,74 +1,15 @@
-use std::env;
+use axum::extract::State;
-use axum::{
- extract::{Json},
-};
+use crate::app::App;
-use web_push::{
- SubscriptionInfo,
- VapidSignatureBuilder,
- WebPushMessageBuilder,
- ContentEncoding,
- WebPushClient,
- IsahcWebPushClient,
-};
+mod echo;
+mod register;
+mod unregister;
+pub use echo::handler as echo;
+pub use register::handler as register;
+pub use unregister::handler as unregister;
-pub async fn vapid() -> String {
- let vapid_public_key = env::var("VAPID_PUBLIC_KEY").unwrap_or_default();
- String::from(vapid_public_key)
-}
-
-
-pub async fn register() -> String {
- String::from("OK")
-}
-
-
-pub async fn unregister() -> String {
- String::from("OK")
-}
-
-async fn push_message(
- endpoint: String,
- keys: Keys,
- message: &String,
-) -> Result<(), crate::error::Internal> {
- let content = message.as_bytes();
-
- let subscription_info = SubscriptionInfo::new(endpoint, keys.p256dh, keys.auth);
- // This will need to come from the DB eventually:
- let private_key = String::from(env::var("VAPID_PRIVATE_KEY").unwrap_or_default());
- let sig_builder = VapidSignatureBuilder::from_base64(&private_key, &subscription_info)?.build()?;
- let mut builder = WebPushMessageBuilder::new(&subscription_info);
- builder.set_payload(ContentEncoding::Aes128Gcm, content);
- builder.set_vapid_signature(sig_builder);
- let client = IsahcWebPushClient::new()?;
- client.send(builder.build()?).await?;
-
- Ok(())
-}
-
-
-#[axum::debug_handler]
-pub async fn echo(
- Json(payload): Json<PushPayload>,
-) -> Result<(), crate::error::Internal> {
- push_message(payload.endpoint, payload.keys, &payload.msg).await?;
-
- Ok(())
-}
-
-
-#[derive(serde::Deserialize)]
-pub struct Keys {
- pub p256dh: String,
- pub auth: String,
-}
-
-#[derive(serde::Deserialize)]
-pub struct PushPayload {
- pub msg: String,
- pub endpoint: String,
- pub keys: Keys,
+pub async fn vapid(State(app): State<App>) -> String {
+ app.push().public_key().to_owned()
}
diff --git a/src/push/handlers/register.rs b/src/push/handlers/register.rs
new file mode 100644
index 0000000..201928b
--- /dev/null
+++ b/src/push/handlers/register.rs
@@ -0,0 +1,40 @@
+use axum::extract::{Json, State};
+use web_push::SubscriptionInfo;
+
+use crate::{app::App, error::Internal, push::Id, token::extract::Identity};
+
+#[derive(serde::Deserialize)]
+pub struct Request {
+ endpoint: String,
+ p256dh: String,
+ auth: String,
+}
+
+#[derive(serde::Serialize)]
+pub struct Response {
+ id: Id,
+}
+
+pub async fn handler(
+ State(app): State<App>,
+ identity: Identity,
+ Json(request): Json<Request>,
+) -> Result<Json<Response>, Internal> {
+ let subscription = request.into();
+
+ let id = app.push().register(&identity.user, &subscription).await?;
+
+ Ok(Json(Response { id }))
+}
+
+impl From<Request> for SubscriptionInfo {
+ fn from(request: Request) -> Self {
+ let Request {
+ endpoint,
+ p256dh,
+ auth,
+ } = request;
+ let info = Self::new(endpoint, p256dh, auth);
+ info
+ }
+}
diff --git a/src/push/handlers/unregister.rs b/src/push/handlers/unregister.rs
new file mode 100644
index 0000000..a00ee92
--- /dev/null
+++ b/src/push/handlers/unregister.rs
@@ -0,0 +1,16 @@
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+};
+
+use crate::{app::App, error::Internal, push::Id, token::extract::Identity};
+
+pub async fn handler(
+ State(app): State<App>,
+ identity: Identity,
+ Path(subscription): Path<Id>,
+) -> Result<StatusCode, Internal> {
+ app.push().unregister(&identity.user, &subscription).await?;
+
+ Ok(StatusCode::NO_CONTENT)
+}
diff --git a/src/push/id.rs b/src/push/id.rs
new file mode 100644
index 0000000..b28d6ab
--- /dev/null
+++ b/src/push/id.rs
@@ -0,0 +1,27 @@
+use std::fmt;
+
+use crate::id::Id as BaseId;
+
+// Stable identifier for a push subscription. Prefixed with `S`.
+#[derive(Clone, Debug, Eq, Hash, PartialEq, sqlx::Type, serde::Deserialize, serde::Serialize)]
+#[sqlx(transparent)]
+#[serde(transparent)]
+pub struct Id(BaseId);
+
+impl From<BaseId> for Id {
+ fn from(id: BaseId) -> Self {
+ Self(id)
+ }
+}
+
+impl Id {
+ pub fn generate() -> Self {
+ BaseId::generate("S")
+ }
+}
+
+impl fmt::Display for Id {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
diff --git a/src/push/mod.rs b/src/push/mod.rs
index c3d4495..c32cb27 100644
--- a/src/push/mod.rs
+++ b/src/push/mod.rs
@@ -1 +1,7 @@
+pub mod app;
pub mod handlers;
+mod id;
+mod repo;
+pub mod vapid;
+
+use id::Id;
diff --git a/src/push/repo.rs b/src/push/repo.rs
index 2d492ea..ddef706 100644
--- a/src/push/repo.rs
+++ b/src/push/repo.rs
@@ -1,9 +1,8 @@
use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite};
+use web_push::SubscriptionInfo;
-use super::{Subscription, Id};
-use crate::{
- user::{self, User},
-}
+use super::Id;
+use crate::user::{self, User};
pub trait Provider {
fn subscriptions(&mut self) -> Subscriptions;
@@ -21,74 +20,68 @@ impl Subscriptions<'_> {
pub async fn create(
&mut self,
user: &User,
- endpoint: &String,
- key_p256dh: &String,
- key_auth: &String,
- expiration_time: &String,
- ) -> Result<Subscription, sqlx::Error> {
+ info: &SubscriptionInfo,
+ ) -> Result<Id, sqlx::Error> {
let id = Id::generate();
- let subscription = sqlx::query!(
+ sqlx::query!(
r#"
- insert into subscription
- (id, user, endpoint, key_p256dh, key_auth, expiration_time)
- values ($1, $2, $3, $4, $5, $6)
- returning
- id as "id: Id",
- user as "user: user::Id",
- endpoint as "endpoint: String",
- key_p256dh as "key_p256dh: String",
- key_auth as "key_auth: String",
- expiration_time as "expiration_time: String"
+ insert into subscription (id, user, endpoint, key_p256dh, key_auth)
+ values ($1, $2, $3, $4, $5)
"#,
id,
user.id,
- endpoint,
- key_p256dh,
- key_auth,
- expiration_time,
+ info.endpoint,
+ info.keys.p256dh,
+ info.keys.auth,
)
- .fetch_one(&mut *self.0)
+ .execute(&mut *self.0)
.await?;
- Ok(subscription)
+ Ok(id)
}
- pub async fn for_user(&mut self, user: &User) -> Result<vec<Subscription>, sqlx::Error> {
- let subscriptions = sqlx::query!(
+ pub async fn by_id(&mut self, id: &Id) -> Result<Subscription, sqlx::Error> {
+ let subscription = sqlx::query!(
r#"
select
id as "id: Id",
user as "user: user::Id",
- endpoint as "endpoint: String",
- key_p256dh as "key_p256dh: String",
- key_auth as "key_auth: String",
+ endpoint,
+ key_p256dh,
+ key_auth
from subscription
- where user = $1
+ where id = $1
"#,
- user.id,
+ id,
)
- .fetch_all(&mut *self.0)
+ .map(|row| Subscription {
+ id: row.id,
+ user: row.user,
+ info: SubscriptionInfo::new(row.endpoint, row.key_p256dh, row.key_auth),
+ })
+ .fetch_one(&mut *self.0)
.await?;
- Ok(subscriptions)
+ Ok(subscription)
}
- pub async fn delete(
- &mut self,
- subscription: &Subscription,
- deleted: &Instant,
- ) -> Result<(), sqlx::Error> {
- let id = subscription.id();
-
+ pub async fn delete(&mut self, subscription: &Subscription) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
- delete from subscription where id = $1
+ delete from subscription
+ where id = $1
"#,
- id,
+ subscription.id,
)
.execute(&mut *self.0)
.await?;
Ok(())
}
}
+
+pub struct Subscription {
+ pub id: Id,
+ pub user: user::Id,
+ pub info: SubscriptionInfo,
+}
diff --git a/src/push/vapid.rs b/src/push/vapid.rs
new file mode 100644
index 0000000..b13a00c
--- /dev/null
+++ b/src/push/vapid.rs
@@ -0,0 +1,28 @@
+use std::fmt;
+
+use web_push::{PartialVapidSignatureBuilder, VapidSignatureBuilder, WebPushError};
+
+#[derive(Clone)]
+pub struct PrivateKey(String);
+
+impl PrivateKey {
+ pub fn as_signature_builder(&self) -> Result<PartialVapidSignatureBuilder, WebPushError> {
+ let Self(key) = self;
+ VapidSignatureBuilder::from_base64_no_sub(key)
+ }
+}
+
+impl fmt::Debug for PrivateKey {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_tuple("PrivateKey").field(&"********").finish()
+ }
+}
+
+impl<S> From<S> for PrivateKey
+where
+ S: Into<String>,
+{
+ fn from(value: S) -> Self {
+ Self(value.into())
+ }
+}
diff --git a/src/routes.rs b/src/routes.rs
index 293dae3..ce60835 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -4,7 +4,7 @@ use axum::{
routing::{delete, get, post},
};
-use crate::{app::App, boot, conversation, event, expire, invite, message, setup, push, ui, user};
+use crate::{app::App, boot, conversation, event, expire, invite, message, push, setup, ui, user};
pub fn routes(app: &App) -> Router<App> {
// UI routes that can be accessed before the administrator completes setup.
@@ -26,8 +26,11 @@ pub fn routes(app: &App) -> Router<App> {
let api_bootstrap = Router::new()
.route("/api/setup", post(setup::handlers::setup))
.route("/api/vapid", get(push::handlers::vapid))
- .route("/api/register", post(push::handlers::register))
- .route("/api/unregister", post(push::handlers::unregister))
+ .route("/api/push", post(push::handlers::register))
+ .route(
+ "/api/push/:subscription",
+ delete(push::handlers::unregister),
+ )
.route("/api/echo", post(push::handlers::echo));
// API routes that require the administrator to complete setup first.