1use crate::addon::Addon;
2use anyhow::Result;
3use std::path::Path;
4
5pub struct OauthAddon {
6 pub provider: String,
7}
8
9impl Addon for OauthAddon {
10 fn name(&self) -> &str {
11 "oauth"
12 }
13
14 fn check_prerequisites(&self, project_root: &Path) -> Result<()> {
15 super::check_romance_project(project_root)?;
16 super::check_auth_exists(project_root)
17 }
18
19 fn is_already_installed(&self, project_root: &Path) -> bool {
20 project_root.join("backend/src/oauth.rs").exists()
21 }
22
23 fn install(&self, project_root: &Path) -> Result<()> {
24 install_oauth(project_root, &self.provider)
25 }
26
27 fn uninstall(&self, project_root: &Path) -> Result<()> {
28 use colored::Colorize;
29
30 println!("{}", "Uninstalling OAuth...".bold());
31
32 if super::remove_file_if_exists(&project_root.join("backend/src/oauth.rs"))? {
34 println!(" {} backend/src/oauth.rs", "delete".red());
35 }
36 if super::remove_file_if_exists(&project_root.join("backend/src/handlers/oauth.rs"))? {
37 println!(" {} backend/src/handlers/oauth.rs", "delete".red());
38 }
39 if super::remove_file_if_exists(&project_root.join("backend/src/routes/oauth.rs"))? {
40 println!(" {} backend/src/routes/oauth.rs", "delete".red());
41 }
42 if super::remove_file_if_exists(
43 &project_root.join("frontend/src/features/auth/OAuthButton.tsx"),
44 )? {
45 println!(
46 " {} frontend/src/features/auth/OAuthButton.tsx",
47 "delete".red()
48 );
49 }
50
51 super::remove_mod_from_main(project_root, "oauth")?;
53
54 super::remove_line_from_file(
56 &project_root.join("backend/src/handlers/mod.rs"),
57 "pub mod oauth;",
58 )?;
59
60 super::remove_line_from_file(
62 &project_root.join("backend/src/routes/mod.rs"),
63 "pub mod oauth;",
64 )?;
65 super::remove_line_from_file(
66 &project_root.join("backend/src/routes/mod.rs"),
67 ".merge(oauth::router())",
68 )?;
69
70 crate::ai_context::regenerate(project_root).ok();
72
73 println!();
74 println!("{}", "OAuth uninstalled successfully.".green().bold());
75
76 Ok(())
77 }
78
79 fn dependencies(&self) -> Vec<&str> {
80 vec!["auth"]
81 }
82}
83
84fn install_oauth(project_root: &Path, provider: &str) -> Result<()> {
85 use crate::template::TemplateEngine;
86 use crate::utils;
87 use colored::Colorize;
88 use heck::ToPascalCase;
89 use tera::Context;
90
91 let valid_providers = ["google", "github", "discord"];
92 if !valid_providers.contains(&provider) {
93 anyhow::bail!(
94 "Unsupported OAuth provider '{}'. Supported: {}",
95 provider,
96 valid_providers.join(", ")
97 );
98 }
99
100 println!(
101 "{}",
102 format!("Installing OAuth ({})...", provider).bold()
103 );
104
105 let engine = TemplateEngine::new()?;
106 let timestamp = crate::generator::migration::next_timestamp();
107
108 let mut ctx = Context::new();
109 ctx.insert("provider", provider);
110 ctx.insert("provider_pascal", &provider.to_pascal_case());
111 ctx.insert("timestamp", ×tamp);
112
113 let content = engine.render("addon/oauth/oauth.rs.tera", &ctx)?;
115 utils::write_file(&project_root.join("backend/src/oauth.rs"), &content)?;
116 println!(" {} backend/src/oauth.rs", "create".green());
117
118 let content = engine.render("addon/oauth/oauth_handlers.rs.tera", &ctx)?;
120 utils::write_file(
121 &project_root.join("backend/src/handlers/oauth.rs"),
122 &content,
123 )?;
124 println!(" {} backend/src/handlers/oauth.rs", "create".green());
125
126 let content = engine.render("addon/oauth/oauth_routes.rs.tera", &ctx)?;
128 utils::write_file(
129 &project_root.join("backend/src/routes/oauth.rs"),
130 &content,
131 )?;
132 println!(" {} backend/src/routes/oauth.rs", "create".green());
133
134 let content = engine.render("addon/oauth/oauth_migration.rs.tera", &ctx)?;
136 let migration_module = format!("m{}_add_oauth_to_users", timestamp);
137 utils::write_file(
138 &project_root.join(format!("backend/migration/src/{}.rs", migration_module)),
139 &content,
140 )?;
141 println!(
142 " {} backend/migration/src/{}.rs",
143 "create".green(),
144 migration_module
145 );
146
147 let content = engine.render("addon/oauth/OAuthButton.tsx.tera", &ctx)?;
149 utils::write_file(
150 &project_root.join("frontend/src/features/auth/OAuthButton.tsx"),
151 &content,
152 )?;
153 println!(
154 " {} frontend/src/features/auth/OAuthButton.tsx",
155 "create".green()
156 );
157
158 let mods_marker = "// === ROMANCE:MODS ===";
160 utils::insert_at_marker(
161 &project_root.join("backend/src/handlers/mod.rs"),
162 mods_marker,
163 "pub mod oauth;",
164 )?;
165 utils::insert_at_marker(
166 &project_root.join("backend/src/routes/mod.rs"),
167 mods_marker,
168 "pub mod oauth;",
169 )?;
170 utils::insert_at_marker(
171 &project_root.join("backend/src/routes/mod.rs"),
172 "// === ROMANCE:ROUTES ===",
173 " .merge(oauth::router())",
174 )?;
175
176 let lib_path = project_root.join("backend/migration/src/lib.rs");
178 utils::insert_at_marker(
179 &lib_path,
180 "// === ROMANCE:MIGRATION_MODS ===",
181 &format!("mod {};", migration_module),
182 )?;
183 utils::insert_at_marker(
184 &lib_path,
185 "// === ROMANCE:MIGRATIONS ===",
186 &format!(" Box::new({}::Migration),", migration_module),
187 )?;
188
189 let user_model_path = project_root.join("backend/src/entities/user.rs");
191 if user_model_path.exists() {
192 let mut user_content = std::fs::read_to_string(&user_model_path)?;
193 if !user_content.contains("oauth_provider") {
194 if let Some(model_pos) = user_content.find("pub struct Model") {
196 if let Some(rel_pos) = user_content[model_pos..].find(" pub created_at:") {
197 let insert_pos = model_pos + rel_pos;
198 user_content.insert_str(insert_pos, " pub oauth_provider: Option<String>,\n pub oauth_id: Option<String>,\n");
199 }
200 }
201 if let Some(up_pos) = user_content.find("pub struct UserPublic") {
203 if let Some(rel_pos) = user_content[up_pos..].find(" pub created_at:") {
204 let insert_pos = up_pos + rel_pos;
205 user_content.insert_str(insert_pos, " pub oauth_provider: Option<String>,\n pub oauth_id: Option<String>,\n");
206 }
207 }
208 std::fs::write(&user_model_path, user_content)?;
209 println!(" {} backend/src/entities/user.rs (added oauth fields)", "update".green());
210 }
211 }
212
213 let auth_handlers_path = project_root.join("backend/src/handlers/auth.rs");
215 if auth_handlers_path.exists() {
216 let mut auth_content = std::fs::read_to_string(&auth_handlers_path)?;
217 if !auth_content.contains("..Default::default()") {
219 if let Some(pos) = auth_content.find("created_at: Set(now),\n updated_at: Set(now),\n };") {
222 let insert_pos = pos + "created_at: Set(now),\n updated_at: Set(now),\n".len();
223 auth_content.insert_str(insert_pos, " ..Default::default()\n");
224 } else if let Some(pos) = auth_content.find("updated_at: Set(now),\n };") {
225 let insert_pos = pos + "updated_at: Set(now),\n".len();
226 auth_content.insert_str(insert_pos, " ..Default::default()\n");
227 }
228 }
229 if !auth_content.contains("oauth_provider:") {
231 auth_content = auth_content.replace(
234 " created_at: created.created_at,",
235 " oauth_provider: created.oauth_provider,\n oauth_id: created.oauth_id,\n created_at: created.created_at,",
236 );
237 auth_content = auth_content.replace(
238 " created_at: user.created_at,",
239 " oauth_provider: user.oauth_provider.clone(),\n oauth_id: user.oauth_id.clone(),\n created_at: user.created_at,",
240 );
241 auth_content = auth_content.replace(
242 " created_at: updated.created_at,",
243 " oauth_provider: updated.oauth_provider,\n oauth_id: updated.oauth_id,\n created_at: updated.created_at,",
244 );
245 auth_content = auth_content.replace(
246 " created_at: u.created_at,",
247 " oauth_provider: u.oauth_provider,\n oauth_id: u.oauth_id,\n created_at: u.created_at,",
248 );
249 }
250 std::fs::write(&auth_handlers_path, auth_content)?;
251 }
252
253 super::add_mod_to_main(project_root, "oauth")?;
255
256 crate::generator::auth::insert_cargo_dependency(
258 &project_root.join("backend/Cargo.toml"),
259 &[
260 ("oauth2", r#"{ version = "4", features = ["reqwest"] }"#),
261 ("reqwest", r#"{ version = "0.12", features = ["json"] }"#),
262 ],
263 )?;
264
265 let provider_upper = provider.to_uppercase();
267 super::append_env_var(
268 &project_root.join("backend/.env"),
269 &format!("{}_CLIENT_ID=your-client-id", provider_upper),
270 )?;
271 super::append_env_var(
272 &project_root.join("backend/.env"),
273 &format!("{}_CLIENT_SECRET=your-client-secret", provider_upper),
274 )?;
275 super::append_env_var(
276 &project_root.join("backend/.env.example"),
277 &format!("{}_CLIENT_ID=your-client-id", provider_upper),
278 )?;
279 super::append_env_var(
280 &project_root.join("backend/.env.example"),
281 &format!("{}_CLIENT_SECRET=your-client-secret", provider_upper),
282 )?;
283
284 println!();
285 println!(
286 "{}",
287 format!("OAuth ({}) installed successfully!", provider)
288 .green()
289 .bold()
290 );
291 println!(
292 " Set {}_CLIENT_ID and {}_CLIENT_SECRET in backend/.env",
293 provider_upper, provider_upper
294 );
295
296 Ok(())
297}