feat: enhance admin setup with email validation and update workflows for fresh lib cloning

pull/10/head
David Montero Crespo 2026-03-07 00:00:22 -03:00
parent 290b149855
commit 41d8e25843
5 changed files with 26 additions and 14 deletions

View File

@ -13,14 +13,21 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: # Do NOT use submodules: recursive — the submodule pointers in this repo
submodules: recursive # are stale and predate package.json. We clone the libs fresh below.
- name: Setup Node.js 22 - name: Setup Node.js 22
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '22' node-version: '22'
# Clone wokwi-libs fresh (stale submodule pointers can't be used)
- name: Clone wokwi-libs
run: |
git clone --depth=1 https://github.com/wokwi/avr8js.git wokwi-libs/avr8js
git clone --depth=1 https://github.com/wokwi/rp2040js.git wokwi-libs/rp2040js
git clone --depth=1 https://github.com/wokwi/wokwi-elements.git wokwi-libs/wokwi-elements
# Cache wokwi-libs node_modules to speed up repeated runs # Cache wokwi-libs node_modules to speed up repeated runs
- name: Cache wokwi-libs node_modules - name: Cache wokwi-libs node_modules
uses: actions/cache@v4 uses: actions/cache@v4

View File

@ -46,20 +46,14 @@ async def setup_admin(body: AdminSetupRequest, db: AsyncSession = Depends(get_db
# Check uniqueness # Check uniqueness
conflict = await db.execute( conflict = await db.execute(
select(User).where(User.username == username) select(User).where((User.username == username) | (User.email == body.email))
) )
if conflict.scalar_one_or_none(): if conflict.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Username already taken.") raise HTTPException(status_code=400, detail="Username or email already taken.")
# Generate a placeholder email for the admin setup account
email = f"{username}@admin.local"
email_conflict = await db.execute(select(User).where(User.email == email))
if email_conflict.scalar_one_or_none():
email = f"{username}.admin@admin.local"
user = User( user = User(
username=username, username=username,
email=email, email=body.email,
hashed_password=hash_password(body.password), hashed_password=hash_password(body.password),
is_admin=True, is_admin=True,
is_active=True, is_active=True,

View File

@ -5,6 +5,7 @@ from pydantic import BaseModel, EmailStr
class AdminSetupRequest(BaseModel): class AdminSetupRequest(BaseModel):
username: str username: str
email: EmailStr
password: str password: str

View File

@ -142,6 +142,7 @@ const modalStyles: Record<string, React.CSSProperties> = {
function SetupScreen({ onDone }: { onDone: () => void }) { function SetupScreen({ onDone }: { onDone: () => void }) {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState(''); const [confirm, setConfirm] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
@ -157,7 +158,7 @@ function SetupScreen({ onDone }: { onDone: () => void }) {
} }
setLoading(true); setLoading(true);
try { try {
await createFirstAdmin(username, password); await createFirstAdmin(username, email, password);
onDone(); onDone();
navigate('/login?redirect=/admin'); navigate('/login?redirect=/admin');
} catch (err: any) { } catch (err: any) {
@ -183,6 +184,15 @@ function SetupScreen({ onDone }: { onDone: () => void }) {
autoFocus autoFocus
placeholder="admin" placeholder="admin"
/> />
<label style={s.label}>Email</label>
<input
style={s.input}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
placeholder="admin@example.com"
/>
<label style={s.label}>Password</label> <label style={s.label}>Password</label>
<input <input
style={s.input} style={s.input}

View File

@ -41,8 +41,8 @@ export async function getAdminSetupStatus(): Promise<{ has_admin: boolean }> {
return data; return data;
} }
export async function createFirstAdmin(username: string, password: string): Promise<AdminUserResponse> { export async function createFirstAdmin(username: string, email: string, password: string): Promise<AdminUserResponse> {
const { data } = await api.post('/admin/setup', { username, password }); const { data } = await api.post('/admin/setup', { username, email, password });
return data; return data;
} }