doido_auth/generators/
eject.rs1use super::route_injector::{
19 read_controllers_mod, read_routes, register_auth_controllers_mod, rewire_local_controllers,
20 CONTROLLERS_MOD_PATH, ROUTES_PATH,
21};
22use super::template;
23use super::{AuthGenerator, GeneratedFile};
24use doido_core::Result;
25
26pub struct AuthControllersGenerator;
27
28fn auth_mod(two_factor: bool) -> String {
29 let oauth_module = "mod oauth_controller;\n";
30 let oauth_use = "pub use oauth_controller::OauthController;\n";
31 let (two_factor_module, two_factor_use) = if two_factor {
32 (
33 "mod two_factor_controller;\n",
34 "pub use two_factor_controller::TwoFactorController;\n",
35 )
36 } else {
37 ("", "")
38 };
39 template("auth/mod.rs.template")
40 .replace("{oauth_module}", oauth_module)
41 .replace("{oauth_use}", oauth_use)
42 .replace("{two_factor_module}", two_factor_module)
43 .replace("{two_factor_use}", two_factor_use)
44}
45
46impl AuthGenerator for AuthControllersGenerator {
47 fn name(&self) -> &str {
48 "auth:controllers"
49 }
50
51 fn generate(&self, args: &[&str]) -> Result<Vec<GeneratedFile>> {
52 let api = args.contains(&"--api");
53 let two_factor = args.contains(&"--two-factor");
54 let controllers_only = args.contains(&"--controllers-only");
55 let views_only = args.contains(&"--views-only");
56
57 let emit_controllers = !views_only;
58 let emit_views = !controllers_only && !api;
60
61 let suffix = if api { "api" } else { "html" };
62 let mut files = Vec::new();
63
64 if emit_controllers {
65 files.push(GeneratedFile {
66 path: "app/controllers/auth/mod.rs".to_string(),
67 content: auth_mod(two_factor),
68 });
69 files.push(GeneratedFile {
70 path: "app/controllers/auth/sessions_controller.rs".to_string(),
71 content: template(&format!("auth/sessions_controller_{suffix}.rs.template"))
72 .to_string(),
73 });
74 files.push(GeneratedFile {
75 path: "app/controllers/auth/registrations_controller.rs".to_string(),
76 content: template(&format!(
77 "auth/registrations_controller_{suffix}.rs.template"
78 ))
79 .to_string(),
80 });
81 files.push(GeneratedFile {
82 path: "app/controllers/auth/passwords_controller.rs".to_string(),
83 content: template(&format!("auth/passwords_controller_{suffix}.rs.template"))
84 .to_string(),
85 });
86 files.push(GeneratedFile {
87 path: "app/controllers/auth/oauth_controller.rs".to_string(),
88 content: template("auth/oauth_controller.rs.template").to_string(),
89 });
90 if two_factor {
91 files.push(GeneratedFile {
92 path: "app/controllers/auth/two_factor_controller.rs".to_string(),
93 content: template(&format!("auth/two_factor_controller_{suffix}.rs.template"))
94 .to_string(),
95 });
96 }
97 files.push(GeneratedFile {
98 path: CONTROLLERS_MOD_PATH.to_string(),
99 content: register_auth_controllers_mod(&read_controllers_mod()),
100 });
101 files.push(GeneratedFile {
102 path: ROUTES_PATH.to_string(),
103 content: rewire_local_controllers(&read_routes(), two_factor),
104 });
105 }
106
107 if emit_views {
108 for (file, rel) in [
109 ("sign_in", "auth/views/sign_in.html.tera"),
110 ("sign_up", "auth/views/sign_up.html.tera"),
111 ("password_new", "auth/views/password_new.html.tera"),
112 ("password_edit", "auth/views/password_edit.html.tera"),
113 ] {
114 files.push(GeneratedFile {
115 path: format!("app/views/auth/{file}.html.tera"),
116 content: template(rel).to_string(),
117 });
118 }
119 if two_factor {
120 files.push(GeneratedFile {
121 path: "app/views/auth/two_factor.html.tera".to_string(),
122 content: template("auth/views/two_factor.html.tera").to_string(),
123 });
124 }
125 }
126
127 Ok(files)
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn ejects_controllers_views_and_rewires_routes() {
137 let files = AuthControllersGenerator.generate(&[]).unwrap();
138 for path in [
139 "app/controllers/auth/mod.rs",
140 "app/controllers/auth/sessions_controller.rs",
141 "app/controllers/auth/registrations_controller.rs",
142 "app/controllers/auth/passwords_controller.rs",
143 "app/controllers/auth/oauth_controller.rs",
144 "app/views/auth/sign_in.html.tera",
145 "app/views/auth/sign_up.html.tera",
146 ] {
147 assert!(
148 files.iter().any(|f| f.path == path),
149 "expected ejected file {path}"
150 );
151 }
152 let controllers_mod = files
153 .iter()
154 .find(|f| f.path == CONTROLLERS_MOD_PATH)
155 .unwrap();
156 assert!(controllers_mod.content.contains("pub mod auth;"));
157 }
158
159 #[test]
160 fn rewires_installed_bare_route_to_local_controllers() {
161 std::fs::create_dir_all("config").ok();
163 std::fs::write(
164 ROUTES_PATH,
165 "use doido::controller::axum;\nuse crate::models::user::Model as User;\n\npub fn router() -> axum::Router {\n doido::auth::routes! {\n auth_routes!(User);\n }\n}\n",
166 )
167 .unwrap();
168
169 let files = AuthControllersGenerator.generate(&[]).unwrap();
170 let routes = files.iter().find(|f| f.path == ROUTES_PATH).unwrap();
171 assert!(routes.content.contains("controllers: {"));
172 assert!(routes
173 .content
174 .contains("sessions: auth::SessionsController"));
175 assert!(routes.content.contains("use crate::controllers::auth;"));
176 assert!(!routes.content.contains("auth_routes!(User);"));
177 assert!(routes
180 .content
181 .contains("use crate::models::user::Model as User;"));
182
183 let _ = std::fs::remove_file(ROUTES_PATH);
184 }
185
186 #[test]
187 fn api_flag_skips_views() {
188 let files = AuthControllersGenerator.generate(&["--api"]).unwrap();
189 assert!(!files.iter().any(|f| f.path.starts_with("app/views/auth/")));
190 assert!(files
191 .iter()
192 .any(|f| f.path == "app/controllers/auth/sessions_controller.rs"));
193 }
194
195 #[test]
196 fn views_only_skips_controllers_and_routes() {
197 let files = AuthControllersGenerator
198 .generate(&["--views-only"])
199 .unwrap();
200 assert!(!files
201 .iter()
202 .any(|f| f.path.starts_with("app/controllers/auth/")));
203 assert!(!files.iter().any(|f| f.path == ROUTES_PATH));
204 assert!(files
205 .iter()
206 .any(|f| f.path == "app/views/auth/sign_in.html.tera"));
207 }
208}