From 0b4659e2d85c5311835651d3f1aa54c48f3849de Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:09:32 -0400 Subject: fix for task resizing not working after merge, and fix for all-day view for initial task-nodes (i.e. if you just created task node and checked all-day, cal view was previously showing task 1 day early) --- src/client/views/nodes/calendarBox/CalendarBox.tsx | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx index 26aed72c3..92b3224e9 100644 --- a/src/client/views/nodes/calendarBox/CalendarBox.tsx +++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx @@ -22,6 +22,7 @@ import './CalendarBox.scss'; import { DateField } from '../../../../fields/DateField'; import { undoable } from '../../../util/UndoManager'; import { DocumentType } from '../../../documents/DocumentTypes'; +import { truncate } from 'fs/promises'; type CalendarView = 'multiMonth' | 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay'; @@ -74,8 +75,20 @@ export class CalendarBox extends CollectionSubView() { @computed get calendarEvents(): EventSourceInput | undefined { return this.childDocs.map(doc => { - const { start, end } = dateRangeStrToDates(StrCast(doc.$task_dateRange)); - const isCompleted = BoolCast(doc.$task_completed); // AARAV ADD + // const { start, end } = dateRangeStrToDates(StrCast(doc.$task_dateRange)); + const isCompleted = BoolCast(doc.$task_completed); + + const rangeStr = StrCast(doc.$task_dateRange); + const [startStr, endStr] = rangeStr.split('|'); + let start: string | Date, end: string | Date; + + if (BoolCast(doc.$task_allDay)) { + start = startStr; + end = endStr; + } else { + ({ start, end } = dateRangeStrToDates(rangeStr)); + } + return { title: StrCast(doc.title), start, @@ -157,7 +170,7 @@ export class CalendarBox extends CollectionSubView() { delete doc.$task_endTime; } else { doc.$task_startTime = new DateField(startDate); - doc.task_endTime = new DateField(endDate); + doc.$task_endTime = new DateField(endDate); } }, 'change event date'); @@ -220,7 +233,7 @@ export class CalendarBox extends CollectionSubView() { }} initialDate={untracked(() => this.dateSelect.start)} navLinks={true} - editable={false} + editable={true} // expandRows={true} // handleWindowResize={true} displayEventTime={false} @@ -231,6 +244,7 @@ export class CalendarBox extends CollectionSubView() { events={this.calendarEvents} eventClick={this.handleEventClick} eventDrop={this.handleEventDrop} + eventResize={this.handleEventDrop} unselectAuto={false} // unselect={() => {}} select={(info: DateSelectArg) => { -- cgit v1.2.3-70-g09d2 From 6e7a29470e2c9e2fad287b4851b9eb561821ea9e Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:10:05 -0400 Subject: fixed drag and drop of calendar events not following cursor properly --- .../views/nodes/calendarBox/CalendarBox.scss | 9 +++ src/client/views/nodes/calendarBox/CalendarBox.tsx | 77 +++++++++++++++++++--- src/server/apis/google/GoogleTasksHandler.ts | 54 +++++++++++++++ 3 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 src/server/apis/google/GoogleTasksHandler.ts (limited to 'src') diff --git a/src/client/views/nodes/calendarBox/CalendarBox.scss b/src/client/views/nodes/calendarBox/CalendarBox.scss index 891db9d90..89dc294a5 100644 --- a/src/client/views/nodes/calendarBox/CalendarBox.scss +++ b/src/client/views/nodes/calendarBox/CalendarBox.scss @@ -50,3 +50,12 @@ pointer-events: unset; } } + +.custom-drag-mirror { + transition: none !important; + transform: none !important; +} + +.fc-event-dragging { + opacity: 0 !important; +} diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx index 92b3224e9..a4183a11a 100644 --- a/src/client/views/nodes/calendarBox/CalendarBox.tsx +++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx @@ -298,15 +298,74 @@ export class CalendarBox extends CollectionSubView() { ev.preventDefault(); }); }} + + // for dragging and dropping (mirror) + + eventDragStart={(arg) => { + const mirror = arg.el.cloneNode(true) as HTMLElement; + const rect = arg.el.getBoundingClientRect(); + + mirror.style.position = 'fixed'; + mirror.style.pointerEvents = 'none'; + mirror.style.opacity = '0.8'; + mirror.style.zIndex = '10000'; + mirror.classList.add('custom-drag-mirror'); + mirror.style.width = `${rect.width}px`; + mirror.style.height = `${rect.height}px`; + + document.body.appendChild(mirror); + + const moveListener = (ev: MouseEvent) => { + mirror.style.left = `${ev.clientX}px`; + mirror.style.top = `${ev.clientY}px`; + }; + + window.addEventListener('mousemove', moveListener); + + // hide the actual box + arg.el.style.visibility = 'hidden'; + arg.el.style.opacity = '0'; + + (arg.el as any)._mirrorElement = mirror; + (arg.el as any)._moveListener = moveListener; + }} + + eventDragStop={(arg) => { + const el = arg.el as any; + const mirror = el._mirrorElement; + const moveListener = el._moveListener; + + // show the actual box + el.style.visibility = 'visible'; + el.style.opacity = '1'; + + if (mirror) document.body.removeChild(mirror); + if (moveListener) window.removeEventListener('mousemove', moveListener); + }} + /> ); } render() { + const scale = this._props.ScreenToLocalTransform().Scale; + const scaledWidth = this._props.PanelWidth(); + const scaledHeight = this._props.PanelHeight(); + return (
{ + this.createDashEventsTarget(r); + this.fixWheelEvents(r, this._props.isContentActive); + }} onPointerDown={e => { setTimeout( action(() => { @@ -317,17 +376,17 @@ export class CalendarBox extends CollectionSubView() { if (cname.includes('timeGridDay')) this.dataDoc[this.calTypeFieldKey] = 'timeGridDay'; }) ); - }} - style={{ - width: this._props.PanelWidth() / this._props.ScreenToLocalTransform().Scale, - height: this._props.PanelHeight() / this._props.ScreenToLocalTransform().Scale, - transform: `scale(${this._props.ScreenToLocalTransform().Scale})`, - }} - ref={r => { - this.createDashEventsTarget(r); - this.fixWheelEvents(r, this._props.isContentActive); }}> +
{this.renderCalendar} +
); } diff --git a/src/server/apis/google/GoogleTasksHandler.ts b/src/server/apis/google/GoogleTasksHandler.ts new file mode 100644 index 000000000..a8af86fe2 --- /dev/null +++ b/src/server/apis/google/GoogleTasksHandler.ts @@ -0,0 +1,54 @@ +import express from 'express'; +import { google } from 'googleapis'; +import { GoogleCredentialsLoader } from './CredentialsLoader'; +import User from '../../authentication/DashUserModel'; +import { DashUserModel } from '../../authentication/DashUserModel'; + + +const router = express.Router(); + +router.post('/tasks/create', async (req, res) => { + try { + const { title, notes, due } = req.body; + + // Make sure user is authenticated + if (!req.user) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + // Assuming access token is stored in user model + const user = req.user as typeof User; // replace with your actual User type if needed + const accessToken = user.googleAccessToken; // <-- change this based on where you store it + + if (!accessToken) { + return res.status(400).json({ error: 'Google access token not found for user' }); + } + + const credentials = GoogleCredentialsLoader.ProjectCredentials; + const auth = new google.auth.OAuth2( + credentials.client_id, + credentials.client_secret, + credentials.redirect_uris[0] + ); + + auth.setCredentials({ access_token: accessToken }); + + const tasks = google.tasks({ version: 'v1', auth }); + + const result = await tasks.tasks.insert({ + tasklist: '@default', + requestBody: { + title, + notes, + due, + }, + }); + + res.status(200).json(result.data); + } catch (err) { + console.error('Error creating Google Task:', err); + res.status(500).json({ error: 'Failed to create task' }); + } +}); + +export default router; -- cgit v1.2.3-70-g09d2 From c96a14c881ea28d820a53a38fa9360814dcb6385 Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Tue, 3 Jun 2025 22:41:11 -0400 Subject: gcal task addition route added -- problems with fetchOrGenerateAccessToken --- src/client/apis/GoogleAuthenticationManager.tsx | 2 + src/client/views/nodes/TaskBox.tsx | 40 ++++++++++++++++++ src/server/ApiManagers/GeneralGoogleManager.ts | 35 ++++++++++++++++ src/server/apis/google/GoogleTasksHandler.ts | 54 ------------------------- 4 files changed, 77 insertions(+), 54 deletions(-) delete mode 100644 src/server/apis/google/GoogleTasksHandler.ts (limited to 'src') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 94ce42d8d..46581397d 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -41,6 +41,8 @@ export class GoogleAuthenticationManager extends React.Component { if (new RegExp(AuthenticationUrl).test(response)) { this.isOpen = true; this.authenticationLink = response; + + // GETS STUCK AT THIS PROMISE!! return new Promise(resolve => { this.disposer?.(); this.disposer = reaction( diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx index 9d59746f8..1c7aeeb82 100644 --- a/src/client/views/nodes/TaskBox.tsx +++ b/src/client/views/nodes/TaskBox.tsx @@ -8,6 +8,7 @@ import { DateField } from '../../../fields/DateField'; import { Doc } from '../../../fields/Doc'; import './TaskBox.scss'; +import { GoogleAuthenticationManager } from '../../apis/GoogleAuthenticationManager'; /** * Props (reference to document) for Task Box @@ -267,6 +268,45 @@ export class TaskBox extends React.Component { )} + + {/** test button */} + + + + ); } diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts index 12913b1ef..aa06ca1b3 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -3,6 +3,8 @@ import { Method } from '../RouteManager'; import { GoogleApiServerUtils } from '../apis/google/GoogleApiServerUtils'; import RouteSubscriber from '../RouteSubscriber'; import { Database } from '../database'; +import { google } from 'googleapis'; + const EndpointHandlerMap = new Map([ ['create', (api, params) => api.create(params)], @@ -61,5 +63,38 @@ export default class GeneralGoogleManager extends ApiManager { res.send(undefined); }, }); + + // AARAV ADD (creating a task) + + register({ + method: Method.POST, + subscription: new RouteSubscriber('googleTasks').add('create'), + secureHandler: async ({ req, res, user }) => { + try { + const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user.id); + if (!credentials?.access_token) { + return res.status(401).send('Google access token not found.'); + } + + const auth = new google.auth.OAuth2(); + auth.setCredentials({ access_token: credentials.access_token }); + + const tasks = google.tasks({ version: 'v1', auth }); + + const { title, notes, due } = req.body; + const result = await tasks.tasks.insert({ + tasklist: '@default', + requestBody: { title, notes, due }, + }); + + res.status(200).send(result.data); + } catch (err) { + console.error('Google Tasks error:', err); + res.status(500).send('Failed to create task.'); + } + }, + }); + + } } diff --git a/src/server/apis/google/GoogleTasksHandler.ts b/src/server/apis/google/GoogleTasksHandler.ts deleted file mode 100644 index a8af86fe2..000000000 --- a/src/server/apis/google/GoogleTasksHandler.ts +++ /dev/null @@ -1,54 +0,0 @@ -import express from 'express'; -import { google } from 'googleapis'; -import { GoogleCredentialsLoader } from './CredentialsLoader'; -import User from '../../authentication/DashUserModel'; -import { DashUserModel } from '../../authentication/DashUserModel'; - - -const router = express.Router(); - -router.post('/tasks/create', async (req, res) => { - try { - const { title, notes, due } = req.body; - - // Make sure user is authenticated - if (!req.user) { - return res.status(401).json({ error: 'User not authenticated' }); - } - - // Assuming access token is stored in user model - const user = req.user as typeof User; // replace with your actual User type if needed - const accessToken = user.googleAccessToken; // <-- change this based on where you store it - - if (!accessToken) { - return res.status(400).json({ error: 'Google access token not found for user' }); - } - - const credentials = GoogleCredentialsLoader.ProjectCredentials; - const auth = new google.auth.OAuth2( - credentials.client_id, - credentials.client_secret, - credentials.redirect_uris[0] - ); - - auth.setCredentials({ access_token: accessToken }); - - const tasks = google.tasks({ version: 'v1', auth }); - - const result = await tasks.tasks.insert({ - tasklist: '@default', - requestBody: { - title, - notes, - due, - }, - }); - - res.status(200).json(result.data); - } catch (err) { - console.error('Error creating Google Task:', err); - res.status(500).json({ error: 'Failed to create task' }); - } -}); - -export default router; -- cgit v1.2.3-70-g09d2 From 645df1d00f953524c6da22103d26c38ae4331cd6 Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:59:34 -0400 Subject: partial google calendar task integration + fix with dark mode colors for task nodes so that text is still visible --- src/client/apis/GoogleAuthenticationManager.tsx | 35 ++++++----- src/client/views/MainViewModal.tsx | 2 +- src/client/views/nodes/TaskBox.scss | 11 +++- src/client/views/nodes/TaskBox.tsx | 69 ++++++++++---------- src/server/ApiManagers/FireflyManager.ts | 11 +++- src/server/ApiManagers/GeneralGoogleManager.ts | 73 +++++++++++++--------- src/server/apis/google/GoogleApiServerUtils.ts | 39 +++++++----- .../apis/google/google_project_credentials.json | 6 +- src/server/authentication/DashUserModel.ts | 6 ++ 9 files changed, 153 insertions(+), 99 deletions(-) (limited to 'src') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 46581397d..1b1d6f734 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -1,4 +1,4 @@ -import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Opt } from '../../fields/Doc'; @@ -6,12 +6,13 @@ import { Networking } from '../Network'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { MainViewModal } from '../views/MainViewModal'; import './GoogleAuthenticationManager.scss'; +import { ObservableReactComponent } from '../views/ObservableReactComponent'; const AuthenticationUrl = 'https://accounts.google.com/o/oauth2/v2/auth'; const prompt = 'Paste authorization code here...'; @observer -export class GoogleAuthenticationManager extends React.Component { +export class GoogleAuthenticationManager extends ObservableReactComponent { // eslint-disable-next-line no-use-before-define public static Instance: GoogleAuthenticationManager; private authenticationLink: Opt = undefined; @@ -23,6 +24,12 @@ export class GoogleAuthenticationManager extends React.Component { @observable private credentials: { user_info: { name: string; picture: string }; access_token: string } | undefined = undefined; private disposer: Opt; + constructor(props: object) { + super(props); + makeObservable(this); + GoogleAuthenticationManager.Instance = this; + } + private set isOpen(value: boolean) { runInAction(() => (this.openState = value)); } @@ -49,14 +56,15 @@ export class GoogleAuthenticationManager extends React.Component { () => this.authenticationCode, async authenticationCode => { if (authenticationCode && /\d{1}\/[\w-]{55}/.test(authenticationCode)) { + resolve(authenticationCode); this.disposer?.(); - const response2 = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode }); - runInAction(() => { - this.success = true; - this.credentials = response2 as { user_info: { name: string; picture: string }; access_token: string }; - }); + // const response2 = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode }); + // runInAction(() => { + // this.success = true; + // this.credentials = response2 as { user_info: { name: string; picture: string }; access_token: string }; + // }); + // resolve((response2 as { access_token: string }).access_token); this.resetState(); - resolve((response2 as { access_token: string }).access_token); } } ); @@ -109,11 +117,6 @@ export class GoogleAuthenticationManager extends React.Component { } }); - constructor(props: object) { - super(props); - GoogleAuthenticationManager.Instance = this; - } - private get renderPrompt() { return (
@@ -153,7 +156,11 @@ export class GoogleAuthenticationManager extends React.Component { } render() { - return (this.isOpen = false))} />; + return ( +
+ (this.isOpen = false))} /> +
+ ); } } diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index b05292c47..d7640dc72 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -41,7 +41,7 @@ export class MainViewModal extends React.Component { className="overlay" onClick={this.props?.closeOnExternalClick} style={{ - backgroundColor: isDark(SnappingManager.userColor) ? '#DFDFDF30' : '#32323230', + backgroundColor: isDark(SnappingManager.userColor ?? '') ? '#DFDFDF30' : '#32323230', ...(p.overlayStyle || {}), }} /> diff --git a/src/client/views/nodes/TaskBox.scss b/src/client/views/nodes/TaskBox.scss index 0fcc2f955..6ef0c6454 100644 --- a/src/client/views/nodes/TaskBox.scss +++ b/src/client/views/nodes/TaskBox.scss @@ -6,6 +6,15 @@ width: 100%; height: 100%; box-sizing: border-box; + + input, + textarea, + select, + button { + background-color: #fff !important; + color: #000 !important; + border-color: #ccc !important; + } } .task-manager-title { @@ -69,4 +78,4 @@ input[type="datetime-local"] { border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; -} +} \ No newline at end of file diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx index 1c7aeeb82..3990356b9 100644 --- a/src/client/views/nodes/TaskBox.tsx +++ b/src/client/views/nodes/TaskBox.tsx @@ -270,43 +270,38 @@ export class TaskBox extends React.Component { )} {/** test button */} - - - - + ); } diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts index 07ef271a1..6393a1f74 100644 --- a/src/server/ApiManagers/FireflyManager.ts +++ b/src/server/ApiManagers/FireflyManager.ts @@ -23,7 +23,16 @@ export default class FireflyManager extends ApiManager { return undefined; }); - generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50, styles: string[], styleUrl: string | undefined, variations: number = 4) => + generateImageFromStructure = ( + prompt: string = 'a realistic illustration of a cat coding', + width: number = 2048, + height: number = 2048, + structureUrl: string, + strength: number = 50, + styles: string[], + styleUrl: string | undefined, + variations: number = 4 + ) => this.getBearerToken().then(response => response?.json().then((data: { access_token: string }) => //prettier-ignore diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts index aa06ca1b3..e8debfc12 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -1,11 +1,10 @@ import ApiManager, { Registration } from './ApiManager'; -import { Method } from '../RouteManager'; +import { Method, _success } from '../RouteManager'; import { GoogleApiServerUtils } from '../apis/google/GoogleApiServerUtils'; import RouteSubscriber from '../RouteSubscriber'; import { Database } from '../database'; import { google } from 'googleapis'; - const EndpointHandlerMap = new Map([ ['create', (api, params) => api.create(params)], ['retrieve', (api, params) => api.get(params)], @@ -18,9 +17,10 @@ export default class GeneralGoogleManager extends ApiManager { method: Method.GET, subscription: '/readGoogleAccessToken', secureHandler: async ({ user, res }) => { - const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user.id); + const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user); if (!credentials?.access_token) { - return res.send(GoogleApiServerUtils.generateAuthenticationUrl()); + const url = GoogleApiServerUtils.generateAuthenticationUrl(); + return res.send(url); } return res.send(credentials); }, @@ -49,7 +49,7 @@ export default class GeneralGoogleManager extends ApiManager { secureHandler: async ({ req, res, user }) => { const sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service; const action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action; - const endpoint = await GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user.id); + const endpoint = await GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user); const handler = EndpointHandlerMap.get(action); if (endpoint && handler) { try { @@ -70,31 +70,48 @@ export default class GeneralGoogleManager extends ApiManager { method: Method.POST, subscription: new RouteSubscriber('googleTasks').add('create'), secureHandler: async ({ req, res, user }) => { - try { - const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user.id); - if (!credentials?.access_token) { - return res.status(401).send('Google access token not found.'); + try { + const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user.id); + const access_token = user.googleToken || credentials?.access_token; // if googleToken expires, we need to renew it. + + if (!access_token) { + return res.status(401).send('Google access token not found.'); + } + + const auth = new google.auth.OAuth2(); + auth.setCredentials({ access_token: access_token }); + + const tasks = google.tasks({ version: 'v1', auth }); + + const { title, notes, due } = req.body; + const result = await tasks.tasks.insert({ + tasklist: '@default', + requestBody: { title, notes, due }, + }); + + res.status(200).send(result.data); + } catch (err) { + console.error('Google Tasks error:', err); + res.status(500).send('Failed to create task.'); } - - const auth = new google.auth.OAuth2(); - auth.setCredentials({ access_token: credentials.access_token }); - - const tasks = google.tasks({ version: 'v1', auth }); - - const { title, notes, due } = req.body; - const result = await tasks.tasks.insert({ - tasklist: '@default', - requestBody: { title, notes, due }, - }); - - res.status(200).send(result.data); - } catch (err) { - console.error('Google Tasks error:', err); - res.status(500).send('Failed to create task.'); - } }, - }); + }); - + register({ + method: Method.GET, + subscription: '/refreshGoogle', + secureHandler: async ({ user, req, res }) => { + const code = req.query.code as string; + _success(res, code); + user.googleToken = code; + user.save(); + + // const response2 = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode }); + // runInAction(() => { + // this.success = true; + // this.credentials = response2 as { user_info: { name: string; picture: string }; access_token: string }; + // }); + }, + }); } } diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 7373df473..75f904331 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -6,6 +6,7 @@ import * as request from 'request-promise'; import { Opt } from '../../../fields/Doc'; import { Database } from '../../database'; import { GoogleCredentialsLoader } from './CredentialsLoader'; +import { DashUserModel } from '../../authentication/DashUserModel'; /** * Scopes give Google users fine granularity of control @@ -13,7 +14,7 @@ import { GoogleCredentialsLoader } from './CredentialsLoader'; * This is the somewhat overkill list of what Dash requests * from the user. */ -const scope = ['documents.readonly', 'documents', 'presentations', 'presentations.readonly', 'drive', 'drive.file', 'photoslibrary', 'photoslibrary.appendonly', 'photoslibrary.sharing', 'userinfo.profile'].map( +const scope = ['tasks', 'documents.readonly', 'documents', 'presentations', 'presentations.readonly', 'drive', 'drive.file', 'photoslibrary', 'photoslibrary.appendonly', 'photoslibrary.sharing', 'userinfo.profile'].map( relative => `https://www.googleapis.com/auth/${relative}` ); @@ -118,8 +119,9 @@ export namespace GoogleApiServerUtils { * @param userId the id of the Dash user making the request to the API * @returns the relevant 'googleapis' wrapper, if any */ - export async function GetEndpoint(sector: string, userId: string): Promise { - const auth = await retrieveOAuthClient(userId); + export async function GetEndpoint(sector: string, user: DashUserModel): Promise { + if (!user.googleToken) await retrieveOAuthClient(user); + const auth = user.googleToken; // await retrieveOAuthClient(user); if (!auth) { return; } @@ -145,14 +147,14 @@ export namespace GoogleApiServerUtils { * npm-installed API wrappers that use authenticated client instances rather than access codes for * security. */ - export async function retrieveOAuthClient(userId: string): Promise { - const { credentials, refreshed } = await retrieveCredentials(userId); + export async function retrieveOAuthClient(user: DashUserModel): Promise { + const { credentials, refreshed } = await retrieveCredentials(user); if (!credentials) { return; } - let client = authenticationClients.get(userId); + let client = authenticationClients.get(user.id); if (!client) { - authenticationClients.set(userId, (client = generateClient(credentials))); + authenticationClients.set(user.id, (client = generateClient(credentials))); } else if (refreshed) { client.setCredentials(credentials); } @@ -181,7 +183,16 @@ export namespace GoogleApiServerUtils { * @returns the newly generated url to the authentication landing page */ export function generateAuthenticationUrl(): string { - return worker.generateAuthUrl({ scope, access_type: 'offline' }); + const oauth2Client = new google.auth.OAuth2( + '838617994486-a28072lirm8uk8cm78t7ic4krp0rgkgv.apps.googleusercontent.com', + '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', + 'http://localhost:1050/refreshGoogle' // Ensure this matches the redirect URI in Google Cloud Console + ); + + return oauth2Client.generateAuthUrl({ + access_type: 'offline', + scope: ['https://www.googleapis.com/auth/tasks'], + }); } /** @@ -267,15 +278,15 @@ export namespace GoogleApiServerUtils { * @returns the credentials, or undefined if the user has no stored associated credentials, * and a flag indicating whether or not they were refreshed during retrieval */ - export async function retrieveCredentials(userId: string): Promise<{ credentials: Opt; refreshed: boolean }> { - let credentials = await Database.Auxiliary.GoogleAccessToken.Fetch(userId); + export async function retrieveCredentials(user: DashUserModel): Promise<{ credentials: Opt; refreshed: boolean }> { + let credentials = await Database.Auxiliary.GoogleAccessToken.Fetch(user.id); let refreshed = false; if (!credentials) { return { credentials: undefined, refreshed }; } // check for token expiry if (credentials.expiry_date! <= new Date().getTime()) { - credentials = { ...credentials, ...(await refreshAccessToken(credentials, userId)) }; + credentials = { ...credentials, ...(await refreshAccessToken(credentials, user)) }; refreshed = true; } return { credentials, refreshed }; @@ -291,11 +302,11 @@ export namespace GoogleApiServerUtils { * his/her credentials be refreshed * @returns the updated credentials */ - async function refreshAccessToken(credentials: Credentials, userId: string): Promise { + async function refreshAccessToken(credentials: Credentials, user: DashUserModel): Promise { const headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; const { client_id, client_secret } = GoogleCredentialsLoader.ProjectCredentials; const params = new URLSearchParams({ - refresh_token: credentials.refresh_token!, + refresh_token: credentials.refresh_token!, // AARAV use user.googleToken client_id, client_secret, grant_type: 'refresh_token', @@ -306,7 +317,7 @@ export namespace GoogleApiServerUtils { }); // expires_in is in seconds, but we're building the new expiry date in milliseconds const expiry_date = new Date().getTime() + expires_in * 1000; - await Database.Auxiliary.GoogleAccessToken.Update(userId, access_token, expiry_date); + await Database.Auxiliary.GoogleAccessToken.Update(user.id, access_token, expiry_date); // update the relevant properties credentials.access_token = access_token; credentials.expiry_date = expiry_date; diff --git a/src/server/apis/google/google_project_credentials.json b/src/server/apis/google/google_project_credentials.json index 955c5a3c1..738e13647 100644 --- a/src/server/apis/google/google_project_credentials.json +++ b/src/server/apis/google/google_project_credentials.json @@ -1,11 +1,11 @@ { "installed": { - "client_id": "343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com", - "project_id": "quickstart-1565056383187", + "client_id": "838617994486-a28072lirm8uk8cm78t7ic4krp0rgkgv.apps.googleusercontent.com", + "project_id": "gtasks-test-dash", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_secret": "w8KIFSc0MQpmUYHed4qEzn8b", + "client_secret": "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", "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"] } } \ No newline at end of file diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts index debeef60c..4397e2bd4 100644 --- a/src/server/authentication/DashUserModel.ts +++ b/src/server/authentication/DashUserModel.ts @@ -9,9 +9,14 @@ export type DashUserModel = mongoose.Document & { passwordResetToken?: string; passwordResetExpires?: Date; + + // AARAV ADD + googleToken?: string; + dropboxRefresh?: string; dropboxToken?: string; + userDocumentId: string; sharingDocumentId: string; linkDatabaseId: string; @@ -40,6 +45,7 @@ const userSchema = new mongoose.Schema( passwordResetToken: String, passwordResetExpires: Date, + googleToken: String, dropboxRefresh: String, dropboxToken: String, userDocumentId: String, // id that identifies a document which hosts all of a user's account data -- cgit v1.2.3-70-g09d2 From 09be9002b5aa8f5ad7c602bcef6b53bbe0398cd3 Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Wed, 4 Jun 2025 18:28:35 -0400 Subject: fixed google authentication + linking and task creation via GT button --- src/client/apis/GoogleAuthenticationManager.tsx | 98 ++++++++++++++-------- src/client/views/nodes/TaskBox.tsx | 57 ++++++++++++- src/server/ApiManagers/GeneralGoogleManager.ts | 50 +++++++---- src/server/apis/google/GoogleApiServerUtils.ts | 10 ++- .../apis/google/google_project_credentials.json | 2 +- 5 files changed, 161 insertions(+), 56 deletions(-) (limited to 'src') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 1b1d6f734..a93e03e60 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -42,46 +42,72 @@ export class GoogleAuthenticationManager extends ObservableReactComponent { - const response = await Networking.FetchFromServer('/readGoogleAccessToken'); - // if this is an authentication url, activate the UI to register the new access token - if (new RegExp(AuthenticationUrl).test(response)) { - this.isOpen = true; - this.authenticationLink = response; - - // GETS STUCK AT THIS PROMISE!! - return new Promise(resolve => { - this.disposer?.(); - this.disposer = reaction( - () => this.authenticationCode, - async authenticationCode => { - if (authenticationCode && /\d{1}\/[\w-]{55}/.test(authenticationCode)) { - resolve(authenticationCode); - this.disposer?.(); - // const response2 = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode }); - // runInAction(() => { - // this.success = true; - // this.credentials = response2 as { user_info: { name: string; picture: string }; access_token: string }; - // }); - // resolve((response2 as { access_token: string }).access_token); - this.resetState(); - } - } - ); - }); - } + // public fetchOrGenerateAccessToken = async (displayIfFound = false) => { + // const response = await Networking.FetchFromServer('/readGoogleAccessToken'); + // // if this is an authentication url, activate the UI to register the new access token + + // if (new RegExp(AuthenticationUrl).test(response)) { + // this.isOpen = true; + // this.authenticationLink = response; - // otherwise, we already have a valid, stored access token and user info - const response2 = JSON.parse(response) as { user_info: { name: string; picture: string }; access_token: string }; - if (displayIfFound) { + // // GETS STUCK AT THIS PROMISE!! + // return new Promise(resolve => { + // this.disposer?.(); + // this.disposer = reaction( + // () => this.authenticationCode, + // async authenticationCode => { + // if (authenticationCode && /\d{1}\/[\w-]{55}/.test(authenticationCode)) { + // resolve(authenticationCode); + // this.disposer?.(); + // // const response2 = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode }); + // // runInAction(() => { + // // this.success = true; + // // this.credentials = response2 as { user_info: { name: string; picture: string }; access_token: string }; + // // }); + // // resolve((response2 as { access_token: string }).access_token); + // this.resetState(); + // } + // } + // ); + // }); + // } + + // // otherwise, we already have a valid, stored access token and user info + // const response2 = JSON.parse(response) as { user_info: { name: string; picture: string }; access_token: string }; + // if (displayIfFound) { + // runInAction(() => { + // this.success = true; + // this.credentials = response2; + // }); + // this.resetState(-1, -1); + // this.isOpen = true; + // } + // return (response2 as { access_token: string }).access_token; + // }; + + public fetchOrGenerateAccessToken = async (): Promise => { + const response = await Networking.FetchFromServer('/readGoogleAccessToken'); + + // This will return a JSON object with { access_token, user_info } if already linked + try { + const parsed = JSON.parse(response) as { access_token: string; user_info: { name: string; picture: string } }; + runInAction(() => { this.success = true; - this.credentials = response2; + this.credentials = parsed; }); - this.resetState(-1, -1); - this.isOpen = true; + + return parsed.access_token; + } catch (err) { + console.warn('Not linked yet or invalid JSON. Redirecting to auth...'); + // This is an auth URL β€” redirect the user to /refreshGoogle + if (typeof response === 'string' && response.startsWith('http')) { + window.location.href = response; + return ''; // Won’t be used β€” this page will reload anyway + } + + throw new Error('Unable to fetch Google access token.'); } - return (response2 as { access_token: string }).access_token; }; resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => { @@ -132,7 +158,7 @@ export class GoogleAuthenticationManager extends ObservableReactComponent ) : null} {this.showPasteTargetState ? (this.authenticationCode = e.currentTarget.value))} placeholder={prompt} /> : null} - {this.credentials ? ( + {this.credentials?.user_info?.picture ? ( <> Welcome to Dash, {this.credentials.user_info.name} diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx index 3990356b9..8855e43c8 100644 --- a/src/client/views/nodes/TaskBox.tsx +++ b/src/client/views/nodes/TaskBox.tsx @@ -6,6 +6,7 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { FieldView } from './FieldView'; import { DateField } from '../../../fields/DateField'; import { Doc } from '../../../fields/Doc'; +import { Networking } from '../../Network'; import './TaskBox.scss'; import { GoogleAuthenticationManager } from '../../apis/GoogleAuthenticationManager'; @@ -270,7 +271,7 @@ export class TaskBox extends React.Component { )} {/** test button */} - */} + + {/* */} + + ); diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts index e8debfc12..25589ccb5 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -71,24 +71,20 @@ export default class GeneralGoogleManager extends ApiManager { subscription: new RouteSubscriber('googleTasks').add('create'), secureHandler: async ({ req, res, user }) => { try { - const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user.id); - const access_token = user.googleToken || credentials?.access_token; // if googleToken expires, we need to renew it. - - if (!access_token) { - return res.status(401).send('Google access token not found.'); + const auth = await GoogleApiServerUtils.retrieveOAuthClient(user); + + if (!auth) { + return res.status(401).send('Google credentials missing or invalid.'); } - - const auth = new google.auth.OAuth2(); - auth.setCredentials({ access_token: access_token }); - + const tasks = google.tasks({ version: 'v1', auth }); - - const { title, notes, due } = req.body; + + const { title, notes, due, status, completed } = req.body; const result = await tasks.tasks.insert({ tasklist: '@default', - requestBody: { title, notes, due }, + requestBody: { title, notes, due, status, completed}, }); - + res.status(200).send(result.data); } catch (err) { console.error('Google Tasks error:', err); @@ -102,9 +98,31 @@ export default class GeneralGoogleManager extends ApiManager { subscription: '/refreshGoogle', secureHandler: async ({ user, req, res }) => { const code = req.query.code as string; - _success(res, code); - user.googleToken = code; - user.save(); + console.log('/refreshGoogle hit with code:', code); + + try { + const enriched = await GoogleApiServerUtils.processNewUser(user.id, code); + + if (enriched.refresh_token) { + console.log('Enriched credentials:', enriched); + + if (enriched.refresh_token) { + user.googleToken = enriched.refresh_token; + await user.save(); + console.log('Saved refresh token to user model'); + } else { + console.warn('No refresh token returned'); + } + } + + // await user.save(); + // _success(res, 'Google account successfully linked!'); + res.redirect('/home'); + + } catch (err) { + console.error('Failed to process Google code:', err); + res.status(500).send('Error linking Google account'); + } // const response2 = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode }); // runInAction(() => { diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 75f904331..56bc79119 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -59,6 +59,7 @@ export namespace GoogleApiServerUtils { */ export function processProjectCredentials(): void { const { client_secret: clientSecret, client_id: clientId, redirect_uris: redirectUris } = GoogleCredentialsLoader.ProjectCredentials; + console.log('Loaded Google redirect URIs:', redirectUris); // initialize the global authorization client oAuthOptions = { clientId, @@ -191,7 +192,12 @@ export namespace GoogleApiServerUtils { return oauth2Client.generateAuthUrl({ access_type: 'offline', - scope: ['https://www.googleapis.com/auth/tasks'], + scope: [ + 'https://www.googleapis.com/auth/tasks', + 'openid', + 'profile' + ], + prompt: 'consent', // This ensures we get a refresh token }); } @@ -306,7 +312,7 @@ export namespace GoogleApiServerUtils { const headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; const { client_id, client_secret } = GoogleCredentialsLoader.ProjectCredentials; const params = new URLSearchParams({ - refresh_token: credentials.refresh_token!, // AARAV use user.googleToken + refresh_token: credentials.refresh_token!, // AARAV use user.googleToken client_id, client_secret, grant_type: 'refresh_token', diff --git a/src/server/apis/google/google_project_credentials.json b/src/server/apis/google/google_project_credentials.json index 738e13647..010f9a626 100644 --- a/src/server/apis/google/google_project_credentials.json +++ b/src/server/apis/google/google_project_credentials.json @@ -6,6 +6,6 @@ "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "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", - "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"] + "redirect_uris": ["http://localhost:1050/refreshGoogle"] } } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 8d424c8cb4d178d5fb92b6543d63fa409eb6430b Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 4 Jun 2025 21:05:03 -0400 Subject: changed google authentication to not reload the page. --- src/client/apis/GoogleAuthenticationManager.tsx | 23 +++++++++--------- src/client/util/SettingsManager.tsx | 2 +- src/client/views/nodes/TaskBox.tsx | 11 ++++++++- src/server/ApiManagers/GeneralGoogleManager.ts | 27 +++++----------------- src/server/apis/google/GoogleApiServerUtils.ts | 16 +++++-------- .../apis/google/google_project_credentials.json | 8 +++---- 6 files changed, 39 insertions(+), 48 deletions(-) (limited to 'src') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index a93e03e60..ffec07512 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -1,4 +1,4 @@ -import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; +import { action, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Opt } from '../../fields/Doc'; @@ -8,7 +8,6 @@ import { MainViewModal } from '../views/MainViewModal'; import './GoogleAuthenticationManager.scss'; import { ObservableReactComponent } from '../views/ObservableReactComponent'; -const AuthenticationUrl = 'https://accounts.google.com/o/oauth2/v2/auth'; const prompt = 'Paste authorization code here...'; @observer @@ -85,27 +84,29 @@ export class GoogleAuthenticationManager extends ObservableReactComponent => { + public fetchOrGenerateAccessToken = async (): Promise => { const response = await Networking.FetchFromServer('/readGoogleAccessToken'); - + // This will return a JSON object with { access_token, user_info } if already linked try { const parsed = JSON.parse(response) as { access_token: string; user_info: { name: string; picture: string } }; - + runInAction(() => { this.success = true; this.credentials = parsed; }); - + return parsed.access_token; - } catch (err) { - console.warn('Not linked yet or invalid JSON. Redirecting to auth...'); + } catch { + console.warn('Not linked yet or invalid JSON. open auth...'); // This is an auth URL β€” redirect the user to /refreshGoogle if (typeof response === 'string' && response.startsWith('http')) { - window.location.href = response; - return ''; // Won’t be used β€” this page will reload anyway + if (window.confirm('Authorize Dash to access your Google tasks?')) { + window.open(response)?.focus(); + return undefined; + } } - + throw new Error('Unable to fetch Google access token.'); } }; diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 9e79fd870..f54dea90c 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -40,7 +40,7 @@ export class SettingsManager extends React.Component { @observable private _activeTab = 'Accounts'; @observable private _isOpen = false; - private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)); + private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken()); public closeMgr = action(() => { this._isOpen = false; diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx index 8855e43c8..df81d9c69 100644 --- a/src/client/views/nodes/TaskBox.tsx +++ b/src/client/views/nodes/TaskBox.tsx @@ -6,7 +6,6 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { FieldView } from './FieldView'; import { DateField } from '../../../fields/DateField'; import { Doc } from '../../../fields/Doc'; -import { Networking } from '../../Network'; import './TaskBox.scss'; import { GoogleAuthenticationManager } from '../../apis/GoogleAuthenticationManager'; @@ -324,6 +323,16 @@ export class TaskBox extends React.Component { console.log('GT button clicked'); try { const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); + if (token === undefined) { + const listener = () => { + if (confirm('βœ… Try again?')) { + // refactor this click function and call it again + } + window.removeEventListener('focusin', listener); + }; + setTimeout(() => window.addEventListener('focusin', listener), 100); + return; + } console.log('Got token', token); const response = await fetch('/googleTasks/create', { diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts index 25589ccb5..59d066934 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -1,5 +1,5 @@ import ApiManager, { Registration } from './ApiManager'; -import { Method, _success } from '../RouteManager'; +import { Method } from '../RouteManager'; import { GoogleApiServerUtils } from '../apis/google/GoogleApiServerUtils'; import RouteSubscriber from '../RouteSubscriber'; import { Database } from '../database'; @@ -72,19 +72,19 @@ export default class GeneralGoogleManager extends ApiManager { secureHandler: async ({ req, res, user }) => { try { const auth = await GoogleApiServerUtils.retrieveOAuthClient(user); - + if (!auth) { return res.status(401).send('Google credentials missing or invalid.'); } - + const tasks = google.tasks({ version: 'v1', auth }); - + const { title, notes, due, status, completed } = req.body; const result = await tasks.tasks.insert({ tasklist: '@default', - requestBody: { title, notes, due, status, completed}, + requestBody: { title, notes, due, status, completed }, }); - + res.status(200).send(result.data); } catch (err) { console.error('Google Tasks error:', err); @@ -98,37 +98,22 @@ export default class GeneralGoogleManager extends ApiManager { subscription: '/refreshGoogle', secureHandler: async ({ user, req, res }) => { const code = req.query.code as string; - console.log('/refreshGoogle hit with code:', code); try { const enriched = await GoogleApiServerUtils.processNewUser(user.id, code); if (enriched.refresh_token) { - console.log('Enriched credentials:', enriched); - if (enriched.refresh_token) { user.googleToken = enriched.refresh_token; await user.save(); - console.log('Saved refresh token to user model'); } else { console.warn('No refresh token returned'); } } - - // await user.save(); - // _success(res, 'Google account successfully linked!'); - res.redirect('/home'); - } catch (err) { console.error('Failed to process Google code:', err); res.status(500).send('Error linking Google account'); } - - // const response2 = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode }); - // runInAction(() => { - // this.success = true; - // this.credentials = response2 as { user_info: { name: string; picture: string }; access_token: string }; - // }); }, }); } diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 56bc79119..2f7ef473c 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -185,18 +185,14 @@ export namespace GoogleApiServerUtils { */ export function generateAuthenticationUrl(): string { const oauth2Client = new google.auth.OAuth2( - '838617994486-a28072lirm8uk8cm78t7ic4krp0rgkgv.apps.googleusercontent.com', - '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', + '740987818053-dtflji3hfkn5r9t8ad6jb8740pls8moh.apps.googleusercontent.com', + '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', 'http://localhost:1050/refreshGoogle' // Ensure this matches the redirect URI in Google Cloud Console ); return oauth2Client.generateAuthUrl({ access_type: 'offline', - scope: [ - 'https://www.googleapis.com/auth/tasks', - 'openid', - 'profile' - ], + scope: ['https://www.googleapis.com/auth/tasks', 'openid', 'profile'], prompt: 'consent', // This ensures we get a refresh token }); } @@ -220,12 +216,12 @@ export namespace GoogleApiServerUtils { */ export async function processNewUser(userId: string, authenticationCode: string): Promise { const credentials = await new Promise((resolve, reject) => { - worker.getToken(authenticationCode, (err, credentials) => { - if (err || !credentials) { + worker.getToken(authenticationCode, (err, creds) => { + if (err || !creds) { reject(err); return; } - resolve(credentials); + resolve(creds); }); }); const enriched = injectUserInfo(credentials); diff --git a/src/server/apis/google/google_project_credentials.json b/src/server/apis/google/google_project_credentials.json index 010f9a626..8abc13b80 100644 --- a/src/server/apis/google/google_project_credentials.json +++ b/src/server/apis/google/google_project_credentials.json @@ -1,11 +1,11 @@ { "installed": { - "client_id": "838617994486-a28072lirm8uk8cm78t7ic4krp0rgkgv.apps.googleusercontent.com", - "project_id": "gtasks-test-dash", + "client_id": "740987818053-dtflji3hfkn5r9t8ad6jb8740pls8moh.apps.googleusercontent.com", + "project_id": "dash-web-461920", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_secret": "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", + "client_secret": "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", "redirect_uris": ["http://localhost:1050/refreshGoogle"] } -} \ No newline at end of file +} -- cgit v1.2.3-70-g09d2 From eb9c93a635191ef9ec842592c4a85262811cf108 Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Wed, 4 Jun 2025 21:05:47 -0400 Subject: allowed for incomplete tasks + fix for all day tasks not being added correctly + button styling --- src/client/views/nodes/TaskBox.scss | 30 +++++++-- src/client/views/nodes/TaskBox.tsx | 130 ++++++++++++------------------------ 2 files changed, 68 insertions(+), 92 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/TaskBox.scss b/src/client/views/nodes/TaskBox.scss index 6ef0c6454..a7f75acdb 100644 --- a/src/client/views/nodes/TaskBox.scss +++ b/src/client/views/nodes/TaskBox.scss @@ -40,7 +40,7 @@ resize: none; line-height: 1.4; resize: none; - flex-grow: 1 + flex-grow: 1; } .task-manager-checkboxes { @@ -49,7 +49,8 @@ gap: 16px; } -.task-manager-allday, .task-manager-complete { +.task-manager-allday, +.task-manager-complete { display: flex; align-items: center; gap: 6px; @@ -71,11 +72,32 @@ gap: 4px; } -input[type="datetime-local"] { +input[type='datetime-local'] { width: 100%; font-size: 0.9rem; padding: 6px 8px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; -} \ No newline at end of file +} + +.task-manager-checkboxes { + display: flex; + flex-wrap: wrap; /* allows wrapping on small screens */ + align-items: center; + gap: 16px; + row-gap: 8px; /* optional: tighter vertical spacing if it wraps */ +} + +.task-manager-google { + font-size: 0.85rem; + padding: 6px 12px; + border-radius: 6px; + background-color: #4285f4; + color: white; + border: none; + cursor: pointer; + white-space: nowrap; + transition: background-color 0.2s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx index 8855e43c8..0d4fb622b 100644 --- a/src/client/views/nodes/TaskBox.tsx +++ b/src/client/views/nodes/TaskBox.tsx @@ -255,6 +255,48 @@ export class TaskBox extends React.Component { Complete + + {!allDay && ( @@ -269,94 +311,6 @@ export class TaskBox extends React.Component { )} - - {/** test button */} - {/* */} - - {/* */} - - ); } -- cgit v1.2.3-70-g09d2 From f7cb0dcebb0514cf38f8a7e635ec9959c196145a Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 4 Jun 2025 21:32:34 -0400 Subject: cleaned up getting client id/secret for google. fixed final message after going through authentication process. --- src/server/ApiManagers/GeneralGoogleManager.ts | 39 +++++++++++++------------- src/server/apis/google/GoogleApiServerUtils.ts | 13 ++------- 2 files changed, 22 insertions(+), 30 deletions(-) (limited to 'src') diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts index 59d066934..693b17779 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -96,25 +96,26 @@ export default class GeneralGoogleManager extends ApiManager { register({ method: Method.GET, subscription: '/refreshGoogle', - secureHandler: async ({ user, req, res }) => { - const code = req.query.code as string; - - try { - const enriched = await GoogleApiServerUtils.processNewUser(user.id, code); - - if (enriched.refresh_token) { - if (enriched.refresh_token) { - user.googleToken = enriched.refresh_token; - await user.save(); - } else { - console.warn('No refresh token returned'); - } - } - } catch (err) { - console.error('Failed to process Google code:', err); - res.status(500).send('Error linking Google account'); - } - }, + secureHandler: async ({ user, req, res }) => + new Promise(resolve => + GoogleApiServerUtils.processNewUser(user.id, req.query.code as string) + .then(enriched => { + if (enriched.refresh_token) { + if (enriched.refresh_token) { + user.googleToken = enriched.refresh_token; + user.save(); + } else { + console.warn('No refresh token returned'); + } + } + res.status(200).send('Google account linked successfully!'); + }) + .catch(err => { + console.error('Failed to process Google code:', err); + res.status(500).send('Error linking Google account'); + }) + .finally(resolve) + ), }); } } diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 2f7ef473c..45c661730 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -14,6 +14,7 @@ import { DashUserModel } from '../../authentication/DashUserModel'; * This is the somewhat overkill list of what Dash requests * from the user. */ +// 'https://www.googleapis.com/auth/tasks', 'openid', 'profile' const scope = ['tasks', 'documents.readonly', 'documents', 'presentations', 'presentations.readonly', 'drive', 'drive.file', 'photoslibrary', 'photoslibrary.appendonly', 'photoslibrary.sharing', 'userinfo.profile'].map( relative => `https://www.googleapis.com/auth/${relative}` ); @@ -184,17 +185,7 @@ export namespace GoogleApiServerUtils { * @returns the newly generated url to the authentication landing page */ export function generateAuthenticationUrl(): string { - const oauth2Client = new google.auth.OAuth2( - '740987818053-dtflji3hfkn5r9t8ad6jb8740pls8moh.apps.googleusercontent.com', - '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', - 'http://localhost:1050/refreshGoogle' // Ensure this matches the redirect URI in Google Cloud Console - ); - - return oauth2Client.generateAuthUrl({ - access_type: 'offline', - scope: ['https://www.googleapis.com/auth/tasks', 'openid', 'profile'], - prompt: 'consent', // This ensures we get a refresh token - }); + return worker.generateAuthUrl({ scope, access_type: 'offline', prompt: 'consent' }); } /** -- cgit v1.2.3-70-g09d2 From bd75348c3ad851e05200cf138eaee3b4b349758f Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 4 Jun 2025 21:44:43 -0400 Subject: from last --- src/client/apis/GoogleAuthenticationManager.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'src') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index ffec07512..70e1ccc05 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -183,11 +183,7 @@ export class GoogleAuthenticationManager extends ObservableReactComponent - (this.isOpen = false))} /> - - ); + return (this.isOpen = false))} />; } } -- cgit v1.2.3-70-g09d2 From 59689fe94c27986674dd6ecb7f0e6073861a98a6 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 4 Jun 2025 22:07:36 -0400 Subject: more cleanup of google authorization --- src/server/ApiManagers/GeneralGoogleManager.ts | 12 +----------- src/server/apis/google/GoogleApiServerUtils.ts | 7 +++---- src/server/authentication/DashUserModel.ts | 6 ------ 3 files changed, 4 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts index 693b17779..7581eec13 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -99,17 +99,7 @@ export default class GeneralGoogleManager extends ApiManager { secureHandler: async ({ user, req, res }) => new Promise(resolve => GoogleApiServerUtils.processNewUser(user.id, req.query.code as string) - .then(enriched => { - if (enriched.refresh_token) { - if (enriched.refresh_token) { - user.googleToken = enriched.refresh_token; - user.save(); - } else { - console.warn('No refresh token returned'); - } - } - res.status(200).send('Google account linked successfully!'); - }) + .then(() => res.status(200).send('Google account linked successfully!')) .catch(err => { console.error('Failed to process Google code:', err); res.status(500).send('Error linking Google account'); diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 45c661730..24905896d 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -14,7 +14,6 @@ import { DashUserModel } from '../../authentication/DashUserModel'; * This is the somewhat overkill list of what Dash requests * from the user. */ -// 'https://www.googleapis.com/auth/tasks', 'openid', 'profile' const scope = ['tasks', 'documents.readonly', 'documents', 'presentations', 'presentations.readonly', 'drive', 'drive.file', 'photoslibrary', 'photoslibrary.appendonly', 'photoslibrary.sharing', 'userinfo.profile'].map( relative => `https://www.googleapis.com/auth/${relative}` ); @@ -122,8 +121,7 @@ export namespace GoogleApiServerUtils { * @returns the relevant 'googleapis' wrapper, if any */ export async function GetEndpoint(sector: string, user: DashUserModel): Promise { - if (!user.googleToken) await retrieveOAuthClient(user); - const auth = user.googleToken; // await retrieveOAuthClient(user); + const auth = await retrieveOAuthClient(user); if (!auth) { return; } @@ -299,7 +297,7 @@ export namespace GoogleApiServerUtils { const headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; const { client_id, client_secret } = GoogleCredentialsLoader.ProjectCredentials; const params = new URLSearchParams({ - refresh_token: credentials.refresh_token!, // AARAV use user.googleToken + refresh_token: credentials.refresh_token!, client_id, client_secret, grant_type: 'refresh_token', @@ -308,6 +306,7 @@ export namespace GoogleApiServerUtils { const { access_token, expires_in } = await new Promise<{ access_token: string; expires_in: number }>(resolve => { request.post(url, headerParameters).then(response => resolve(JSON.parse(response))); }); + // expires_in is in seconds, but we're building the new expiry date in milliseconds const expiry_date = new Date().getTime() + expires_in * 1000; await Database.Auxiliary.GoogleAccessToken.Update(user.id, access_token, expiry_date); diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts index 4397e2bd4..debeef60c 100644 --- a/src/server/authentication/DashUserModel.ts +++ b/src/server/authentication/DashUserModel.ts @@ -9,14 +9,9 @@ export type DashUserModel = mongoose.Document & { passwordResetToken?: string; passwordResetExpires?: Date; - - // AARAV ADD - googleToken?: string; - dropboxRefresh?: string; dropboxToken?: string; - userDocumentId: string; sharingDocumentId: string; linkDatabaseId: string; @@ -45,7 +40,6 @@ const userSchema = new mongoose.Schema( passwordResetToken: String, passwordResetExpires: Date, - googleToken: String, dropboxRefresh: String, dropboxToken: String, userDocumentId: String, // id that identifies a document which hosts all of a user's account data -- cgit v1.2.3-70-g09d2 From d917449cb195fd151f6c3558a476a95e6675e2f3 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 4 Jun 2025 22:30:58 -0400 Subject: more typing/cleanup for google authentication and dashUserModel --- src/server/ApiManagers/GeneralGoogleManager.ts | 6 +++--- src/server/apis/google/GoogleApiServerUtils.ts | 24 +++++++++++------------- src/server/authentication/DashUserModel.ts | 14 ++++++++------ src/server/authentication/Passport.ts | 6 +++--- 4 files changed, 25 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts index 7581eec13..81efc3eb5 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -17,7 +17,7 @@ export default class GeneralGoogleManager extends ApiManager { method: Method.GET, subscription: '/readGoogleAccessToken', secureHandler: async ({ user, res }) => { - const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user); + const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user.id as string); if (!credentials?.access_token) { const url = GoogleApiServerUtils.generateAuthenticationUrl(); return res.send(url); @@ -49,7 +49,7 @@ export default class GeneralGoogleManager extends ApiManager { secureHandler: async ({ req, res, user }) => { const sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service; const action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action; - const endpoint = await GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user); + const endpoint = await GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user.id); const handler = EndpointHandlerMap.get(action); if (endpoint && handler) { try { @@ -71,7 +71,7 @@ export default class GeneralGoogleManager extends ApiManager { subscription: new RouteSubscriber('googleTasks').add('create'), secureHandler: async ({ req, res, user }) => { try { - const auth = await GoogleApiServerUtils.retrieveOAuthClient(user); + const auth = await GoogleApiServerUtils.retrieveOAuthClient(user.id); if (!auth) { return res.status(401).send('Google credentials missing or invalid.'); diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 24905896d..ad0f0e580 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -6,7 +6,6 @@ import * as request from 'request-promise'; import { Opt } from '../../../fields/Doc'; import { Database } from '../../database'; import { GoogleCredentialsLoader } from './CredentialsLoader'; -import { DashUserModel } from '../../authentication/DashUserModel'; /** * Scopes give Google users fine granularity of control @@ -59,7 +58,6 @@ export namespace GoogleApiServerUtils { */ export function processProjectCredentials(): void { const { client_secret: clientSecret, client_id: clientId, redirect_uris: redirectUris } = GoogleCredentialsLoader.ProjectCredentials; - console.log('Loaded Google redirect URIs:', redirectUris); // initialize the global authorization client oAuthOptions = { clientId, @@ -120,8 +118,8 @@ export namespace GoogleApiServerUtils { * @param userId the id of the Dash user making the request to the API * @returns the relevant 'googleapis' wrapper, if any */ - export async function GetEndpoint(sector: string, user: DashUserModel): Promise { - const auth = await retrieveOAuthClient(user); + export async function GetEndpoint(sector: string, userId: string): Promise { + const auth = await retrieveOAuthClient(userId); if (!auth) { return; } @@ -147,14 +145,14 @@ export namespace GoogleApiServerUtils { * npm-installed API wrappers that use authenticated client instances rather than access codes for * security. */ - export async function retrieveOAuthClient(user: DashUserModel): Promise { - const { credentials, refreshed } = await retrieveCredentials(user); + export async function retrieveOAuthClient(userId: string): Promise { + const { credentials, refreshed } = await retrieveCredentials(userId); if (!credentials) { return; } - let client = authenticationClients.get(user.id); + let client = authenticationClients.get(userId); if (!client) { - authenticationClients.set(user.id, (client = generateClient(credentials))); + authenticationClients.set(userId, (client = generateClient(credentials))); } else if (refreshed) { client.setCredentials(credentials); } @@ -269,15 +267,15 @@ export namespace GoogleApiServerUtils { * @returns the credentials, or undefined if the user has no stored associated credentials, * and a flag indicating whether or not they were refreshed during retrieval */ - export async function retrieveCredentials(user: DashUserModel): Promise<{ credentials: Opt; refreshed: boolean }> { - let credentials = await Database.Auxiliary.GoogleAccessToken.Fetch(user.id); + export async function retrieveCredentials(userId: string): Promise<{ credentials: Opt; refreshed: boolean }> { + let credentials = await Database.Auxiliary.GoogleAccessToken.Fetch(userId); let refreshed = false; if (!credentials) { return { credentials: undefined, refreshed }; } // check for token expiry if (credentials.expiry_date! <= new Date().getTime()) { - credentials = { ...credentials, ...(await refreshAccessToken(credentials, user)) }; + credentials = { ...credentials, ...(await refreshAccessToken(credentials, userId)) }; refreshed = true; } return { credentials, refreshed }; @@ -293,7 +291,7 @@ export namespace GoogleApiServerUtils { * his/her credentials be refreshed * @returns the updated credentials */ - async function refreshAccessToken(credentials: Credentials, user: DashUserModel): Promise { + async function refreshAccessToken(credentials: Credentials, userId: string): Promise { const headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; const { client_id, client_secret } = GoogleCredentialsLoader.ProjectCredentials; const params = new URLSearchParams({ @@ -309,7 +307,7 @@ export namespace GoogleApiServerUtils { // expires_in is in seconds, but we're building the new expiry date in milliseconds const expiry_date = new Date().getTime() + expires_in * 1000; - await Database.Auxiliary.GoogleAccessToken.Update(user.id, access_token, expiry_date); + await Database.Auxiliary.GoogleAccessToken.Update(userId, access_token, expiry_date); // update the relevant properties credentials.access_token = access_token; credentials.expiry_date = expiry_date; diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts index debeef60c..6fd8dd593 100644 --- a/src/server/authentication/DashUserModel.ts +++ b/src/server/authentication/DashUserModel.ts @@ -2,9 +2,10 @@ import * as bcrypt from 'bcrypt-nodejs'; import * as mongoose from 'mongoose'; import { Utils } from '../../Utils'; -type comparePasswordFunction = (candidatePassword: string, cb: (err: any, isMatch: any) => void) => void; -export type DashUserModel = mongoose.Document & { - email: String; +type comparePasswordFunction = (candidatePassword: string, cb: (err: Error, isMatch: boolean) => void) => void; +type mongooseDocument = { id: string }; // & mongoose.Document; +export type DashUserModel = mongooseDocument & { + email: string; password: string; passwordResetToken?: string; passwordResetExpires?: Date; @@ -65,12 +66,13 @@ const userSchema = new mongoose.Schema( /** * Password hash middleware. */ -userSchema.pre('save', function save(next) { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +userSchema.pre('save', function save(next: any) { const user = this; if (!user.isModified('password')) { return next(); } - bcrypt.genSalt(10, (err: any, salt: string) => { + bcrypt.genSalt(10, (err: Error, salt: string) => { if (err) { return next(err); } @@ -102,7 +104,7 @@ const comparePassword: comparePasswordFunction = function (this: DashUserModel, userSchema.methods.comparePassword = comparePassword; -const User: any = mongoose.model('User', userSchema); +const User = mongoose.model('User', userSchema); export function initializeGuest() { new User({ email: 'guest', diff --git a/src/server/authentication/Passport.ts b/src/server/authentication/Passport.ts index ca9e3058e..a62d38e3e 100644 --- a/src/server/authentication/Passport.ts +++ b/src/server/authentication/Passport.ts @@ -5,13 +5,13 @@ import User, { DashUserModel } from './DashUserModel'; const LocalStrategy = passportLocal.Strategy; passport.serializeUser((req, user, done) => { - done(undefined, (user as any)?.id); + done(undefined, (user as DashUserModel)?.id); }); passport.deserializeUser((id, done) => { User.findById(id) .exec() - .then((user: any) => done(undefined, user)); + .then((user: DashUserModel) => done(undefined, user)); }); // AUTHENTICATE JUST WITH EMAIL AND PASSWORD @@ -30,6 +30,6 @@ passport.use( }); } }) - .catch((error: any) => done(error)); + .catch((error: Error) => done(error)); }) ); -- cgit v1.2.3-70-g09d2 From 55d1653c0d6c19bddefb9e57753374d43d8f23dc Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 4 Jun 2025 22:32:21 -0400 Subject: from last --- src/server/ApiManagers/GeneralGoogleManager.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts index 81efc3eb5..4f0b8c02b 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -17,10 +17,9 @@ export default class GeneralGoogleManager extends ApiManager { method: Method.GET, subscription: '/readGoogleAccessToken', secureHandler: async ({ user, res }) => { - const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user.id as string); + const { credentials } = await GoogleApiServerUtils.retrieveCredentials(user.id); if (!credentials?.access_token) { - const url = GoogleApiServerUtils.generateAuthenticationUrl(); - return res.send(url); + return res.send(GoogleApiServerUtils.generateAuthenticationUrl()); } return res.send(credentials); }, -- cgit v1.2.3-70-g09d2 From 87bd169df4a4384e41b32b3252b3b3c6618f4510 Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:25:20 -0400 Subject: fixed being able to add tasks to Google Tasks. Cleaned up google authorization button clicks + alerts --- src/client/apis/GoogleAuthenticationManager.tsx | 2 +- src/client/views/nodes/TaskBox.tsx | 109 ++++++++++++++------- .../apis/google/google_project_credentials.json | 7 +- 3 files changed, 77 insertions(+), 41 deletions(-) (limited to 'src') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 70e1ccc05..23e658329 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -101,7 +101,7 @@ export class GoogleAuthenticationManager extends ObservableReactComponent { const isCompleted = !!this.props.Document.$task_completed; const startTime = doc.$task_startTime instanceof DateField && doc.$task_startTime.date instanceof Date ? toLocalDateTimeString(doc.$task_startTime.date) : ''; - const endTime = doc.$task_endTime instanceof DateField && doc.$task_endTime.date instanceof Date ? toLocalDateTimeString(doc.$task_endTime.date) : ''; + const handleGoogleTaskClick = async () => { + console.log('GT button clicked'); + + try { + const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); + if (!token) { + const listener = () => { + window.removeEventListener('focusin', listener); + if (confirm('βœ… Authorization complete. Try adding the task again?')) { + // you could refactor the click handler here + handleGoogleTaskClick(); + } + window.removeEventListener('focusin', listener); + }; + setTimeout(() => window.addEventListener('focusin', listener), 100); + return; + } + + let due: string | undefined; + + if (allDay) { + const rawRange = typeof doc.$task_dateRange === 'string' ? doc.$task_dateRange : ''; + const datePart = rawRange.split('|')[0]; + + if (datePart && !isNaN(new Date(datePart).getTime())) { + // Set time to midnight UTC to represent the start of the all-day event + const baseDate = datePart.includes('T') ? datePart : datePart + 'T00:00:00Z'; + due = new Date(baseDate).toISOString(); + } else { + due = undefined; + } + } else if (doc.$task_endTime instanceof DateField && doc.$task_endTime.date) { + due = doc.$task_endTime.date.toISOString(); + } else if (doc.$task_startTime instanceof DateField && doc.$task_startTime.date) { + due = doc.$task_startTime.date.toISOString(); + } else { + due = undefined; + } + + const response = await fetch('/googleTasks/create', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + title: taskTitle || 'Untitled Task', + notes: taskDesc, + due, + status: doc.$task_completed ? 'completed' : 'needsAction', + completed: doc.$task_completed ? new Date().toISOString() : undefined, + }), + }); + + const result = await response.json(); + console.log('Google Task result:', result); + + if (result?.id) { + alert('βœ… Task sent to Google Tasks!'); + } else { + alert(`❌ Failed: ${result?.error?.message || 'Unknown error'}`); + } + } catch (err) { + console.error('Fetch error:', err); + alert('❌ Task creation failed.'); + } + }; + return (
@@ -257,44 +325,11 @@ export class TaskBox extends React.Component {
diff --git a/src/server/apis/google/google_project_credentials.json b/src/server/apis/google/google_project_credentials.json index 8abc13b80..8a9a38dac 100644 --- a/src/server/apis/google/google_project_credentials.json +++ b/src/server/apis/google/google_project_credentials.json @@ -1,11 +1,12 @@ { "installed": { - "client_id": "740987818053-dtflji3hfkn5r9t8ad6jb8740pls8moh.apps.googleusercontent.com", - "project_id": "dash-web-461920", + "client_id": "838617994486-a28072lirm8uk8cm78t7ic4krp0rgkgv.apps.googleusercontent.com", + "project_id": "gtasks-test-dash", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_secret": "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", + "client_secret": "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", "redirect_uris": ["http://localhost:1050/refreshGoogle"] } } + -- cgit v1.2.3-70-g09d2 From d73f7d9960285aceec01ef41a80bbb19e5d86f8c Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:35:57 -0400 Subject: style changes --- src/client/views/nodes/TaskBox.scss | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/TaskBox.scss b/src/client/views/nodes/TaskBox.scss index a7f75acdb..cc04f7973 100644 --- a/src/client/views/nodes/TaskBox.scss +++ b/src/client/views/nodes/TaskBox.scss @@ -1,4 +1,5 @@ .task-manager-container { + color-scheme: light; display: flex; flex-direction: column; padding: 8px; @@ -6,15 +7,6 @@ width: 100%; height: 100%; box-sizing: border-box; - - input, - textarea, - select, - button { - background-color: #fff !important; - color: #000 !important; - border-color: #ccc !important; - } } .task-manager-title { @@ -93,11 +85,17 @@ input[type='datetime-local'] { font-size: 0.85rem; padding: 6px 12px; border-radius: 6px; - background-color: #4285f4; + background-color: #5e88c8; color: white; border: none; cursor: pointer; white-space: nowrap; transition: background-color 0.2s ease; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + + &:hover { + background-color: #4773b0; // darker shade of your base blue + color: white; + transform: scale(1.01); // subtle hover feel without real size change + } } -- cgit v1.2.3-70-g09d2 From 1ec0291a689ed0aea0bb6fdd91e3c08113bfac46 Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:34:53 -0400 Subject: changed add to google tasks button to sync to google button. Implemented auto creation and auto deletion in Google when node is created/deleted in dash --- src/client/documents/Documents.ts | 1 + src/client/views/nodes/TaskBox.tsx | 97 +++++++++++++++++++++++--- src/server/ApiManagers/GeneralGoogleManager.ts | 65 ++++++++++++++++- src/server/RouteManager.ts | 8 +++ 4 files changed, 160 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 4ad9c9bd8..120ad0688 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -528,6 +528,7 @@ export class DocumentOptions { $task_endTime?: DateInfo | DateField = new DateInfo('end date and time', /*filterable*/ false); $task_allDay?: BoolInfo | boolean = new BoolInfo('whether task is all-day or not', /*filterable*/ false); $task_completed?: BoolInfo | boolean = new BoolInfo('whether the task is completed', /*filterable*/ false); + $googleTaskId?: STRt = new StrInfo('Google Task ID for syncing'); _calendar_date?: DateInfo | DateField = new DateInfo('current selected date of a calendar', /*filterable*/ false); _calendar_dateRange?: STRt = new StrInfo('date range shown on a calendar', false); diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx index b59599052..7df3876c3 100644 --- a/src/client/views/nodes/TaskBox.tsx +++ b/src/client/views/nodes/TaskBox.tsx @@ -159,13 +159,62 @@ export class TaskBox extends React.Component { makeObservable(this); } + _googleTaskCreateDisposer?: IReactionDisposer; _heightDisposer?: IReactionDisposer; _widthDisposer?: IReactionDisposer; componentDidMount() { this.setTaskDateRange(); - const doc = this.props.Document; + + // adding task on creation to google + + (async () => { + if (!doc.$googleTaskId && doc.title) { + try { + const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); + if (!token) return; + + const body: any = { + title: doc.title || 'Untitled Task', + notes: doc.text || '', + status: doc.$task_completed ? 'completed' : 'needsAction', + completed: doc.$task_completed ? new Date().toISOString() : undefined, + }; + + if (doc.$task_allDay && typeof doc.$task_dateRange === 'string') { + const datePart = doc.$task_dateRange.split('|')[0]; + if (datePart && !isNaN(new Date(datePart).getTime())) { + const baseDate = datePart.includes('T') ? datePart : datePart + 'T00:00:00Z'; + body.due = new Date(baseDate).toISOString(); + } + } else if (doc.$task_endTime instanceof DateField) { + body.due = doc.$task_endTime.date.toISOString(); + } + + const res = await fetch('/googleTasks/create', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(body), + }); + + const result = await res.json(); + if (result?.id) { + doc.$googleTaskId = result.id; + console.log('βœ… Google Task created on mount:', result); + } else { + console.warn('❌ Google Task creation failed:', result); + } + } catch (err) { + console.error('❌ Error creating Google Task:', err); + } + } + })(); + this._heightDisposer = reaction( () => Number(doc._height), height => { @@ -188,8 +237,32 @@ export class TaskBox extends React.Component { } componentWillUnmount() { + const doc = this.props.Document; + this._googleTaskCreateDisposer?.(); this._heightDisposer?.(); this._widthDisposer?.(); + + // task deletion + if (doc.$googleTaskId) { + (async () => { + try { + const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); + if (!token) return; + + await fetch(`/googleTasks/${doc.$googleTaskId}`, { + method: 'DELETE', + credentials: 'include', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + console.log(`βœ… Deleted Google Task ${doc.$googleTaskId}`); + } catch (err) { + console.warn('❌ Failed to delete Google Task:', err); + } + })(); + } } /** @@ -213,7 +286,7 @@ export class TaskBox extends React.Component { const startTime = doc.$task_startTime instanceof DateField && doc.$task_startTime.date instanceof Date ? toLocalDateTimeString(doc.$task_startTime.date) : ''; const endTime = doc.$task_endTime instanceof DateField && doc.$task_endTime.date instanceof Date ? toLocalDateTimeString(doc.$task_endTime.date) : ''; - const handleGoogleTaskClick = async () => { + const handleGoogleTaskSync = async () => { console.log('GT button clicked'); try { @@ -221,9 +294,9 @@ export class TaskBox extends React.Component { if (!token) { const listener = () => { window.removeEventListener('focusin', listener); - if (confirm('βœ… Authorization complete. Try adding the task again?')) { + if (confirm('βœ… Authorization complete. Try syncing the task again?')) { // you could refactor the click handler here - handleGoogleTaskClick(); + handleGoogleTaskSync(); } window.removeEventListener('focusin', listener); }; @@ -252,8 +325,12 @@ export class TaskBox extends React.Component { due = undefined; } - const response = await fetch('/googleTasks/create', { - method: 'POST', + const isUpdate = !!doc.$googleTaskId; + const endpoint = isUpdate ? `/googleTasks/${doc.$googleTaskId}` : '/googleTasks/create'; + const method = isUpdate ? 'PATCH' : 'POST'; + + const response = await fetch(endpoint, { + method, credentials: 'include', headers: { 'Content-Type': 'application/json', @@ -272,13 +349,13 @@ export class TaskBox extends React.Component { console.log('Google Task result:', result); if (result?.id) { - alert('βœ… Task sent to Google Tasks!'); + alert('βœ… Task synced with Google Tasks!'); } else { alert(`❌ Failed: ${result?.error?.message || 'Unknown error'}`); } } catch (err) { console.error('Fetch error:', err); - alert('❌ Task creation failed.'); + alert('❌ Task syncing failed.'); } }; @@ -327,9 +404,9 @@ export class TaskBox extends React.Component { className="task-manager-google" onClick={event => { event.preventDefault(); - handleGoogleTaskClick(); + handleGoogleTaskSync(); }}> - Add to Google Tasks + Sync to Google diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts index 4f0b8c02b..79ca55444 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -63,8 +63,9 @@ export default class GeneralGoogleManager extends ApiManager { }, }); - // AARAV ADD (creating a task) + // AARAV ADD + // Task Creation register({ method: Method.POST, subscription: new RouteSubscriber('googleTasks').add('create'), @@ -92,6 +93,68 @@ export default class GeneralGoogleManager extends ApiManager { }, }); + // Task Update + register({ + method: Method.PATCH, + subscription: new RouteSubscriber('googleTasks').add('taskId'), + // any way to add static params? like /update (this is not very descriptive) + secureHandler: async ({ req, res, user }) => { + try { + const auth = await GoogleApiServerUtils.retrieveOAuthClient(user.id); + + if (!auth) { + return res.status(401).send('Google credentials missing or invalid.'); + } + + const tasks = google.tasks({ version: 'v1', auth }); + + const { taskId } = req.params; + const { title, notes, due, status, completed } = req.body; + + const result = await tasks.tasks.patch({ + tasklist: '@default', + task: taskId, + requestBody: { title, notes, due, status, completed }, + }); + + res.status(200).send(result.data); + } catch (err) { + console.error('Google Tasks update error:', err); + res.status(500).send('Failed to update task.'); + } + }, + }); + + // Task Deletion + register({ + method: Method.DELETE, + subscription: new RouteSubscriber('googleTasks').add('taskId'), + secureHandler: async ({ req, res, user }) => { + try { + const auth = await GoogleApiServerUtils.retrieveOAuthClient(user.id); + + if (!auth) { + return res.status(401).send('Google credentials missing or invalid.'); + } + + const tasks = google.tasks({ version: 'v1', auth }); + const { taskId } = req.params; + + await tasks.tasks.delete({ + tasklist: '@default', + task: taskId, + }); + + res.status(200).send({ success: true }); + } catch (err) { + console.error('Google Tasks delete error:', err); + res.status(500).send('Failed to delete task.'); + } + }, + }); + + + register({ method: Method.GET, subscription: '/refreshGoogle', diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts index 2f6cf80b5..c5d70da3d 100644 --- a/src/server/RouteManager.ts +++ b/src/server/RouteManager.ts @@ -8,6 +8,8 @@ import { DashUserModel } from './authentication/DashUserModel'; export enum Method { GET, POST, + PATCH, + DELETE, } export interface CoreArguments { @@ -207,6 +209,12 @@ export default class RouteManager { case Method.POST: this.server.post(route, supervised); break; + case Method.PATCH: + this.server.patch(route, supervised); + break; + case Method.DELETE: + this.server.delete(route, supervised); + break; default: } } -- cgit v1.2.3-70-g09d2 From 353fcddb918ee9ccaedd69032f9212fc66c04a7f Mon Sep 17 00:00:00 2001 From: Skitty1238 <157652284+Skitty1238@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:54:09 -0400 Subject: sync button only accessible when a change is made to the task (to counter unnecessary syncing) --- src/client/views/nodes/TaskBox.scss | 8 +++++- src/client/views/nodes/TaskBox.tsx | 55 +++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/nodes/TaskBox.scss b/src/client/views/nodes/TaskBox.scss index cc04f7973..071fe1f7c 100644 --- a/src/client/views/nodes/TaskBox.scss +++ b/src/client/views/nodes/TaskBox.scss @@ -92,10 +92,16 @@ input[type='datetime-local'] { white-space: nowrap; transition: background-color 0.2s ease; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - + &:hover { background-color: #4773b0; // darker shade of your base blue color: white; transform: scale(1.01); // subtle hover feel without real size change } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; + } } diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx index 7df3876c3..82c22057e 100644 --- a/src/client/views/nodes/TaskBox.tsx +++ b/src/client/views/nodes/TaskBox.tsx @@ -23,6 +23,23 @@ interface TaskBoxProps { */ @observer export class TaskBox extends React.Component { + // contains the last synced task information + lastSyncedTask: { + title: string; + text: string; + due?: string; + completed: boolean; + } = { + title: '', + text: '', + due: '', + completed: false, + }; + + state = { + needsSync: false, + }; + /** * Method to reuturn the * @param fieldStr @@ -234,6 +251,34 @@ export class TaskBox extends React.Component { } } ); + + this._googleTaskCreateDisposer = reaction( + () => { + const { title, text, $task_completed, $task_dateRange, $task_startTime, $task_endTime, $task_allDay } = doc; + + const completed = !!$task_completed; + let due: string | undefined; + + if ($task_allDay && typeof $task_dateRange === 'string') { + const datePart = $task_dateRange.split('|')[0]; + if (datePart && !isNaN(new Date(datePart).getTime())) { + due = new Date(datePart + 'T00:00:00Z').toISOString(); + } + } else if ($task_endTime && $task_endTime instanceof DateField && $task_endTime.date) { + due = $task_endTime.date.toISOString(); + } else if ($task_startTime && $task_startTime instanceof DateField && $task_startTime.date) { + due = $task_startTime.date.toISOString(); + } + + return { title, text, completed, due }; + }, + current => { + const hasChanged = current.title !== this.lastSyncedTask.title || current.text !== this.lastSyncedTask.text || current.due !== this.lastSyncedTask.due || current.completed !== this.lastSyncedTask.completed; + + this.setState({ needsSync: hasChanged }); + }, + { fireImmediately: true } + ); } componentWillUnmount() { @@ -350,6 +395,15 @@ export class TaskBox extends React.Component { if (result?.id) { alert('βœ… Task synced with Google Tasks!'); + if (result?.id) { + this.lastSyncedTask = { + title: taskTitle, + text: taskDesc, + due, + completed: isCompleted, + }; + this.setState({ needsSync: false }); + } } else { alert(`❌ Failed: ${result?.error?.message || 'Unknown error'}`); } @@ -402,6 +456,7 @@ export class TaskBox extends React.Component {