Skip to content

Commit a3c2381

Browse files
committed
Refactor endpoints, update JSON
1 parent b20151b commit a3c2381

File tree

9 files changed

+182
-15
lines changed

9 files changed

+182
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ See [STATUS.md](server/STATUS.md) to learn more about which features will remain
1919
- Add `POST` + `body` support for Endpoints #592
2020
- Refactor `Endpoint` handlers, uses a Context now #592
2121
- New sign up / register flow. Add `/register` Endpoint #489 #254
22+
- New sign up / register flow. Add `/register`, `/confirm-email`, `/add-public-key` endpoints #489 #254
2223
- Add multi-tenancy support. Users can create their own `Drives` on subdomains. #288
2324
- Refactor URLs. `store.self_url()` returns an `AtomicUrl`, which provides methods to easily add paths, find subdomains and more.
2425
- Add support for subdomains, use a Wildcard TLS certificate #502

lib/defaults/default_base_models.json

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,23 @@
7878
},
7979
{
8080
"@id": "https://atomicdata.dev/classes/Property",
81-
"https://atomicdata.dev/properties/description": "A Resource that should redirect the browser to a new location. It can also set a `redirectAgent`, which is used in Invites to create an Agent Resource on the Server from a Public Key that the user posesses. See the [Invite docs](https://docs.atomicdata.dev/invitations.html).",
81+
"https://atomicdata.dev/properties/description": "A Property is a single field in a Class. It's the thing that a property field in an Atom points to. An example is `birthdate`. An instance of Property requires various Properties, most notably a `datatype` (e.g. `string` or `integer`), a human readable `description` (such as the thing you're reading), and a `shortname`.",
8282
"https://atomicdata.dev/properties/isA": [
8383
"https://atomicdata.dev/classes/Class"
8484
],
85-
"https://atomicdata.dev/properties/requires": [
86-
"https://atomicdata.dev/properties/destination"
87-
],
85+
"https://atomicdata.dev/properties/parent": "https://atomicdata.dev/classes",
8886
"https://atomicdata.dev/properties/recommends": [
89-
"https://atomicdata.dev/properties/invite/redirectAgent"
87+
"https://atomicdata.dev/properties/classtype",
88+
"https://atomicdata.dev/properties/isDynamic",
89+
"https://atomicdata.dev/properties/isLocked",
90+
"https://atomicdata.dev/properties/allowsOnly"
91+
],
92+
"https://atomicdata.dev/properties/requires": [
93+
"https://atomicdata.dev/properties/shortname",
94+
"https://atomicdata.dev/properties/datatype",
95+
"https://atomicdata.dev/properties/description"
9096
],
91-
"https://atomicdata.dev/properties/shortname": "redirect"
97+
"https://atomicdata.dev/properties/shortname": "property"
9298
},
9399
{
94100
"@id": "https://atomicdata.dev/classes/Class",

lib/defaults/default_store.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,6 @@
789789
"https://atomicdata.dev/properties/isA": [
790790
"https://atomicdata.dev/classes/Property"
791791
],
792-
"https://atomicdata.dev/properties/lastCommit": "https://atomicdata.dev/commits/VB3gtWMkysTX5hKjbYjIM1hfVGPywT3pEPL8c7NwaUAJID6RzptGRPzmix8aKKDeb8Pj1WFv0UPV0YVPxcduBg==",
793792
"https://atomicdata.dev/properties/parent": "https://atomicdata.dev/properties",
794793
"https://atomicdata.dev/properties/shortname": "sub-resources"
795794
},
@@ -801,10 +800,19 @@
801800
"https://atomicdata.dev/properties/isA": [
802801
"https://atomicdata.dev/classes/Property"
803802
],
804-
"https://atomicdata.dev/properties/lastCommit": "https://atomicdata.dev/commits/fS0krtm1wDk0lodH0psnUKmBHBMKLuxnjkd7E7QbkzDk/irQ43gNW3lWxkwQj58ZNg6rUAUMDGJrLy1X3cHwBQ==",
805803
"https://atomicdata.dev/properties/parent": "https://atomicdata.dev/properties",
806804
"https://atomicdata.dev/properties/shortname": "tags"
807805
},
806+
{
807+
"@id": "https://atomicdata.dev/properties/token",
808+
"https://atomicdata.dev/properties/datatype": "https://atomicdata.dev/datatypes/string",
809+
"https://atomicdata.dev/properties/description": "A server-generated string that should not mean anything to the client. It could be a JWT token, or something else.",
810+
"https://atomicdata.dev/properties/isA": [
811+
"https://atomicdata.dev/classes/Property"
812+
],
813+
"https://atomicdata.dev/properties/parent": "https://atomicdata.dev/properties",
814+
"https://atomicdata.dev/properties/shortname": "token"
815+
},
808816
{
809817
"@id": "https://atomicdata.dev/properties/write",
810818
"https://atomicdata.dev/properties/classtype": "https://atomicdata.dev/classes/Agent",

lib/src/db.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use crate::{
2626
email::{self, MailMessage},
2727
endpoints::{default_endpoints, Endpoint, HandleGetContext},
2828
errors::{AtomicError, AtomicResult},
29+
plugins::default_endpoints,
2930
query::QueryResult,
3031
resources::PropVals,
3132
storelike::Storelike,

lib/src/endpoints.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! Examples of endpoints are versions for resources, or (pages for) collections.
44
//! See https://docs.atomicdata.dev/endpoints.html or https://atomicdata.dev/classes/Endpoint
55
6-
use crate::{errors::AtomicResult, plugins, urls, Db, Resource, Storelike, Value};
6+
use crate::{errors::AtomicResult, urls, Db, Resource, Storelike, Value};
77

88
/// The function that is called when a POST request matches the path
99
type HandleGet = fn(context: HandleGetContext) -> AtomicResult<Resource>;

lib/src/plugins/add_pubkey.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*!
2+
Sends users a link to add a new public key to their account.
3+
Useful when a users loses their private key.
4+
*/
5+
6+
use serde::{Deserialize, Serialize};
7+
8+
use crate::{
9+
agents::Agent,
10+
email::{EmailAddress, MailAction, MailMessage},
11+
endpoints::Endpoint,
12+
errors::AtomicResult,
13+
plugins::utils::return_success,
14+
urls, Db, Resource, Storelike,
15+
};
16+
17+
pub fn request_email_add_pubkey() -> Endpoint {
18+
Endpoint {
19+
path: urls::PATH_ADD_PUBKEY.to_string(),
20+
params: [urls::TOKEN.to_string(), urls::INVITE_PUBKEY.to_string()].into(),
21+
description: "Requests an email to add a new PublicKey to an Agent.".to_string(),
22+
shortname: "request-pubkey-reset".to_string(),
23+
handle: Some(handle_request_email_pubkey),
24+
}
25+
}
26+
27+
pub fn confirm_add_pubkey() -> Endpoint {
28+
Endpoint {
29+
path: urls::PATH_CONFIRM_PUBKEY.to_string(),
30+
params: [urls::TOKEN.to_string(), urls::INVITE_PUBKEY.to_string()].into(),
31+
description: "Confirms a token to add a new Public Key.".to_string(),
32+
shortname: "request-pubkey-reset".to_string(),
33+
handle: Some(handle_confirm_add_pubkey),
34+
}
35+
}
36+
37+
#[derive(Debug, Serialize, Deserialize)]
38+
struct AddPubkeyToken {
39+
agent: String,
40+
}
41+
42+
pub fn handle_request_email_pubkey(
43+
url: url::Url,
44+
store: &Db,
45+
_for_agent: Option<&str>,
46+
) -> AtomicResult<Resource> {
47+
let mut email_option: Option<EmailAddress> = None;
48+
for (k, v) in url.query_pairs() {
49+
if let "email" = k.as_ref() {
50+
email_option = Some(EmailAddress::new(v.to_string())?)
51+
}
52+
}
53+
// by default just return the Endpoint
54+
let Some(email) = email_option else {
55+
return request_email_add_pubkey().to_resource(store);
56+
};
57+
58+
// Find the agent by their email
59+
let agent = match Agent::from_email(&email.to_string(), store) {
60+
Ok(a) => a,
61+
// If we can't find the agent, we should still return a `success` response,
62+
// in order to prevent users to know that the email exists.
63+
Err(_) => return return_success(),
64+
};
65+
66+
// send the user an e-mail to confirm sign up
67+
let store_clone = store.clone();
68+
let confirmation_token_struct = AddPubkeyToken {
69+
agent: agent.subject,
70+
};
71+
let token = crate::token::sign_claim(store, confirmation_token_struct)?;
72+
let mut confirm_url = store
73+
.get_server_url()
74+
.clone()
75+
.set_path(urls::PATH_CONFIRM_PUBKEY)
76+
.url();
77+
confirm_url.set_query(Some(&format!("token={}", token)));
78+
let message = MailMessage {
79+
to: email,
80+
subject: "Add a new Passphrase to your account".to_string(),
81+
body: "You've requested adding a new Passphrase. Click the link below to do so!"
82+
.to_string(),
83+
action: Some(MailAction {
84+
name: "Add new Passphrase to account".to_string(),
85+
url: confirm_url.into(),
86+
}),
87+
};
88+
// async, because mails are slow
89+
tokio::spawn(async move {
90+
store_clone
91+
.send_email(message)
92+
.await
93+
.unwrap_or_else(|e| tracing::error!("Error sending email: {}", e));
94+
});
95+
96+
return_success()
97+
}
98+
99+
#[tracing::instrument(skip(store))]
100+
pub fn handle_confirm_add_pubkey(
101+
url: url::Url,
102+
store: &Db,
103+
for_agent: Option<&str>,
104+
) -> AtomicResult<Resource> {
105+
let mut token_opt: Option<String> = None;
106+
let mut pubkey_option = None;
107+
108+
for (k, v) in url.query_pairs() {
109+
match k.as_ref() {
110+
"token" | urls::TOKEN => token_opt = Some(v.to_string()),
111+
"public-key" | urls::INVITE_PUBKEY => pubkey_option = Some(v.to_string()),
112+
_ => {}
113+
}
114+
}
115+
116+
let Some(token) = token_opt else {
117+
return confirm_add_pubkey().to_resource(store);
118+
};
119+
120+
let pubkey = pubkey_option.ok_or("No public-key provided")?;
121+
122+
// Parse and verify the JWT token
123+
let confirmation = crate::token::verify_claim::<AddPubkeyToken>(store, &token)?.custom;
124+
125+
// Add the key to the agent
126+
let mut agent = store.get_resource(&confirmation.agent)?;
127+
agent.push_propval(urls::ACTIVE_KEYS, pubkey.into(), true)?;
128+
agent.save(store)?;
129+
130+
return_success()
131+
}

lib/src/plugins/mod.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Plugins can have functions that are called at specific moments by Atomic-Server.
66
77
For example:
88
9-
- Before returning a Resource. These are either Endpoints or Class Extenders.
9+
- Before returning a Resource. These are either [Endpoint]s or Class Extenders.
1010
- Before applying a Commit.
1111
1212
In the long term, these plugins will probably be powered by WASM and can be extended at runtime.
@@ -16,13 +16,13 @@ However, they are designed in such a way that they have a limited scope and a cl
1616
## Extending resources
1717
1818
There are two ways of extending / modifying a Resource.
19-
Endpoints are great for APIs that have a fixed route, and Class Extenders are great for APIs that don't have a fixed route.
19+
[Endpoint]s are great for APIs that have a fixed route, and Class Extenders are great for APIs that don't have a fixed route.
2020
Endpoints are easier to generate from Rust, and will be available the second a server is Running.
2121
22-
### Endpoints
22+
### [Endpoint]s
2323
2424
Resources that typically parse query parameters and return a dynamic resource.
25-
When adding an endpoint, add it to the list of endpoints in [lib/src/endpoints.rs]
25+
When adding an endpoint, add it to the list of [default_endpoints] in this file.
2626
Endpoints are all instances of the [crate] class.
2727
They are presented in the UI as a form.
2828
@@ -31,22 +31,41 @@ They are presented in the UI as a form.
3131
Similar to Endpoints, Class Extenders can modify their contents before creating a response.
3232
Contrary to Endpoints, these can be any type of Class.
3333
They are used for performing custom queries, or calculating dynamic attributes.
34+
Add these by registering the handler at [crate::db::Db::get_resource_extended].
3435
*/
3536

37+
use crate::endpoints::Endpoint;
38+
3639
// Class Extenders
3740
pub mod chatroom;
3841
pub mod importer;
3942
pub mod invite;
4043

4144
// Endpoints
45+
pub mod add_pubkey;
4246
#[cfg(feature = "html")]
4347
pub mod bookmark;
4448
pub mod files;
4549
pub mod path;
4650
pub mod register;
47-
pub mod reset_pubkey;
4851
pub mod search;
4952
pub mod versioning;
5053

5154
// Utilities / helpers
5255
mod utils;
56+
57+
pub fn default_endpoints() -> Vec<Endpoint> {
58+
vec![
59+
versioning::version_endpoint(),
60+
versioning::all_versions_endpoint(),
61+
path::path_endpoint(),
62+
search::search_endpoint(),
63+
files::upload_endpoint(),
64+
register::register_endpoint(),
65+
register::confirm_email_endpoint(),
66+
add_pubkey::request_email_add_pubkey(),
67+
add_pubkey::confirm_add_pubkey(),
68+
#[cfg(feature = "html")]
69+
bookmark::bookmark_endpoint(),
70+
]
71+
}

lib/src/plugins/register.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ pub fn handle_register_name_and_email(context: HandleGetContext) -> AtomicResult
109109
.get_server_url()
110110
.clone()
111111
.set_path(urls::PATH_CONFIRM_EMAIL)
112+
// .set_subdomain(Some(&name.to_string()))?
112113
.url();
113114
confirm_url.set_query(Some(&format!("token={}", token)));
114115
let message = MailMessage {

lib/src/populate.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ pub fn populate_collections(store: &impl Storelike) -> AtomicResult<()> {
247247
pub fn populate_endpoints(store: &crate::Db) -> AtomicResult<()> {
248248
use crate::atomic_url::Routes;
249249

250-
let endpoints = crate::endpoints::default_endpoints();
250+
let endpoints = crate::plugins::default_endpoints();
251251
let endpoints_collection = store.get_server_url().set_route(Routes::Endpoints);
252252
for endpoint in endpoints {
253253
let mut resource = endpoint.to_resource(store)?;

0 commit comments

Comments
 (0)