1mod account_annotations;
3mod email_annotations;
4mod model_annotations;
5mod oauth_annotations;
6mod password_annotations;
7mod session_annotations;
8mod sign_in_annotations;
9mod user_annotations;
10#[rustfmt::skip]
11#[allow(warnings, clippy::all, clippy::pedantic, clippy::nursery, clippy::restriction, reason = "Generated API declarations retain the upstream generator output")]
12mod source_endpoints;
13#[rustfmt::skip]
14#[allow(warnings, clippy::all, clippy::pedantic, clippy::nursery, clippy::restriction, reason = "Generated API declarations retain the upstream generator output")]
15mod source_models;
16use alibi_core::{AuthRoute, OpenApiEndpoint, OpenApiField, OpenApiModel, PluginOpenApiMetadata};
17use alibi_schema_registry::{FieldDef, plugin_schemas};
18use serde_json::{Value, json};
19
20fn wire_name(name: &str) -> String {
21 let mut words = name.split('_');
22 let mut output = words.next().unwrap_or_default().to_owned();
23 for word in words {
24 let mut letters = word.chars();
25 if let Some(first) = letters.next() {
26 output.extend(first.to_uppercase());
27 output.extend(letters);
28 }
29 }
30 output
31}
32fn field(field: &FieldDef, core: bool) -> Option<OpenApiField> {
33 if field.is_primary_key {
34 return None;
35 }
36 let ty = field.ty.trim_start_matches("Option<").trim_end_matches('>');
37 let schema = match ty {
38 "DateTimeUtc" => json!({"type":"string","format":"date-time"}),
39 "bool" => json!({"type":"boolean"}),
40 "i64" | "f64" => json!({"type":"number"}),
41 "Json" => json!({"type":"json"}),
42 _ => json!({"type":"string"}),
43 };
44 let mut field = OpenApiField::new(
45 wire_name(field.name),
46 schema,
47 !field.ty.starts_with("Option<"),
48 );
49 if core && matches!(field.name.as_str(), "name" | "email") {
50 field.required = true;
51 }
52 if core && field.name == "emailVerified" {
53 field.input = false;
54 if let Some(object) = field.schema.as_object_mut() {
55 _ = object.insert("default".into(), json!(false));
56 }
57 }
58 Some(field)
59}
60#[must_use]
63pub fn plugin_metadata(plugin: &str, routes: &[AuthRoute]) -> PluginOpenApiMetadata {
64 let is_core = |owner: &str| {
65 matches!(
66 owner,
67 "core"
68 | "email-password"
69 | "session-management"
70 | "email-verification"
71 | "password-management"
72 | "user-management"
73 | "account-management"
74 | "oauth"
75 )
76 };
77 let mut metadata = PluginOpenApiMetadata {
78 core: is_core(plugin),
79 ..Default::default()
80 };
81 for route in routes {
82 let owner = if plugin == "email-password"
83 && matches!(
84 route.path.as_str(),
85 "/sign-in/username" | "/is-username-available"
86 ) {
87 "username"
88 } else {
89 plugin
90 };
91 let mut annotation = source_endpoints::endpoint(owner, &route.path)
92 .or_else(|| endpoint(&route.path))
93 .unwrap_or_else(|| OpenApiEndpoint {
94 operation_id: Some(route.operation_id.clone()),
95 ..Default::default()
96 });
97 if owner != plugin {
98 annotation.owner = Some(owner.into());
99 annotation.core = Some(is_core(owner));
100 }
101 metadata
102 .endpoints
103 .push((route.method.clone(), route.path.clone(), annotation));
104 }
105 if let Some(models) = source_models::models(plugin) {
106 metadata.models = models;
107 return metadata;
108 }
109 if let Some(schema) = plugin_schemas().iter().find(|schema| schema.name == plugin) {
110 for (name, definitions) in [
111 ("User", schema.user_fields),
112 ("Session", schema.session_fields),
113 ] {
114 let fields = definitions
115 .iter()
116 .filter_map(|definition| field(definition, false))
117 .collect::<Vec<_>>();
118 if !fields.is_empty() {
119 metadata.models.push(OpenApiModel::new(name, fields));
120 }
121 }
122 for entity in schema.extra_entities {
123 let wire = if plugin == "jwt" {
124 "jwks".into()
125 } else {
126 wire_name(entity.mod_name)
127 };
128 let mut chars = wire.chars();
129 let name = chars
130 .next()
131 .map(|first| {
132 let mut name = first.to_uppercase().collect::<String>();
133 name.push_str(chars.as_str());
134 name
135 })
136 .unwrap_or_default();
137 metadata.models.push(OpenApiModel::new(
138 name,
139 entity
140 .fields
141 .iter()
142 .filter_map(|definition| field(definition, false))
143 .collect(),
144 ));
145 }
146 }
147 metadata
148}
149#[must_use]
150pub fn instance_plugin_metadata<S: alibi_core::AuthSchema>(
151 plugin: &str,
152 routes: &[AuthRoute],
153 ctx: &alibi_core::AuthInitContext<S>,
154) -> PluginOpenApiMetadata {
155 let mut metadata = plugin_metadata(plugin, routes);
156 model_annotations::apply(plugin, ctx, &mut metadata);
157 metadata
158}
159#[must_use]
160pub fn core_routes() -> Vec<AuthRoute> {
161 vec![
162 AuthRoute::get("/ok", "ok"),
163 AuthRoute::get("/error", "error"),
164 AuthRoute::post("/update-user", "update_user"),
165 ]
166}
167fn response(description: &str, schema: &Value) -> Value {
168 json!({"description":description,"content":{"application/json":{"schema":schema}}})
169}
170fn endpoint(path: &str) -> Option<OpenApiEndpoint> {
171 sign_in_annotations::endpoint(path)
172 .or_else(|| oauth_annotations::endpoint(path))
173 .or_else(|| user_annotations::endpoint(path))
174 .or_else(|| account_annotations::endpoint(path))
175 .or_else(|| password_annotations::endpoint(path))
176 .or_else(|| email_annotations::endpoint(path))
177 .or_else(|| session_annotations::endpoint(path))
178 .or_else(|| builtin_endpoint(path))
179}
180
181fn builtin_endpoint(path: &str) -> Option<OpenApiEndpoint> {
182 let mut metadata = OpenApiEndpoint::default();
183 match path {
184 "/callback/{provider}" | "/callback/:id" => {
185 metadata.document_path = Some("/callback/:id".into());
186 let mut properties = serde_json::Map::new();
187 for name in [
188 "code",
189 "error",
190 "device_id",
191 "error_description",
192 "state",
193 "user",
194 "iss",
195 ] {
196 _ = properties.insert(name.into(), json!({"type":"string"}));
197 }
198 metadata.request_body = Some(
199 json!({"required":false,"content":{"application/json":{"schema":{"type":"object","properties":properties}}}}),
200 );
201 }
202 "/ok" => {
203 metadata.description = Some("Check if the API is working".into());
204 _ = metadata.responses.insert("200".into(), response("API is working",&(json!({"type":"object","properties":{"ok":{"type":"boolean","description":"Indicates if the API is working"}},"required":["ok"]}))));
205 }
206 "/error" => {
207 metadata.description = Some("Displays an error page".into());
208 _ = metadata.responses.insert("200".into(),json!({"description":"Success","content":{"text/html":{"schema":{"type":"string","description":"The HTML content of the error page"}}}}));
209 }
210 "/get-session" => {
211 metadata.operation_id = Some("getSession".into());
212 metadata.description = Some("Get the current session".into());
213 _ = metadata.responses.insert("200".into(),response("Success",&(json!({"type":["object","null"],"properties":{"session":{"$ref":"#/components/schemas/Session"},"user":{"$ref":"#/components/schemas/User"}},"required":["session","user"]}))));
216 }
217 _ => return None,
218 }
219 Some(metadata)
220}