| קובץ | עכשיו | אחרי Sprint 1 | משימה |
|---|---|---|---|
alembic.ini |
❌ לא קיים | קיים + migrations/ | FAB-32 |
services/auth_service.py |
raise NotImplementedError | 5 פונקציות עובדות | FAB-33, FAB-34 |
api/v1/endpoints/auth.py |
501 Not Implemented | מחוברים ל-service | FAB-35 |
services/email_service.py |
⚠️ logger stub | שולח מייל אמיתי (Mailtrap) | FAB-36 |
api/v1/endpoints/users.py |
✅ GET /users/me עובד | ללא שינוי | — |
api/v1/endpoints/health.py |
✅ עובד | ללא שינוי | — |
| קובץ | עכשיו | אחרי Sprint 1 | משימה |
|---|---|---|---|
shared/components/error-display/ |
❌ לא קיים | קומפוננטה עובדת | FAB-37 |
shared/components/loading-spinner/ |
⚠️ חסר message input | size + message | FAB-37 |
shared/components/confirm-dialog/ |
❌ לא קיים | קומפוננטה עובדת | FAB-38 |
shared/components/file-upload/ |
❌ לא קיים | קומפוננטה עובדת | FAB-39 |
core/services/auth.service.ts |
5 stubs ריקים | 5 פונקציות HTTP אמיתיות | FAB-40 |
core/guards/auth.guard.ts |
🐛 redirect → /home | redirect → /login ✅ | FAB-41 |
core/interceptors/auth.interceptor.ts |
⚠️ JWT בלבד, אין 401 | + auto-refresh על 401 | FAB-41 |
features/auth/login/ |
⚠️ shell ריק | טופס עובד מחובר | FAB-42 |
features/auth/register/ |
⚠️ שלבים ללא forms | 3 שלבים + OTP עובדים | FAB-43 |
features/home/ + layout/header/ |
⚠️ HTML ריק | ניווט לפי תפקיד + header | FAB-44 |
| משימה | Jira | גודל | תלויה ב | הערה |
|---|---|---|---|---|
| 🔴 Alembic Init | FAB-32 | S | — | BLOCKER — יום 1 |
| auth_service: register/OTP | FAB-33 | M | FAB-32 | |
| auth_service: login/refresh | FAB-34 | S | FAB-32 | |
| Wire Auth Endpoints | FAB-35 | S | FAB-33, FAB-34 | |
| Email OTP | FAB-36 | S | FAB-32 | Mailtrap בחינם |
| 🔴 Shared UI Components | FAB-37 | S | — | BLOCKER — יום 1 |
| Confirm Dialog | FAB-38 | S | FAB-37 | |
| File Upload | FAB-39 | M | FAB-37 | |
| 🔴 auth.service.ts | FAB-40 | S | FAB-37 | BLOCKER frontend |
| Guard Fix + Interceptor | FAB-41 | S | FAB-40 | bug חד-שורתי + 401 refresh |
| Login Component | FAB-42 | M | FAB-40 | |
| Register Steps 1–3 | FAB-43 | L | FAB-40, FAB-39 | הכי גדול בספרינט |
| Home + Header | FAB-44 | S | FAB-40 | |
| Register Step 4 (הצהרות) | FAB-45 | S | FAB-43 | BACKLOG — אם יש זמן |
וודאו שהשרת עולה: uvicorn app.main:app --reload → GET /api/v1/health ✅.
Frontend: ng serve → localhost:4200 נפתח. קראו יחד את הטבלה "לפני ואחרי".
מפתחת 1 (Backend): FAB-32 — Alembic init + migration ראשונה + seed.
בסוף: alembic upgrade head עובד, טבלאות קיימות.
מפתחת 2 (Frontend): FAB-37 — error-display + loading-spinner message input. בסוף: שתי הקומפוננטות עובדות standalone עם Signals.
Backend: FAB-33 (register/OTP/resend) + FAB-34 (login/refresh). בדיקה ב-Swagger: register → verify-otp → login עובד.
Frontend: FAB-38 (confirm-dialog) + FAB-39 (file-upload) + FAB-40 (auth.service 5 methods). FAB-40 קטנה מאוד — רק 5 פונקציות HTTP.
Backend: FAB-35 (wire endpoints) + FAB-36 (email Mailtrap). בסוף: flow שלם ב-Swagger.
Frontend: FAB-41 (guard bug + 401 interceptor) + FAB-42 (login component). בסוף: login עובד ומנתב ל-/home.
שתיהן: FAB-43 (register steps 1–3) — הכי גדול. שלב 2 של ההרשמה קורא register() שמפעיל FAB-33. הגיע הזמן לחבר frontend + backend יחד.
במקביל: FAB-44 (home + header) — קטן, ניתן לעשות בסוף יום.
End-to-end: נרשמות כמשתמשת חדשה, מקבלות OTP, מאמתות, נכנסות. FAB-45 אם יש זמן. כל ה-PRs עוברים review של מפתחת שנייה לפני merge.
@Component({
standalone: true, // ← חובה
imports: [...],
...
})
// ✅ נכון:
size = input<'sm'|'md'>('md');
clicked = output<void>();
// ❌ לא נכון (Angular ישן):
@Input() size: string = 'md';
@Output() clicked = new EventEmitter();
// ✅ נכון:
@if (isLoading()) { <app-spinner /> }
@for (item of items(); track item.id) { ... }
// ❌ לא נכון:
<div *ngIf="isLoading">
<li *ngFor="let item of items">
// ✅ נכון (אין circular dependency):
export const authInterceptor: HttpInterceptorFn =
(req, next) => {
const auth = inject(AuthService); // ← בתוך
...
};
// ❌ לא נכון:
const auth = inject(AuthService); // ← בחוץ
alembic.ini לא קייםmigrations/ לא קייםalembic upgrade head רץ ללא שגיאותGET /api/v1/health → {"status":"ok"}cd backend # שלב 1: אתחול Alembic alembic init migrations # שלב 2: עדכון migrations/env.py # מצאו את השורה: target_metadata = None # החליפו ב: from app.db.base import Base target_metadata = Base.metadata # שלב 3: עדכון alembic.ini # מצאו: sqlalchemy.url = driver://user:pass@localhost/dbname # החליפו ב: sqlalchemy.url = sqlite:///./app.db # שלב 4: יצירת migration alembic revision --autogenerate -m "initial" # שלב 5: הרצה alembic upgrade head
alembic upgrade head רץ ללא שגיאותmigrations/versions/GET /api/v1/health מחזיר 200_generate_otp() ו-_create_token() כבר קיימות בקובץ — השתמשו בהן.
get_password_hash() קיים ב-core/security.py.
backend/app/services/auth_service.pyמחפשות raise NotImplementedError ומחליפות ב-לוגיקה האמיתית.
def register(db: Session, data: RegisterRequest) -> dict:
# 1. בדוק שהמייל לא קיים
if db.query(User).filter(User.email == data.email).first():
raise HTTPException(400, "Email already registered")
# 2. צור OTP
otp = _generate_otp()
otp_expires = datetime.utcnow() + timedelta(minutes=settings.OTP_EXPIRE_MINUTES)
# 3. שמור משתמש
user = User(
email=data.email,
password_hash=get_password_hash(data.password),
display_name=data.display_name,
otp_code=otp,
otp_expires_at=otp_expires,
account_status=AccountStatus.PENDING_OTP,
)
db.add(user)
db.commit()
# 4. שלח OTP במייל
email_service.send_otp(user.email, otp)
return {"message": "OTP sent", "email": user.email}
def verify_otp(db: Session, data: OtpVerifyRequest) -> dict:
user = db.query(User).filter(User.email == data.email).first()
if not user:
raise HTTPException(404, "User not found")
if user.otp_code != data.otp_code:
raise HTTPException(400, "Invalid OTP")
if user.otp_expires_at < datetime.utcnow():
raise HTTPException(400, "OTP expired")
user.account_status = AccountStatus.PENDING_APPROVAL
user.otp_code = None
db.commit()
return {"message": "Email verified"}
POST /api/v1/auth/register — משתמש נוצר ב-DB עם otp_codePOST /api/v1/auth/verify-otp — סטטוס משתנה ל-PENDING_APPROVALPOST /api/v1/auth/resend-otp — OTP חדש נוצרverify_password() ו-decode_token() קיימים ב-core/security.py.
_create_token() קיים ב-auth_service — access 15 דק, refresh 7 ימים.
def login(db: Session, data: LoginRequest) -> TokenResponse:
user = db.query(User).filter(User.email == data.email).first()
if not user or not verify_password(data.password, user.password_hash):
raise HTTPException(401, "Invalid credentials")
if user.account_status == AccountStatus.PENDING_OTP:
raise HTTPException(403, "Please verify your email first")
access = _create_token(str(user.id), "access", minutes=15)
refresh = _create_token(str(user.id), "refresh", days=7)
return TokenResponse(access_token=access, refresh_token=refresh)
POST /api/v1/auth/login → מחזיר access_token + refresh_tokenPOST /api/v1/auth/refresh → access_token חדשbackend/app/api/v1/endpoints/auth.pyכל endpoint מחזיר כרגע raise HTTPException(status_code=501). מחברות ל-service:
@router.post("/register")
def register(data: RegisterRequest, db: Session = Depends(get_db)):
return auth_service.register(db=db, data=data)
@router.post("/verify-otp")
def verify_otp(data: OtpVerifyRequest, db: Session = Depends(get_db)):
return auth_service.verify_otp(db=db, data=data)
@router.post("/login", response_model=TokenResponse)
def login(data: LoginRequest, db: Session = Depends(get_db)):
return auth_service.login(db=db, data=data)
# + resend-otp, refresh
SMTP_HOST ריק → OTP מודפס ל-console. זה בכוונה!
לבדיקה אמיתית — הירשמו ל-Mailtrap (חינם).
# backend/.env SMTP_HOST=sandbox.smtp.mailtrap.io SMTP_PORT=2525 SMTP_USER=<מ-Mailtrap> SMTP_PASSWORD=<מ-Mailtrap> SMTP_FROM=noreply@anu-banecha.local
backend/app/services/email_service.pydef send_otp(to_email: str, otp: str) -> None:
if not settings.SMTP_HOST:
# dev fallback — intentional
logger.info(f"[DEV EMAIL] OTP for {to_email}: {otp}")
return
msg = MIMEMultipart()
msg["From"] = settings.SMTP_FROM
msg["To"] = to_email
msg["Subject"] = "קוד האימות שלך — אנו בניך"
msg.attach(MIMEText(f"קוד האימות שלך: {otp}\nתוקף: 10 דקות.", "plain"))
with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT) as server:
server.login(settings.SMTP_USER, settings.SMTP_PASSWORD)
server.sendmail(settings.SMTP_FROM, to_email, msg.as_string())
error-display לא קייםloading-spinner קיים אבל חסר message input<app-error-display message="שגיאה!" /> עובד<app-loading-spinner message="טוען..." /> עובד// frontend/src/app/shared/components/error-display/error-display.component.ts
@Component({
standalone: true,
selector: 'app-error-display',
template: `
@if (message()) {
<div class="error-box">⚠️ {{ message() }}</div>
}
`,
styles: [`
.error-box {
background: #fff5f5; border: 1px solid #fc8181;
color: #c53030; border-radius: 8px;
padding: 12px 16px; font-size: 14px;
}
`]
})
export class ErrorDisplayComponent {
message = input<string>('');
}
// frontend/src/app/shared/components/loading-spinner/loading-spinner.component.ts
// הוסיפו ל-class הקיים — אל תמחקו את size:
message = input<string>('');
/* בtemplate — הוסיפו מתחת לאייקון הספינר: */
@if (message()) {
<p class="spinner-message">{{ message() }}</p>
}
error-display קיים, standalone, מקבל message signal inputloading-spinner מקבל גם size וגם message@Input() בשום מקום — רק input()*ngIf — רק @iftitle = input<string>('האם אתה בטוחה?');
message = input<string>('');
confirmLabel = input<string>('אישור');
cancelLabel = input<string>('ביטול');
confirmed = output<void>();
cancelled = output<void>();
confirmedcancelledacceptedTypes = input<string>('image/*,.pdf');
maxSizeMB = input<number>(5);
label = input<string>('בחרי קובץ');
fileSelected = output<File>();
uploadError = output<string>();
uploadError.emit("הקובץ גדול מ-5MB")uploadError.emit("סוג קובץ לא נתמך")refreshToken() ו-loadCurrentUser() כבר מומשו — אל תגעו בהן.
רק 5 הפונקציות האלה הן stubs.
frontend/src/app/core/services/auth.service.ts| פונקציה | מה עושה |
|---|---|
register(data) | POST /api/v1/auth/register |
verifyOtp(data) | POST /api/v1/auth/verify-otp |
login(data) | POST /api/v1/auth/login + saveTokens() |
logout() | clearTokens() + navigate('/login') |
resendOtp(email) | POST /api/v1/auth/resend-otp |
login(data: LoginRequest): Observable<TokenResponse> {
return this.apiService.post<TokenResponse>('/api/v1/auth/login', data).pipe(
tap(tokens => this.saveTokens(tokens))
);
}
logout(): void {
this.clearTokens();
this.router.navigate(['/login']);
}
login() שומר tokens → getAccessToken() מחזיר אותםlogout() מנקה + מנתב ל-/login/home 🐛/login ✅// ❌ לפני: return this.router.createUrlTree(['/home']); // ✅ אחרי: return this.router.createUrlTree(['/login']);
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService); // ← inject בתוך הפונקציה!
const token = auth.getAccessToken();
const authReq = token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req;
return next(authReq).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 401) {
return auth.refreshToken().pipe(
switchMap(() => next(req.clone({
setHeaders: { Authorization: `Bearer ${auth.getAccessToken()}` }
})))
);
}
return throwError(() => err);
})
);
};
/forum ללא token → redirect ל-/login (לא /home)inject() נמצא בתוך הפונקציה — לא ברמת המודולform, isLoading, errorMessage כבר קיימים ב-shell.
צריך לממש את onSubmit() ולכתוב את ה-HTML.
onSubmit(): void {
if (this.form.invalid) return;
this.isLoading.set(true);
this.errorMessage.set('');
this.authService.login(this.form.value as LoginRequest).subscribe({
next: () => this.router.navigate(['/home']),
error: (e: HttpErrorResponse) => {
this.errorMessage.set(e.error?.detail ?? 'שגיאה בהתחברות');
this.isLoading.set(false);
}
});
}
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<h1>כניסה</h1>
<input formControlName="email" type="email" placeholder="אימייל" />
<input formControlName="password" type="password" placeholder="סיסמה" />
<app-error-display [message]="errorMessage()" />
@if (isLoading()) {
<app-loading-spinner message="מתחברת..." />
} @else {
<button type="submit" [disabled]="form.invalid">כניסה</button>
}
<a routerLink="/register">אין לך חשבון? הירשמי</a>
</form>
/homeapp-error-display מציג שגיאהcurrentStep, nextStep(), prevStep().
צריך להוסיף forms ו-service calls.
| שלב | שדות | מה קורה בסוף |
|---|---|---|
| שלב 1 | שם מלא, ת"ז, תאריך לידה, מגדר, מגזר, סוג (אלמן/נה/יתום) | nextStep() → שלב 2 |
| שלב 2 | אימייל + טלפון | register() → OTP נשלח → מסך OTP |
| OTP | קוד 6 ספרות | verifyOtp() → שלב 3 |
| שלב 3 | 3 מסמכים (תעודת פטירה, selfie, ת"ז) — <app-file-upload> |
שמירה → navigate('/home') |
onStep2Submit(): void {
this.isLoading.set(true);
this.authService.register(this.buildPayload()).subscribe({
next: () => this.currentStep.set('otp'), // → מסך OTP
error: (e) => {
this.errorMessage.set(e.error?.detail ?? 'שגיאה');
this.isLoading.set(false);
}
});
}
verifyOtp() → מגיע לשלב 3user = this.authService.currentUser();
@if (user()?.role === 'Admin') {
<button routerLink="/admin">לוח ניהול</button>
} @else if (user()?.role === 'Moderator') {
<button routerLink="/moderator">דיווחים ממתינים</button>
} @else {
<button routerLink="/forum">לפורום</button>
}
<header>
<span class="logo">אנו בניך</span>
<span class="username">{{ user()?.display_name }}</span>
<button (click)="authService.logout()">יציאה</button>
</header>
שלב 4 של ההרשמה: שלושה checkboxes:
כפתור "סיום" פעיל רק כשכל שלושה מסומנים.
RegisterRequest schema. FAB-43 שולחת את אותו payload מה-frontend.
5 דקות לוודא שה-fields תואמים — חוסכות שעות של debugging.
<app-file-upload> שנבנה ב-FAB-39.
וודאו ש-FAB-39 מוזגה לפני שמתחילות את שלב 3 של FAB-43.
| פיצ'ר | מצב | ספרינט |
|---|---|---|
| אישור הרשמה ע"י Admin | Sprint 2 | Sprint 2 |
| Dockerfiles + CI/CD | Sprint 2 | Sprint 2 |
| פורום | Sprint 3 | Sprint 3 |
| ייעוץ מקצועי + מודרציה | Sprint 4 | Sprint 4 |
| הודעות פרטיות | Sprint 5 | Sprint 5 |
alembic upgrade head רץ ללא שגיאותGET /api/v1/health → 200POST /api/v1/auth/register → משתמש נוצר + OTP נשלחPOST /api/v1/auth/verify-otp → סטטוס PENDING_APPROVALPOST /api/v1/auth/resend-otp → OTP חדשPOST /api/v1/auth/login → access_token + refresh_tokenPOST /api/v1/auth/refresh → access_token חדשGET /api/v1/users/me עם Authorization header → פרטי משתמש/login (לא /home!)app-error-display — מציג קופסת שגיאה עם messageapp-loading-spinner — מציג ספינר + messageapp-confirm-dialog — overlay + confirmed/cancelled eventsapp-file-upload — preview + ולידציה גודל + סוגng build עובר ללא שגיאות TypeScriptng lint עובר ללא שגיאותruff check . עובר על Backend@Input() / @Output() — רק input() / output()*ngIf / *ngFor — רק @if / @forstandalone: true| ספרינט | נושא | מה בונים |
|---|---|---|
| Sprint 2 | אישור + ענן | Admin אישור הרשמה (dual-approval), Dockerfiles, CI/CD, PostgreSQL |
| Sprint 3 | פורום | פוסטים + סינון קבוצה/מגזר + דיווח 3 שלבי |
| Sprint 4 | ייעוץ + מודרציה | אנשי מקצוע + מבקרים + חריגות אוטומטיות |
| Sprint 5 | הודעות + סיום | הודעות פרטיות + Audit Log + RTL polish + מחיקת חשבון |