add users

This commit is contained in:
debu-2 2026-10-04 20:42:40 +05:30
parent c01914736b
commit ee23b71f0f
12 changed files with 506 additions and 182 deletions

View file

@ -46,6 +46,17 @@ async def migrate_user_org_columns(connection) -> None:
await connection.execute(
text("UPDATE users SET member_status = 'active' WHERE member_status IS NULL")
)
await connection.execute(
text(
"ALTER TABLE users ADD COLUMN IF NOT EXISTS invitation_email VARCHAR(320)"
)
)
await connection.execute(
text(
"ALTER TABLE users ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN "
"DEFAULT FALSE"
)
)
async def migrate_legacy_user_tables(connection) -> None:

View file

@ -48,6 +48,8 @@ class Account(Base):
ForeignKey("users.id"),
nullable=True,
)
invitation_email: Mapped[str | None] = mapped_column(String(320), nullable=True)
must_change_password: Mapped[bool] = mapped_column(Boolean, default=False)
password_hash: Mapped[str] = mapped_column(String(255))
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),

View file

@ -14,7 +14,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import get_settings
from app.database import get_postgres_session
from app.models import Account
from app.schemas import AccountRegister, AccountRead, AuthResponse, LoginRequest
from app.schemas import (
AccountRegister,
AccountRead,
AuthResponse,
LoginRequest,
PasswordChangeRequest,
)
from app.services.domain_policy import assert_company_domain_allowed
from app.services.domain_provisioning import account_is_domain_owner
from app.services.domain_provisioning import (
@ -223,12 +229,35 @@ async def login(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password.",
)
if account.member_status == "invited":
account.member_status = "active"
await session.commit()
return AuthResponse(
access_token=create_access_token(account),
account=await account_read(session, account),
)
@router.post("/change-password", response_model=AccountRead)
async def change_password(
payload: PasswordChangeRequest,
account: Account = Depends(get_current_account),
session: AsyncSession = Depends(get_postgres_session),
) -> AccountRead:
validate_password(payload.new_password)
if not password_hash.verify(payload.current_password, account.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Current password is incorrect.",
)
account.password_hash = password_hash.hash(payload.new_password)
account.must_change_password = False
if account.member_status == "invited":
account.member_status = "active"
await session.commit()
return await account_read(session, account)
@router.get("/me", response_model=AccountRead)
async def get_me(
account: Account = Depends(get_current_account),

View file

@ -39,8 +39,7 @@ async def add_person(
manager: Account = Depends(require_org_manager),
session: AsyncSession = Depends(get_postgres_session),
) -> OrgMemberRead:
if not payload.send_invitation:
validate_password(payload.password or "")
validate_password(payload.password)
try:
member = await create_org_member(

View file

@ -35,6 +35,7 @@ class AccountRead(BaseModel):
phone_number: str
company_domain: str | None
is_domain_owner: bool = False
must_change_password: bool = False
created_at: datetime
model_config = ConfigDict(from_attributes=True)
@ -55,18 +56,22 @@ class OrgMemberCreate(BaseModel):
first_name: str = Field(min_length=1, max_length=80)
last_name: str = Field(min_length=1, max_length=80)
email: EmailStr
invitation_email: EmailStr | None = None
org_role: Literal["user", "admin"] = "user"
send_invitation: bool = True
password: str | None = Field(default=None, min_length=8, max_length=128)
force_password_change: bool = False
password: str = Field(min_length=8, max_length=128)
class OrgMemberRead(BaseModel):
id: UUID
email: EmailStr
invitation_email: EmailStr | None = None
first_name: str
last_name: str
org_role: str
member_status: str
must_change_password: bool = False
is_domain_owner: bool = False
groups: list[str] = Field(default_factory=list)
created_at: datetime
@ -74,6 +79,11 @@ class OrgMemberRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
class PasswordChangeRequest(BaseModel):
current_password: str = Field(min_length=1, max_length=128)
new_password: str = Field(min_length=8, max_length=128)
class OrgMemberListResponse(BaseModel):
items: list[OrgMemberRead]
counts: dict[str, int]

View file

@ -0,0 +1,71 @@
"""Employee invitation emails sent from the platform domain."""
from __future__ import annotations
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import get_settings
from app.models import Account, MailDomain
from app.services.domain_provisioning import get_domain_by_name
from app.services.mail_sender import deliver_email
async def send_employee_invitation(
session: AsyncSession,
*,
member: Account,
manager: Account,
initial_password: str,
force_password_change: bool,
) -> None:
if not member.invitation_email:
raise ValueError("Invitation email is required.")
settings = get_settings()
platform_domain = await get_domain_by_name(session, settings.platform_domain)
if platform_domain is None:
raise ValueError("Platform mail domain is not configured.")
login_url = f"https://{settings.app_public_host}/login"
manager_name = f"{manager.first_name} {manager.last_name}".strip()
company = member.company_domain or settings.platform_domain
change_note = (
"You will be asked to choose a new password immediately after signing in."
if force_password_change
else "You can change your password later from account settings."
)
body_text = (
f"Hi {member.first_name},\n\n"
f"{manager_name} invited you to AGMail for {company}.\n\n"
f"Sign in at: {login_url}\n"
f"Work email: {member.email}\n"
f"Temporary password: {initial_password}\n\n"
f"{change_note}\n\n"
"— AGMail"
)
body_html = (
f"<p>Hi {member.first_name},</p>"
f"<p><strong>{manager_name}</strong> invited you to AGMail for "
f"<strong>{company}</strong>.</p>"
f"<p><a href=\"{login_url}\">Sign in to AGMail</a></p>"
f"<ul>"
f"<li><strong>Work email:</strong> {member.email}</li>"
f"<li><strong>Temporary password:</strong> {initial_password}</li>"
f"</ul>"
f"<p>{change_note}</p>"
f"<p>— AGMail</p>"
)
sender = f"AGMail <noreply@{settings.platform_domain}>"
await deliver_email(
sender=sender,
recipients=[member.invitation_email],
cc=[],
bcc=[],
subject=f"You're invited to {company} on AGMail",
body_text=body_text,
body_html=body_html,
domain=platform_domain,
)

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import secrets
from datetime import date
from pwdlib import PasswordHash
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
@ -11,6 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.models import Account
from app.schemas import OrgMemberCreate, OrgMemberRead
from app.services.domain_provisioning import account_is_domain_owner
from app.services.org_invitations import send_employee_invitation
password_hasher = PasswordHash.recommended()
PLACEHOLDER_BIRTHDATE = date(1990, 1, 1)
@ -46,10 +48,12 @@ async def member_read(
return OrgMemberRead(
id=member.id,
email=member.email,
invitation_email=member.invitation_email,
first_name=member.first_name,
last_name=member.last_name,
org_role=role,
member_status=member.member_status,
must_change_password=member.must_change_password,
is_domain_owner=is_owner,
groups=[],
created_at=member.created_at,
@ -84,10 +88,6 @@ def _placeholder_phone() -> str:
return f"+1{digits}"
def _email_local_part(email: str) -> str:
return email.split("@", 1)[0].lower()
async def create_org_member(
session: AsyncSession,
*,
@ -102,13 +102,18 @@ async def create_org_member(
if not email.endswith(f"@{company_domain}"):
raise ValueError(f"Email must use your company domain @{company_domain}.")
invitation_email = (
str(payload.invitation_email).lower() if payload.invitation_email else None
)
if payload.send_invitation:
raw_password = secrets.token_urlsafe(24)
if not invitation_email:
raise ValueError("A personal invitation email is required.")
if invitation_email.endswith(f"@{company_domain}"):
raise ValueError(
"Use a personal email address for the invitation, not your company domain."
)
member_status = "invited"
else:
if not payload.password:
raise ValueError("Password is required when invitation email is disabled.")
raw_password = payload.password
member_status = "active"
existing = await session.scalar(
@ -135,11 +140,23 @@ async def create_org_member(
company_domain=company_domain,
org_role=payload.org_role,
member_status=member_status,
invitation_email=invitation_email,
must_change_password=payload.force_password_change,
created_by_id=manager.id,
password_hash=password_hasher.hash(raw_password),
password_hash=password_hasher.hash(payload.password),
)
session.add(member)
await session.flush()
if payload.send_invitation:
await send_employee_invitation(
session,
member=member,
manager=manager,
initial_password=payload.password,
force_password_change=payload.force_password_change,
)
return await member_read(session, member)

View file

@ -66,6 +66,8 @@ export const updateOrgSecurity = (payload, token) =>
request("/org/security", { method: "PATCH", payload, token });
export const updateOrgMemberRole = (memberId, payload, token) =>
request(`/people/${memberId}/role`, { method: "PATCH", payload, token });
export const changePassword = (payload, token) =>
request("/auth/change-password", { method: "POST", payload, token });
export const sendEmail = (payload, token) =>
request("/emails", { method: "POST", payload, token });

View file

@ -0,0 +1,299 @@
import { ArrowRight, Plus } from "lucide-react";
import { useMemo, useState } from "react";
import { createOrgMember } from "../api";
const GROUP_OPTIONS = ["Admin", "HR", "Engineering", "Finance"];
const EMAIL_LOCAL_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62}[A-Za-z0-9])?$/;
function RequiredLabel({ children }) {
return (
<span className="org-setup-label-text">
{children}
<span className="org-setup-required" aria-hidden="true">*</span>
</span>
);
}
function SetupField({ label, error, children, hint }) {
return (
<label className={`org-setup-field${error ? " has-error" : ""}`}>
{label}
{children}
{hint && <small className="org-setup-field-hint">{hint}</small>}
{error && <span className="org-setup-field-error" role="alert">{error}</span>}
</label>
);
}
function validateForm(form, companyDomain) {
const errors = {};
if (!form.first_name.trim()) errors.first_name = "Please enter a first name.";
if (!form.last_name.trim()) errors.last_name = "Please enter a last name.";
const local = form.email_local.trim();
if (!local) {
errors.email_local = "Please enter a work email address.";
} else if (!EMAIL_LOCAL_PATTERN.test(local)) {
errors.email_local = "Enter a valid email address before @.";
}
if (!form.password.trim()) {
errors.password = "Please set an initial password.";
}
if (form.send_invitation) {
const invite = form.invitation_email.trim();
if (!invite) {
errors.invitation_email = "Please enter a personal email for the invitation.";
} else if (invite.endsWith(`@${companyDomain}`)) {
errors.invitation_email = "Use a personal email, not your company domain.";
}
}
return errors;
}
export default function AddEmployeeForm({
companyDomain,
platformDomain = "gentic.email",
variant = "drawer",
submitLabel = "Create user",
onCreated,
onCancel,
}) {
const token = sessionStorage.getItem("agmail_access_token");
const [form, setForm] = useState({
first_name: "",
last_name: "",
email_local: "",
invitation_email: "",
org_role: "user",
groups: [],
send_invitation: true,
force_password_change: true,
password: "",
});
const [fieldErrors, setFieldErrors] = useState({});
const [submitAttempted, setSubmitAttempted] = useState(false);
const [status, setStatus] = useState({ loading: false, error: "" });
const formValid = useMemo(
() => Object.keys(validateForm(form, companyDomain)).length === 0,
[form, companyDomain],
);
function updateField(field, value) {
setForm((current) => {
const next = { ...current, [field]: value };
if (submitAttempted || fieldErrors[field]) {
const nextErrors = validateForm(next, companyDomain);
setFieldErrors((errors) => ({ ...errors, [field]: nextErrors[field] }));
}
return next;
});
}
function toggleGroup(group) {
setForm((current) => {
const exists = current.groups.includes(group);
return {
...current,
groups: exists
? current.groups.filter((item) => item !== group)
: [...current.groups, group],
};
});
}
async function handleSubmit(event) {
event.preventDefault();
setSubmitAttempted(true);
const errors = validateForm(form, companyDomain);
setFieldErrors(errors);
if (Object.keys(errors).length > 0) {
return;
}
setStatus({ loading: true, error: "" });
const email = `${form.email_local.trim().toLowerCase()}@${companyDomain}`;
try {
const member = await createOrgMember(
{
first_name: form.first_name.trim(),
last_name: form.last_name.trim(),
email,
invitation_email: form.send_invitation
? form.invitation_email.trim().toLowerCase()
: undefined,
org_role: form.org_role,
send_invitation: form.send_invitation,
force_password_change: form.force_password_change,
password: form.password,
},
token,
);
setForm({
first_name: "",
last_name: "",
email_local: "",
invitation_email: "",
org_role: "user",
groups: [],
send_invitation: true,
force_password_change: true,
password: "",
});
setFieldErrors({});
setSubmitAttempted(false);
setStatus({ loading: false, error: "" });
onCreated?.(member);
} catch (error) {
setStatus({ loading: false, error: error.message });
}
}
const formClass = variant === "inline"
? "org-setup-form employee-add-form"
: "people-drawer-form employee-add-form";
return (
<form className={formClass} onSubmit={handleSubmit} noValidate>
<div className={variant === "inline" ? "org-setup-form-shell" : undefined}>
<div className="people-drawer-row">
<SetupField
label={<RequiredLabel>First name</RequiredLabel>}
error={fieldErrors.first_name}
>
<input
value={form.first_name}
onChange={(event) => updateField("first_name", event.target.value)}
placeholder="Rahul"
/>
</SetupField>
<SetupField
label={<RequiredLabel>Last name</RequiredLabel>}
error={fieldErrors.last_name}
>
<input
value={form.last_name}
onChange={(event) => updateField("last_name", event.target.value)}
placeholder="Sharma"
/>
</SetupField>
</div>
<SetupField
label={<RequiredLabel>Work email</RequiredLabel>}
error={fieldErrors.email_local}
hint="This will be their login email address on your company domain."
>
<div className="org-setup-email-field">
<input
value={form.email_local}
onChange={(event) => updateField("email_local", event.target.value)}
placeholder="rahul"
/>
<span>@{companyDomain}</span>
</div>
</SetupField>
<SetupField
label={<RequiredLabel>Invitation email</RequiredLabel>}
error={fieldErrors.invitation_email}
hint={`Personal email where AGMail sends the setup invite from @${platformDomain}.`}
>
<input
type="email"
value={form.invitation_email}
onChange={(event) => updateField("invitation_email", event.target.value)}
placeholder="rahul@gmail.com"
disabled={!form.send_invitation}
/>
</SetupField>
<SetupField
label="Role"
hint="Users can access email. User Admins can manage people."
>
<select
value={form.org_role}
onChange={(event) => updateField("org_role", event.target.value)}
>
<option value="user">User</option>
<option value="admin">User Admin</option>
</select>
</SetupField>
<fieldset className="people-groups-field">
<legend>Groups</legend>
<div className="people-groups-options">
{GROUP_OPTIONS.map((group) => (
<button
key={group}
type="button"
className={`people-group-chip${form.groups.includes(group) ? " active" : ""}`}
onClick={() => toggleGroup(group)}
>
{group}
</button>
))}
</div>
</fieldset>
<SetupField
label={<RequiredLabel>Initial password</RequiredLabel>}
error={fieldErrors.password}
hint="Must include uppercase, lowercase, and a number."
>
<input
type="password"
value={form.password}
onChange={(event) => updateField("password", event.target.value)}
placeholder="Set an initial password"
/>
</SetupField>
<label className="people-toggle-row">
<span>
<strong>Send invitation email</strong>
<small>Invite is sent from @{platformDomain} to the personal email above.</small>
</span>
<input
type="checkbox"
checked={form.send_invitation}
onChange={(event) => updateField("send_invitation", event.target.checked)}
/>
</label>
<label className="people-toggle-row">
<span>
<strong>Require password change on first login</strong>
<small>Employee must choose a new password before using AGMail.</small>
</span>
<input
type="checkbox"
checked={form.force_password_change}
onChange={(event) => updateField("force_password_change", event.target.checked)}
/>
</label>
{status.error && <p className="form-error" role="alert">{status.error}</p>}
<footer className={variant === "inline" ? "org-setup-actions" : "people-drawer-actions"}>
{onCancel && (
<button type="button" className="button button-outline" onClick={onCancel}>
Cancel
</button>
)}
<button
className="button button-purple org-setup-submit"
type="submit"
disabled={!formValid || status.loading}
>
{status.loading ? "Creating…" : (
variant === "inline" ? (
<><Plus size={16} /> {submitLabel}</>
) : submitLabel
)}
</button>
</footer>
</div>
</form>
);
}

View file

@ -1,5 +1,6 @@
import { ArrowRight, Check } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import AddEmployeeForm from "./AddEmployeeForm";
const STEPS = [
{ id: 1, title: "Add domain", hint: "Use your company domain for email" },
@ -126,6 +127,7 @@ export default function CompanyOverviewSetup({ account, companyDomain, onComplet
const [details, setDetails] = useState(() => loadDetails(account, companyDomain));
const [fieldErrors, setFieldErrors] = useState({});
const [submitAttempted, setSubmitAttempted] = useState(false);
const [addedEmployees, setAddedEmployees] = useState([]);
useEffect(() => {
localStorage.setItem(storageKey(account.id, "step"), String(step));
@ -296,20 +298,41 @@ export default function CompanyOverviewSetup({ account, companyDomain, onComplet
)}
{step === 5 && (
<div className="org-setup-form-shell">
<>
<div className="org-setup-panel-intro">
<h2>Add employees</h2>
<p className="org-setup-lead">
<p className="org-setup-section-label">
Invite teammates to <strong>{companyDomainName}</strong>. You can add people now or skip and do this later from People.
</p>
<div className="org-setup-placeholder">
Employee invites will be available here soon.
</div>
{addedEmployees.length > 0 && (
<ul className="employee-added-list">
{addedEmployees.map((employee) => (
<li key={employee.id}>
<strong>{employee.first_name} {employee.last_name}</strong>
<span>{employee.email}</span>
<em>{employee.member_status === "invited" ? "Invited" : "Active"}</em>
</li>
))}
</ul>
)}
<AddEmployeeForm
companyDomain={companyDomainName}
variant="inline"
submitLabel="Add employee"
onCreated={(member) => setAddedEmployees((current) => [...current, member])}
/>
<div className="org-setup-form-shell org-setup-step-actions">
<div className="org-setup-actions">
<button type="button" className="button button-purple org-setup-submit" onClick={() => setStep(6)}>
Continue <ArrowRight size={16} />
{addedEmployees.length > 0 ? "Continue" : "Skip for now"} <ArrowRight size={16} />
</button>
</div>
</div>
</>
)}
{step === 6 && (

View file

@ -9,7 +9,8 @@ import {
XCircle,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { createOrgMember, listOrgMembers } from "../api";
import { listOrgMembers } from "../api";
import AddEmployeeForm from "./AddEmployeeForm";
const STATUS_TABS = [
{ key: "all", label: "All users" },
@ -18,8 +19,6 @@ const STATUS_TABS = [
{ key: "suspended", label: "Suspended" },
];
const GROUP_OPTIONS = ["Admin", "HR", "Engineering", "Finance"];
function initials(firstName, lastName) {
return `${firstName?.[0] || ""}${lastName?.[0] || ""}`.toUpperCase();
}
@ -60,57 +59,6 @@ function StatusBadge({ status }) {
}
function AddUserPanel({ companyDomain, onClose, onCreated }) {
const token = sessionStorage.getItem("agmail_access_token");
const [form, setForm] = useState({
first_name: "",
last_name: "",
email_local: "",
org_role: "user",
groups: [],
send_invitation: true,
password: "",
});
const [status, setStatus] = useState({ loading: false, error: "" });
function updateField(field, value) {
setForm((current) => ({ ...current, [field]: value }));
}
function toggleGroup(group) {
setForm((current) => {
const exists = current.groups.includes(group);
return {
...current,
groups: exists
? current.groups.filter((item) => item !== group)
: [...current.groups, group],
};
});
}
async function handleSubmit(event) {
event.preventDefault();
setStatus({ loading: true, error: "" });
const email = `${form.email_local.trim().toLowerCase()}@${companyDomain}`;
try {
const member = await createOrgMember(
{
first_name: form.first_name.trim(),
last_name: form.last_name.trim(),
email,
org_role: form.org_role,
send_invitation: form.send_invitation,
password: form.send_invitation ? undefined : form.password,
},
token,
);
onCreated?.(member);
onClose();
} catch (error) {
setStatus({ loading: false, error: error.message });
}
}
return (
<div className="people-drawer-scrim" onClick={onClose}>
<aside
@ -126,109 +74,14 @@ function AddUserPanel({ companyDomain, onClose, onCreated }) {
</button>
</header>
<form className="people-drawer-form" onSubmit={handleSubmit}>
<div className="people-drawer-row">
<label>
First name <span aria-hidden="true">*</span>
<input
required
value={form.first_name}
onChange={(event) => updateField("first_name", event.target.value)}
placeholder="Rahul"
<AddEmployeeForm
companyDomain={companyDomain}
onCreated={(member) => {
onCreated?.(member);
onClose();
}}
onCancel={onClose}
/>
</label>
<label>
Last name <span aria-hidden="true">*</span>
<input
required
value={form.last_name}
onChange={(event) => updateField("last_name", event.target.value)}
placeholder="Sharma"
/>
</label>
</div>
<label>
Email <span aria-hidden="true">*</span>
<div className="people-email-field">
<input
required
value={form.email_local}
onChange={(event) => updateField("email_local", event.target.value)}
placeholder="rahul"
/>
<span>@{companyDomain}</span>
</div>
<small>This will be their login email address</small>
</label>
<label>
Role
<select
value={form.org_role}
onChange={(event) => updateField("org_role", event.target.value)}
>
<option value="user">User</option>
<option value="admin">User Admin</option>
</select>
<small>Users can access email. User Admins can manage people.</small>
</label>
<fieldset className="people-groups-field">
<legend>Groups</legend>
<div className="people-groups-options">
{GROUP_OPTIONS.map((group) => (
<button
key={group}
type="button"
className={`people-group-chip${form.groups.includes(group) ? " active" : ""}`}
onClick={() => toggleGroup(group)}
>
{group}
</button>
))}
</div>
<small>Group assignment is saved locally for now.</small>
</fieldset>
<label className="people-toggle-row">
<span>
<strong>Send invitation email</strong>
<small>They will receive an activation link to set their password.</small>
</span>
<input
type="checkbox"
checked={form.send_invitation}
onChange={(event) => updateField("send_invitation", event.target.checked)}
/>
</label>
{!form.send_invitation && (
<label>
Password <span aria-hidden="true">*</span>
<input
required
type="password"
value={form.password}
onChange={(event) => updateField("password", event.target.value)}
placeholder="Set an initial password"
minLength={8}
/>
<small>Must include uppercase, lowercase, and a number.</small>
</label>
)}
{status.error && <p className="form-error" role="alert">{status.error}</p>}
<footer className="people-drawer-actions">
<button type="button" className="button button-outline" onClick={onClose}>
Cancel
</button>
<button className="button button-purple" type="submit" disabled={status.loading}>
{status.loading ? "Creating…" : "Create user"}
</button>
</footer>
</form>
</aside>
</div>
);

View file

@ -226,6 +226,14 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.org-setup-summary { margin:0 0 24px; padding:0; list-style:none; display:grid; gap:10px; }
.org-setup-summary li { display:flex; align-items:center; gap:8px; color:#3d3948; font-size:13px; font-weight:500; }
.org-setup-summary svg { color:#7357F6; flex-shrink:0; }
.employee-add-form .people-drawer-row { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
.employee-add-form .people-groups-field { margin:0; padding:0; border:0; }
.employee-added-list { margin:0 0 20px; padding:0; max-width:720px; list-style:none; display:grid; gap:8px; }
.employee-added-list li { padding:12px 14px; display:grid; grid-template-columns:1fr auto auto; gap:12px; align-items:center; border:1px solid #ECE9F2; border-radius:10px; background:#FAFAFC; font-size:13px; }
.employee-added-list strong { color:#1a1628; }
.employee-added-list span { color:#6f6c7b; }
.employee-added-list em { color:#7357F6; font-size:11px; font-weight:700; font-style:normal; text-transform:uppercase; letter-spacing:.3px; }
.org-setup-step-actions { margin-top:8px; }
.dashboard-sidebar-manage { margin-top:var(--space-4); }
@media(max-width:900px) {
.org-setup-head h1 { font-size:28px; }