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<users::Model> to auto-extract the authenticated user:
90//!
91//! use ferro::AuthUser;
92//!
93//! #[handler]
94//! pub async fn profile(user: AuthUser<users::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 sea_orm::ActiveValue;
103use serde::Deserialize;
104
105use crate::models::users;
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) = users::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 = users::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 = users::Entity::insert(user)
185 .exec_with_returning(&ferro::database::connection().await)
186 .await
187 .map_err(|e| {
188 HttpResponse::json(serde_json::json!({
189 "message": format!("Failed to create user: {}", e)
190 }))
191 .status(500)
192 })?;
193
194 // Log in the new user
195 Auth::login(user.id as i64);
196
197 Ok(HttpResponse::json(serde_json::json!({
198 "user": {
199 "id": user.id,
200 "name": user.name,
201 "email": user.email,
202 }
203 }))
204 .status(201))
205}
206
207/// Log in an existing user
208#[handler]
209pub async fn login(req: Request) -> Response {
210 let input: LoginInput = req.input().await.map_err(|_| {
211 HttpResponse::json(serde_json::json!({
212 "message": "Invalid request body."
213 }))
214 .status(422)
215 })?;
216
217 // Validate input
218 let data = serde_json::json!({
219 "email": input.email,
220 "password": input.password,
221 });
222
223 if let Err(errors) = Validator::new(&data)
224 .rules("email", rules![required(), email()])
225 .rules("password", rules![required()])
226 .validate()
227 {
228 return Err(HttpResponse::json(serde_json::json!({
229 "message": "Validation failed.",
230 "errors": errors,
231 }))
232 .status(422));
233 }
234
235 // Attempt authentication
236 let email = input.email.clone();
237 let password = input.password.clone();
238
239 let result = Auth::attempt(|| async {
240 let user = users::Model::find_by_email(&email).await?;
241 match user {
242 Some(user) => {
243 if verify(&password, &user.password)? {
244 Ok(Some(user.id as i64))
245 } else {
246 Ok(None)
247 }
248 }
249 None => Ok(None),
250 }
251 })
252 .await;
253
254 match result {
255 Ok(Some(_id)) => {
256 // Re-fetch user for response
257 let user = users::Model::find_by_email(&input.email)
258 .await
259 .map_err(|e| {
260 HttpResponse::json(serde_json::json!({
261 "message": format!("Database error: {}", e)
262 }))
263 .status(500)
264 })?;
265
266 match user {
267 Some(user) => json_response!({
268 "user": {
269 "id": user.id,
270 "name": user.name,
271 "email": user.email,
272 }
273 }),
274 None => Err(HttpResponse::json(serde_json::json!({
275 "email": ["These credentials do not match our records."]
276 }))
277 .status(422)),
278 }
279 }
280 Ok(None) => Err(HttpResponse::json(serde_json::json!({
281 "email": ["These credentials do not match our records."]
282 }))
283 .status(422)),
284 Err(e) => Err(HttpResponse::json(serde_json::json!({
285 "message": format!("Authentication error: {}", e)
286 }))
287 .status(500)),
288 }
289}
290
291/// Log out the current user
292#[handler]
293pub async fn logout(_req: Request) -> Response {
294 Auth::logout();
295 json_response!({
296 "message": "Logged out successfully."
297 })
298}
299"#
300 .to_string()
301}