Seth commited on
Commit
6a96b23
·
1 Parent(s): 38a6ce1
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
- # Force consent so Google returns a refresh_token when (re)adding gmail.send for existing users.
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
- useEffect(() => {
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
- Connect Gmail once so &quot;Send invite&quot; can send mail from your address:{' '}
 
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"