ferro_cli/templates/
auth.rs1pub fn auth_migration_template() -> String {
9 r#"use sea_orm_migration::prelude::*;
10
11#[derive(DeriveMigrationName)]
12pub struct Migration;
13
14#[async_trait::async_trait]
15impl MigrationTrait for Migration {
16 async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
17 // Add auth fields to existing users table
18 manager
19 .alter_table(
20 Table::alter()
21 .table(Users::Table)
22 .add_column(ColumnDef::new(Users::Name).string().not_null().default(""))
23 .add_column(ColumnDef::new(Users::Email).string().not_null().default(""))
24 .add_column(ColumnDef::new(Users::Password).string().not_null().default(""))
25 .add_column(ColumnDef::new(Users::RememberToken).string().null())
26 .to_owned(),
27 )
28 .await?;
29
30 // Add unique index on email
31 manager
32 .create_index(
33 Index::create()
34 .name("idx_users_email_unique")
35 .table(Users::Table)
36 .col(Users::Email)
37 .unique()
38 .to_owned(),
39 )
40 .await?;
41
42 Ok(())
43 }
44
45 async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
46 manager
47 .drop_index(
48 Index::drop()
49 .name("idx_users_email_unique")
50 .table(Users::Table)
51 .to_owned(),
52 )
53 .await?;
54
55 manager
56 .alter_table(
57 Table::alter()
58 .table(Users::Table)
59 .drop_column(Users::Name)
60 .drop_column(Users::Email)
61 .drop_column(Users::Password)
62 .drop_column(Users::RememberToken)
63 .to_owned(),
64 )
65 .await?;
66
67 Ok(())
68 }
69}
70
71#[derive(DeriveIden)]
72enum Users {
73 Table,
74 Name,
75 Email,
76 Password,
77 RememberToken,
78}
79"#
80 .to_string()
81}
82
83pub fn auth_controller_template() -> String {
85 r#"//! Authentication controller
86//!
87//! Handles user registration, login, and logout.
88//!
89//! Tip: Use AuthUser<user::Model> to auto-extract the authenticated user:
90//!
91//! use ferro::AuthUser;
92//!
93//! #[handler]
94//! pub async fn profile(user: AuthUser<user::Model>) -> Response {
95//! Ok(HttpResponse::json(serde_json::json!({"user": user.name})))
96//! }
97
98use ferro::database::ModelMut;
99use ferro::http::{HttpResponse, Request, Response};
100use ferro::{handler, hash, json_response, rules, verify};
101use ferro::{Auth, Validator, required, string, email, min};
102use ferro::ActiveValue;
103use serde::Deserialize;
104
105use crate::models::user;
106
107#[derive(Deserialize)]
108struct RegisterInput {
109 name: String,
110 email: String,
111 password: String,
112 password_confirmation: String,
113}
114
115#[derive(Deserialize)]
116struct LoginInput {
117 email: String,
118 password: String,
119}
120
121/// Register a new user
122#[handler]
123pub async fn register(req: Request) -> Response {
124 let input: RegisterInput = req.input().await.map_err(|_| {
125 HttpResponse::json(serde_json::json!({
126 "message": "Invalid request body."
127 }))
128 .status(422)
129 })?;
130
131 // Validate input
132 let data = serde_json::json!({
133 "name": input.name,
134 "email": input.email,
135 "password": input.password,
136 "password_confirmation": input.password_confirmation,
137 });
138
139 let mut validator = Validator::new(&data)
140 .rules("name", rules![required(), string()])
141 .rules("email", rules![required(), email()])
142 .rules("password", rules![required(), string(), min(8)]);
143
144 // Check password confirmation
145 if input.password != input.password_confirmation {
146 validator = validator.with_error("password_confirmation", "Passwords do not match.");
147 }
148
149 // Check email uniqueness
150 if let Some(_existing) = user::Model::find_by_email(&input.email).await.map_err(|e| {
151 HttpResponse::json(serde_json::json!({
152 "message": format!("Database error: {}", e)
153 }))
154 .status(500)
155 })? {
156 validator = validator.with_error("email", "This email is already registered.");
157 }
158
159 if let Err(errors) = validator.validate() {
160 return Err(HttpResponse::json(serde_json::json!({
161 "message": "Validation failed.",
162 "errors": errors,
163 }))
164 .status(422));
165 }
166
167 // Hash password
168 let password_hash = hash(&input.password).map_err(|e| {
169 HttpResponse::json(serde_json::json!({
170 "message": format!("Failed to hash password: {}", e)
171 }))
172 .status(500)
173 })?;
174
175 // Create user
176 let user = user::ActiveModel {
177 name: ActiveValue::Set(input.name.clone()),
178 email: ActiveValue::Set(input.email.clone()),
179 password: ActiveValue::Set(password_hash),
180 remember_token: ActiveValue::Set(None),
181 ..Default::default()
182 };
183
184 let user = user::Entity::insert_one(user)
185 .await
186 .map_err(|e| {
187 HttpResponse::json(serde_json::json!({
188 "message": format!("Failed to create user: {}", e)
189 }))
190 .status(500)
191 })?;
192
193 // Log in the new user
194 Auth::login(user.id as i64);
195
196 Ok(HttpResponse::json(serde_json::json!({
197 "user": {
198 "id": user.id,
199 "name": user.name,
200 "email": user.email,
201 }
202 }))
203 .status(201))
204}
205
206/// Log in an existing user
207#[handler]
208pub async fn login(req: Request) -> Response {
209 let input: LoginInput = req.input().await.map_err(|_| {
210 HttpResponse::json(serde_json::json!({
211 "message": "Invalid request body."
212 }))
213 .status(422)
214 })?;
215
216 // Validate input
217 let data = serde_json::json!({
218 "email": input.email,
219 "password": input.password,
220 });
221
222 if let Err(errors) = Validator::new(&data)
223 .rules("email", rules![required(), email()])
224 .rules("password", rules![required()])
225 .validate()
226 {
227 return Err(HttpResponse::json(serde_json::json!({
228 "message": "Validation failed.",
229 "errors": errors,
230 }))
231 .status(422));
232 }
233
234 // Attempt authentication
235 let email = input.email.clone();
236 let password = input.password.clone();
237
238 let result = Auth::attempt(|| async {
239 let user = user::Model::find_by_email(&email).await?;
240 match user {
241 Some(user) => {
242 if verify(&password, &user.password)? {
243 Ok(Some(user.id as i64))
244 } else {
245 Ok(None)
246 }
247 }
248 None => Ok(None),
249 }
250 })
251 .await;
252
253 match result {
254 Ok(Some(_id)) => {
255 // Re-fetch user for response
256 let user = user::Model::find_by_email(&input.email)
257 .await
258 .map_err(|e| {
259 HttpResponse::json(serde_json::json!({
260 "message": format!("Database error: {}", e)
261 }))
262 .status(500)
263 })?;
264
265 match user {
266 Some(user) => json_response!({
267 "user": {
268 "id": user.id,
269 "name": user.name,
270 "email": user.email,
271 }
272 }),
273 None => Err(HttpResponse::json(serde_json::json!({
274 "email": ["These credentials do not match our records."]
275 }))
276 .status(422)),
277 }
278 }
279 Ok(None) => Err(HttpResponse::json(serde_json::json!({
280 "email": ["These credentials do not match our records."]
281 }))
282 .status(422)),
283 Err(e) => Err(HttpResponse::json(serde_json::json!({
284 "message": format!("Authentication error: {}", e)
285 }))
286 .status(500)),
287 }
288}
289
290/// Log out the current user
291#[handler]
292pub async fn logout(_req: Request) -> Response {
293 Auth::logout();
294 json_response!({
295 "message": "Logged out successfully."
296 })
297}
298"#
299 .to_string()
300}