Hamzavelous commited on
Commit
aa8d4f4
·
1 Parent(s): 6678b68

Deploy Echo Sense full-stack app to Docker Space

Browse files

React frontend + Flask API on port 7860 with SQLite and embedded ChromaDB.

README.md CHANGED
@@ -39,13 +39,12 @@ Add these in **Settings → Repository secrets** on Hugging Face:
39
 
40
  ## Demo accounts
41
 
42
- Register a patient from the app. Create a counselor via API:
43
 
44
- ```bash
45
- curl -X POST https://hamzavelous-echosense-ai.hf.space/api/auth/register-counselor \
46
- -H "Content-Type: application/json" \
47
- -d '{"full_name":"Dr. Smith","email":"counselor@echo.local","password":"counselor123"}'
48
- ```
49
 
50
  ## Local development
51
 
 
39
 
40
  ## Demo accounts
41
 
42
+ Register a **patient** or **counselor** from the login page:
43
 
44
+ - **Patient:** Login → *Create patient account* → `/register`
45
+ - **Counselor:** Login → *Register as counselor* → `/register/counselor`
46
+
47
+ Pre-seeded admin (from container startup): `admin@echo.local` / `admin123`
 
48
 
49
  ## Local development
50
 
backend/app/modules/auth/routes.py CHANGED
@@ -102,14 +102,22 @@ def refresh():
102
 
103
 
104
  @auth_bp.post("/register-counselor")
105
- @limiter.limit("5 per hour")
106
  def register_counselor():
107
- """Bootstrap endpoint for demo — create counselor accounts."""
108
  data = request.get_json() or {}
109
  if not all(data.get(f) for f in ["full_name", "email", "password"]):
110
- return jsonify({"error": "Missing fields"}), 400
 
 
 
 
 
 
 
 
 
111
  if User.query.filter_by(email=data["email"].lower()).first():
112
- return jsonify({"error": "Email exists"}), 409
113
 
114
  user = User(
115
  email=data["email"].lower(),
@@ -122,11 +130,22 @@ def register_counselor():
122
  user_id=user.id,
123
  counselor_id=generate_counselor_id(),
124
  full_name=data["full_name"],
125
- phone=data.get("phone"),
126
- specialization=data.get("specialization", "General Counseling"),
127
  )
128
  db.session.add(counselor)
 
129
  db.session.commit()
 
130
  access = create_access_token(identity=str(user.id), additional_claims={"role": user.role.value})
131
  refresh = create_refresh_token(identity=str(user.id))
132
- return jsonify({"access_token": access, "refresh_token": refresh}), 201
 
 
 
 
 
 
 
 
 
 
102
 
103
 
104
  @auth_bp.post("/register-counselor")
105
+ @limiter.limit("10 per hour")
106
  def register_counselor():
 
107
  data = request.get_json() or {}
108
  if not all(data.get(f) for f in ["full_name", "email", "password"]):
109
+ return jsonify({"error": "Missing required fields"}), 400
110
+
111
+ phone = (data.get("phone") or "").strip() or None
112
+ if phone:
113
+ phone_digits = "".join(c for c in phone if c.isdigit())
114
+ if len(phone_digits) < 10:
115
+ return jsonify({"error": "Invalid phone number"}), 400
116
+ if not phone.startswith("+"):
117
+ phone = f"+{phone_digits}"
118
+
119
  if User.query.filter_by(email=data["email"].lower()).first():
120
+ return jsonify({"error": "Email already registered"}), 409
121
 
122
  user = User(
123
  email=data["email"].lower(),
 
130
  user_id=user.id,
131
  counselor_id=generate_counselor_id(),
132
  full_name=data["full_name"],
133
+ phone=phone,
134
+ specialization=(data.get("specialization") or "General Counseling").strip(),
135
  )
136
  db.session.add(counselor)
137
+ audit_log(user.id, "REGISTER", "counselor", {"counselor_id": counselor.counselor_id})
138
  db.session.commit()
139
+
140
  access = create_access_token(identity=str(user.id), additional_claims={"role": user.role.value})
141
  refresh = create_refresh_token(identity=str(user.id))
142
+ return jsonify({
143
+ "access_token": access,
144
+ "refresh_token": refresh,
145
+ "user": {"id": user.id, "email": user.email, "role": user.role.value},
146
+ "counselor": {
147
+ "counselor_id": counselor.counselor_id,
148
+ "full_name": counselor.full_name,
149
+ "specialization": counselor.specialization,
150
+ },
151
+ }), 201
deploy/hf/README.md CHANGED
@@ -39,13 +39,12 @@ Add these in **Settings → Repository secrets** on Hugging Face:
39
 
40
  ## Demo accounts
41
 
42
- Register a patient from the app. Create a counselor via API:
43
 
44
- ```bash
45
- curl -X POST https://hamzavelous-echosense-ai.hf.space/api/auth/register-counselor \
46
- -H "Content-Type: application/json" \
47
- -d '{"full_name":"Dr. Smith","email":"counselor@echo.local","password":"counselor123"}'
48
- ```
49
 
50
  ## Local development
51
 
 
39
 
40
  ## Demo accounts
41
 
42
+ Register a **patient** or **counselor** from the login page:
43
 
44
+ - **Patient:** Login → *Create patient account* → `/register`
45
+ - **Counselor:** Login → *Register as counselor* → `/register/counselor`
46
+
47
+ Pre-seeded admin (from container startup): `admin@echo.local` / `admin123`
 
48
 
49
  ## Local development
50
 
frontend/src/App.tsx CHANGED
@@ -3,6 +3,7 @@ import { useAuth } from '@/context/AuthContext'
3
  import { homeRouteForRole } from '@/config/navigation'
4
  import Login from '@/pages/Login'
5
  import Register from '@/pages/Register'
 
6
  import PatientChat from '@/pages/PatientChat'
7
  import TherapyPlan from '@/pages/TherapyPlan'
8
  import CopingTools from '@/pages/CopingTools'
@@ -27,6 +28,7 @@ export default function App() {
27
  <Routes>
28
  <Route path="/login" element={<Login />} />
29
  <Route path="/register" element={<Register />} />
 
30
  <Route path="/chat" element={<PrivateRoute roles={['PATIENT']}><PatientChat /></PrivateRoute>} />
31
  <Route path="/journal" element={<PrivateRoute roles={['PATIENT']}><Journal /></PrivateRoute>} />
32
  <Route path="/session-request" element={<PrivateRoute roles={['PATIENT']}><SessionRequestPage /></PrivateRoute>} />
 
3
  import { homeRouteForRole } from '@/config/navigation'
4
  import Login from '@/pages/Login'
5
  import Register from '@/pages/Register'
6
+ import CounselorRegister from '@/pages/CounselorRegister'
7
  import PatientChat from '@/pages/PatientChat'
8
  import TherapyPlan from '@/pages/TherapyPlan'
9
  import CopingTools from '@/pages/CopingTools'
 
28
  <Routes>
29
  <Route path="/login" element={<Login />} />
30
  <Route path="/register" element={<Register />} />
31
+ <Route path="/register/counselor" element={<CounselorRegister />} />
32
  <Route path="/chat" element={<PrivateRoute roles={['PATIENT']}><PatientChat /></PrivateRoute>} />
33
  <Route path="/journal" element={<PrivateRoute roles={['PATIENT']}><Journal /></PrivateRoute>} />
34
  <Route path="/session-request" element={<PrivateRoute roles={['PATIENT']}><SessionRequestPage /></PrivateRoute>} />
frontend/src/context/AuthContext.tsx CHANGED
@@ -13,6 +13,7 @@ interface AuthCtx {
13
  loading: boolean
14
  login: (email: string, password: string) => Promise<unknown>
15
  register: (form: Record<string, unknown>) => Promise<unknown>
 
16
  logout: () => void
17
  setProfile: (p: Record<string, unknown> | null) => void
18
  }
@@ -57,6 +58,17 @@ export function AuthProvider({ children }: { children: ReactNode }) {
57
  return data
58
  }
59
 
 
 
 
 
 
 
 
 
 
 
 
60
  const logout = () => {
61
  localStorage.clear()
62
  setUser(null)
@@ -64,7 +76,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
64
  }
65
 
66
  return (
67
- <AuthContext.Provider value={{ user, profile, loading, login, register, logout, setProfile }}>
68
  {children}
69
  </AuthContext.Provider>
70
  )
 
13
  loading: boolean
14
  login: (email: string, password: string) => Promise<unknown>
15
  register: (form: Record<string, unknown>) => Promise<unknown>
16
+ registerCounselor: (form: Record<string, unknown>) => Promise<unknown>
17
  logout: () => void
18
  setProfile: (p: Record<string, unknown> | null) => void
19
  }
 
58
  return data
59
  }
60
 
61
+ const registerCounselor = async (form: Record<string, unknown>) => {
62
+ const { data } = await api.post('/api/auth/register-counselor', form)
63
+ localStorage.setItem('access_token', data.access_token)
64
+ localStorage.setItem('refresh_token', data.refresh_token)
65
+ localStorage.setItem('user', JSON.stringify(data.user))
66
+ localStorage.setItem('profile', JSON.stringify(data.counselor))
67
+ setUser(data.user)
68
+ setProfile(data.counselor)
69
+ return data
70
+ }
71
+
72
  const logout = () => {
73
  localStorage.clear()
74
  setUser(null)
 
76
  }
77
 
78
  return (
79
+ <AuthContext.Provider value={{ user, profile, loading, login, register, registerCounselor, logout, setProfile }}>
80
  {children}
81
  </AuthContext.Provider>
82
  )
frontend/src/pages/CounselorRegister.tsx ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion } from 'framer-motion'
2
+ import { Link, useNavigate } from 'react-router-dom'
3
+ import { Heart, Stethoscope } from 'lucide-react'
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
5
+ import { Input } from '@/components/ui/input'
6
+ import { Label } from '@/components/ui/label'
7
+ import { Button } from '@/components/ui/button'
8
+ import { Alert } from '@/components/ui/alert'
9
+ import { useState } from 'react'
10
+ import { useAuth } from '@/context/AuthContext'
11
+ import PhoneInput, { formatFullPhone } from '@/components/PhoneInput'
12
+ import { homeRouteForRole } from '@/config/navigation'
13
+
14
+ const SPECIALIZATIONS = [
15
+ 'General Counseling',
16
+ 'Crisis Intervention',
17
+ 'Anxiety & Depression',
18
+ 'Trauma & PTSD',
19
+ 'Youth Counseling',
20
+ 'Family Therapy',
21
+ 'Other',
22
+ ]
23
+
24
+ export default function CounselorRegister() {
25
+ const [form, setForm] = useState({
26
+ full_name: '',
27
+ email: '',
28
+ password: '',
29
+ specialization: SPECIALIZATIONS[0],
30
+ })
31
+ const [phoneDialCode, setPhoneDialCode] = useState('+92')
32
+ const [phoneLocal, setPhoneLocal] = useState('')
33
+ const [error, setError] = useState('')
34
+ const [loading, setLoading] = useState(false)
35
+ const { registerCounselor } = useAuth()
36
+ const navigate = useNavigate()
37
+
38
+ const set = (k: string) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) =>
39
+ setForm({ ...form, [k]: e.target.value })
40
+
41
+ const handleSubmit = async (e: React.FormEvent) => {
42
+ e.preventDefault()
43
+ setError('')
44
+ const phoneDigits = phoneLocal.replace(/\D/g, '')
45
+ if (phoneDigits.length > 0 && phoneDigits.length < 7) {
46
+ setError('Please enter a valid phone number (at least 7 digits), or leave it blank.')
47
+ return
48
+ }
49
+ const phone = phoneDigits.length >= 7 ? formatFullPhone(phoneDialCode, phoneLocal) : undefined
50
+ setLoading(true)
51
+ try {
52
+ const data = await registerCounselor({ ...form, phone }) as { user: { role: string } }
53
+ navigate(homeRouteForRole(data.user.role))
54
+ } catch {
55
+ setError('Registration failed. Email may already be in use.')
56
+ }
57
+ setLoading(false)
58
+ }
59
+
60
+ return (
61
+ <div className="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-calm-50 via-background to-secondary/30">
62
+ <motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} className="w-full max-w-lg">
63
+ <div className="text-center mb-6">
64
+ <div className="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10 mb-2">
65
+ <Stethoscope className="h-6 w-6 text-primary" />
66
+ </div>
67
+ <h1 className="text-xl font-semibold text-calm-900">Counselor registration</h1>
68
+ <p className="text-sm text-muted-foreground">Join Echo Sense as a licensed support professional</p>
69
+ </div>
70
+ <Card className="shadow-lg shadow-calm-900/5">
71
+ <CardHeader>
72
+ <CardTitle>Create counselor account</CardTitle>
73
+ <CardDescription>Access triage, patient navigator, and decision workspace tools</CardDescription>
74
+ </CardHeader>
75
+ <CardContent>
76
+ {error && <Alert className="mb-4">{error}</Alert>}
77
+ <form onSubmit={handleSubmit} className="grid gap-3 sm:grid-cols-2">
78
+ <div className="space-y-2 sm:col-span-2">
79
+ <Label>Full name</Label>
80
+ <Input value={form.full_name} onChange={set('full_name')} required placeholder="Dr. Jane Smith" />
81
+ </div>
82
+ <div className="space-y-2 sm:col-span-2">
83
+ <Label>Email</Label>
84
+ <Input type="email" value={form.email} onChange={set('email')} required placeholder="counselor@clinic.com" />
85
+ </div>
86
+ <div className="space-y-2 sm:col-span-2">
87
+ <Label>Password</Label>
88
+ <Input type="password" value={form.password} onChange={set('password')} required minLength={6} />
89
+ </div>
90
+ <div className="space-y-2 sm:col-span-2">
91
+ <Label htmlFor="counselor-phone">Phone (optional)</Label>
92
+ <PhoneInput
93
+ id="counselor-phone"
94
+ dialCode={phoneDialCode}
95
+ localNumber={phoneLocal}
96
+ onDialCodeChange={setPhoneDialCode}
97
+ onLocalNumberChange={setPhoneLocal}
98
+ />
99
+ </div>
100
+ <div className="space-y-2 sm:col-span-2">
101
+ <Label>Specialization</Label>
102
+ <select
103
+ className="flex h-10 w-full rounded-md border border-input bg-background px-3 text-sm"
104
+ value={form.specialization}
105
+ onChange={set('specialization')}
106
+ >
107
+ {SPECIALIZATIONS.map((s) => (
108
+ <option key={s} value={s}>{s}</option>
109
+ ))}
110
+ </select>
111
+ </div>
112
+ <Button type="submit" className="sm:col-span-2 mt-2" disabled={loading}>
113
+ {loading ? 'Creating account...' : 'Register as counselor'}
114
+ </Button>
115
+ </form>
116
+ <p className="text-center text-sm text-muted-foreground mt-4">
117
+ Already registered?{' '}
118
+ <Link to="/login" className="text-primary hover:underline">Sign in</Link>
119
+ </p>
120
+ <p className="text-center text-sm text-muted-foreground mt-2">
121
+ Registering as a patient?{' '}
122
+ <Link to="/register" className="text-primary hover:underline inline-flex items-center gap-1">
123
+ <Heart className="h-3.5 w-3.5" /> Patient sign up
124
+ </Link>
125
+ </p>
126
+ </CardContent>
127
+ </Card>
128
+ </motion.div>
129
+ </div>
130
+ )
131
+ }
frontend/src/pages/Login.tsx CHANGED
@@ -79,10 +79,16 @@ export default function Login() {
79
  {loading ? 'Signing in...' : 'Sign in'}
80
  </Button>
81
  </form>
82
- <p className="text-center text-sm text-muted-foreground mt-6">
83
- New patient?{' '}
84
- <Link to="/register" className="text-primary font-medium hover:underline">Create account</Link>
85
- </p>
 
 
 
 
 
 
86
  </CardContent>
87
  </Card>
88
  </motion.div>
 
79
  {loading ? 'Signing in...' : 'Sign in'}
80
  </Button>
81
  </form>
82
+ <div className="text-center text-sm text-muted-foreground mt-6 space-y-2">
83
+ <p>
84
+ New patient?{' '}
85
+ <Link to="/register" className="text-primary font-medium hover:underline">Create patient account</Link>
86
+ </p>
87
+ <p>
88
+ Counselor?{' '}
89
+ <Link to="/register/counselor" className="text-primary font-medium hover:underline">Register as counselor</Link>
90
+ </p>
91
+ </div>
92
  </CardContent>
93
  </Card>
94
  </motion.div>
frontend/src/pages/Register.tsx CHANGED
@@ -107,6 +107,10 @@ export default function Register() {
107
  <p className="text-center text-sm text-muted-foreground mt-4">
108
  Already registered? <Link to="/login" className="text-primary hover:underline">Sign in</Link>
109
  </p>
 
 
 
 
110
  </CardContent>
111
  </Card>
112
  </motion.div>
 
107
  <p className="text-center text-sm text-muted-foreground mt-4">
108
  Already registered? <Link to="/login" className="text-primary hover:underline">Sign in</Link>
109
  </p>
110
+ <p className="text-center text-sm text-muted-foreground mt-2">
111
+ Registering as a counselor?{' '}
112
+ <Link to="/register/counselor" className="text-primary hover:underline">Counselor sign up</Link>
113
+ </p>
114
  </CardContent>
115
  </Card>
116
  </motion.div>