Seth commited on
Commit ·
6a96b23
1
Parent(s): 38a6ce1
update
Browse files
backend/app/auth_routes.py
CHANGED
|
@@ -313,6 +313,32 @@ async def google_login(
|
|
| 313 |
redirect_uri = _redirect_uri(request)
|
| 314 |
request.session["google_oauth_state"] = state
|
| 315 |
request.session["google_oauth_redirect_uri"] = redirect_uri
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
params = {
|
| 317 |
"client_id": client_id,
|
| 318 |
"redirect_uri": redirect_uri,
|
|
@@ -322,8 +348,7 @@ async def google_login(
|
|
| 322 |
"access_type": "offline",
|
| 323 |
"include_granted_scopes": "true",
|
| 324 |
}
|
| 325 |
-
|
| 326 |
-
if reauth_gmail and reauth_gmail.strip() not in ("0", "false", "no"):
|
| 327 |
params["prompt"] = "consent"
|
| 328 |
return RedirectResponse(url=f"{GOOGLE_AUTH_URI}?{urlencode(params)}")
|
| 329 |
|
|
|
|
| 313 |
redirect_uri = _redirect_uri(request)
|
| 314 |
request.session["google_oauth_state"] = state
|
| 315 |
request.session["google_oauth_redirect_uri"] = redirect_uri
|
| 316 |
+
|
| 317 |
+
# Google only reliably returns a refresh_token (needed for gmail.send) when the user sees
|
| 318 |
+
# the consent screen. Match "Reconnect Google" behavior on every sign-in unless we already
|
| 319 |
+
# have a stored token; ?reauth_gmail=1 always forces consent (e.g. revoked token).
|
| 320 |
+
force_consent = bool(reauth_gmail and reauth_gmail.strip().lower() not in ("0", "false", "no"))
|
| 321 |
+
need_prompt_consent = True
|
| 322 |
+
if not force_consent:
|
| 323 |
+
uid = request.session.get("user_id")
|
| 324 |
+
if uid is not None:
|
| 325 |
+
try:
|
| 326 |
+
uid_int = int(uid)
|
| 327 |
+
except (TypeError, ValueError):
|
| 328 |
+
uid_int = None
|
| 329 |
+
if uid_int is not None:
|
| 330 |
+
db = SessionLocal()
|
| 331 |
+
try:
|
| 332 |
+
u = db.query(User).filter(User.id == uid_int).first()
|
| 333 |
+
if (
|
| 334 |
+
u
|
| 335 |
+
and getattr(u, "google_refresh_token", None)
|
| 336 |
+
and str(u.google_refresh_token).strip()
|
| 337 |
+
):
|
| 338 |
+
need_prompt_consent = False
|
| 339 |
+
finally:
|
| 340 |
+
db.close()
|
| 341 |
+
|
| 342 |
params = {
|
| 343 |
"client_id": client_id,
|
| 344 |
"redirect_uri": redirect_uri,
|
|
|
|
| 348 |
"access_type": "offline",
|
| 349 |
"include_granted_scopes": "true",
|
| 350 |
}
|
| 351 |
+
if need_prompt_consent:
|
|
|
|
| 352 |
params["prompt"] = "consent"
|
| 353 |
return RedirectResponse(url=f"{GOOGLE_AUTH_URI}?{urlencode(params)}")
|
| 354 |
|
frontend/src/components/workspace/WonBillingModal.jsx
CHANGED
|
@@ -31,8 +31,9 @@ function fmtMoneyAmount(n) {
|
|
| 31 |
|
| 32 |
/**
|
| 33 |
* Modal to capture PO / customer / line items before marking a deal Won (invoicing).
|
|
|
|
| 34 |
*/
|
| 35 |
-
export default function WonBillingModal({ dealName, busy, onCancel, onSubmit }) {
|
| 36 |
const [poNumber, setPoNumber] = useState('');
|
| 37 |
const [customerLegalName, setCustomerLegalName] = useState('');
|
| 38 |
const [customerAddress, setCustomerAddress] = useState('');
|
|
@@ -86,6 +87,10 @@ export default function WonBillingModal({ dealName, busy, onCancel, onSubmit })
|
|
| 86 |
|
| 87 |
const handleSubmit = () => {
|
| 88 |
setLocalError('');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
if (!poNumber.trim()) {
|
| 90 |
setLocalError('PO number is required.');
|
| 91 |
return;
|
|
@@ -181,6 +186,20 @@ export default function WonBillingModal({ dealName, busy, onCancel, onSubmit })
|
|
| 181 |
agreement. Errors here can cause invoice mistakes.
|
| 182 |
</div>
|
| 183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
<div className="mt-5 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
| 185 |
<div>
|
| 186 |
<label className="mb-1 block text-xs font-medium text-slate-600">PO number</label>
|
|
@@ -383,7 +402,7 @@ export default function WonBillingModal({ dealName, busy, onCancel, onSubmit })
|
|
| 383 |
type="button"
|
| 384 |
className="bg-emerald-600 hover:bg-emerald-700"
|
| 385 |
onClick={handleSubmit}
|
| 386 |
-
disabled={busy}
|
| 387 |
>
|
| 388 |
{busy ? (
|
| 389 |
<>
|
|
|
|
| 31 |
|
| 32 |
/**
|
| 33 |
* Modal to capture PO / customer / line items before marking a deal Won (invoicing).
|
| 34 |
+
* @param {boolean | undefined} gmailInvitesReady — from /api/auth/me; false blocks submit until user reconnects Gmail.
|
| 35 |
*/
|
| 36 |
+
export default function WonBillingModal({ dealName, busy, onCancel, onSubmit, gmailInvitesReady }) {
|
| 37 |
const [poNumber, setPoNumber] = useState('');
|
| 38 |
const [customerLegalName, setCustomerLegalName] = useState('');
|
| 39 |
const [customerAddress, setCustomerAddress] = useState('');
|
|
|
|
| 87 |
|
| 88 |
const handleSubmit = () => {
|
| 89 |
setLocalError('');
|
| 90 |
+
if (gmailInvitesReady === false) {
|
| 91 |
+
setLocalError('Connect Gmail below before marking this deal won.');
|
| 92 |
+
return;
|
| 93 |
+
}
|
| 94 |
if (!poNumber.trim()) {
|
| 95 |
setLocalError('PO number is required.');
|
| 96 |
return;
|
|
|
|
| 186 |
agreement. Errors here can cause invoice mistakes.
|
| 187 |
</div>
|
| 188 |
|
| 189 |
+
{gmailInvitesReady === false ? (
|
| 190 |
+
<p className="mt-3 text-xs text-amber-900 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
|
| 191 |
+
Workspace admins get an email when you mark a deal won. Sign in with Google again (consent
|
| 192 |
+
screen) or use{' '}
|
| 193 |
+
<a
|
| 194 |
+
href="/api/auth/google?reauth_gmail=1"
|
| 195 |
+
className="font-medium text-violet-700 underline"
|
| 196 |
+
>
|
| 197 |
+
Reconnect Google for invites
|
| 198 |
+
</a>
|
| 199 |
+
, then return here and submit again.
|
| 200 |
+
</p>
|
| 201 |
+
) : null}
|
| 202 |
+
|
| 203 |
<div className="mt-5 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
| 204 |
<div>
|
| 205 |
<label className="mb-1 block text-xs font-medium text-slate-600">PO number</label>
|
|
|
|
| 402 |
type="button"
|
| 403 |
className="bg-emerald-600 hover:bg-emerald-700"
|
| 404 |
onClick={handleSubmit}
|
| 405 |
+
disabled={busy || gmailInvitesReady === false}
|
| 406 |
>
|
| 407 |
{busy ? (
|
| 408 |
<>
|
frontend/src/pages/Deals.jsx
CHANGED
|
@@ -904,13 +904,21 @@ export default function Deals() {
|
|
| 904 |
const isAdmin = me?.current_role === 'admin';
|
| 905 |
const currentUserId = me?.user_id ?? null;
|
| 906 |
|
| 907 |
-
|
| 908 |
apiFetch('/api/auth/me')
|
| 909 |
.then((r) => (r.ok ? r.json() : null))
|
| 910 |
.then(setMe)
|
| 911 |
.catch(() => setMe(null));
|
| 912 |
}, []);
|
| 913 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 914 |
useEffect(() => {
|
| 915 |
if (!me || me.current_role !== 'admin') {
|
| 916 |
setTenantMembers([]);
|
|
@@ -1797,6 +1805,7 @@ export default function Deals() {
|
|
| 1797 |
key={wonDialog.dealId}
|
| 1798 |
dealName={deals.find((d) => d.id === wonDialog.dealId)?.name || ''}
|
| 1799 |
busy={wonModalBusy}
|
|
|
|
| 1800 |
onCancel={() => {
|
| 1801 |
if (!wonModalBusy) setWonDialog(null);
|
| 1802 |
}}
|
|
|
|
| 904 |
const isAdmin = me?.current_role === 'admin';
|
| 905 |
const currentUserId = me?.user_id ?? null;
|
| 906 |
|
| 907 |
+
const refreshMe = useCallback(() => {
|
| 908 |
apiFetch('/api/auth/me')
|
| 909 |
.then((r) => (r.ok ? r.json() : null))
|
| 910 |
.then(setMe)
|
| 911 |
.catch(() => setMe(null));
|
| 912 |
}, []);
|
| 913 |
|
| 914 |
+
useEffect(() => {
|
| 915 |
+
refreshMe();
|
| 916 |
+
}, [refreshMe]);
|
| 917 |
+
|
| 918 |
+
useEffect(() => {
|
| 919 |
+
if (wonDialog) refreshMe();
|
| 920 |
+
}, [wonDialog, refreshMe]);
|
| 921 |
+
|
| 922 |
useEffect(() => {
|
| 923 |
if (!me || me.current_role !== 'admin') {
|
| 924 |
setTenantMembers([]);
|
|
|
|
| 1805 |
key={wonDialog.dealId}
|
| 1806 |
dealName={deals.find((d) => d.id === wonDialog.dealId)?.name || ''}
|
| 1807 |
busy={wonModalBusy}
|
| 1808 |
+
gmailInvitesReady={me?.gmail_invites_ready}
|
| 1809 |
onCancel={() => {
|
| 1810 |
if (!wonModalBusy) setWonDialog(null);
|
| 1811 |
}}
|
frontend/src/pages/Settings.jsx
CHANGED
|
@@ -268,7 +268,8 @@ export default function Settings() {
|
|
| 268 |
</p>
|
| 269 |
{me?.gmail_invites_ready === false ? (
|
| 270 |
<p className="text-xs text-amber-900 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2 mb-4">
|
| 271 |
-
|
|
|
|
| 272 |
<a
|
| 273 |
href="/api/auth/google?reauth_gmail=1"
|
| 274 |
className="font-medium text-violet-700 underline"
|
|
|
|
| 268 |
</p>
|
| 269 |
{me?.gmail_invites_ready === false ? (
|
| 270 |
<p className="text-xs text-amber-900 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2 mb-4">
|
| 271 |
+
Sign in with Google again (you will see a consent screen) so invites can send from your
|
| 272 |
+
address, or use{' '}
|
| 273 |
<a
|
| 274 |
href="/api/auth/google?reauth_gmail=1"
|
| 275 |
className="font-medium text-violet-700 underline"
|