diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/apis/GoogleAuthenticationManager.tsx | 84 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 27 | ||||
-rw-r--r-- | src/client/util/SettingsManager.tsx | 2 | ||||
-rw-r--r-- | src/client/views/MainViewModal.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/TaskBox.scss | 66 | ||||
-rw-r--r-- | src/client/views/nodes/TaskBox.tsx | 578 | ||||
-rw-r--r-- | src/client/views/nodes/calendarBox/CalendarBox.scss | 9 | ||||
-rw-r--r-- | src/client/views/nodes/calendarBox/CalendarBox.tsx | 99 | ||||
-rw-r--r-- | src/server/ApiManagers/FireflyManager.ts | 11 | ||||
-rw-r--r-- | src/server/ApiManagers/GeneralGoogleManager.ts | 135 | ||||
-rw-r--r-- | src/server/RouteManager.ts | 8 | ||||
-rw-r--r-- | src/server/apis/google/GoogleApiServerUtils.ts | 11 | ||||
-rw-r--r-- | src/server/apis/google/google_project_credentials.json | 8 | ||||
-rw-r--r-- | src/server/authentication/DashUserModel.ts | 14 | ||||
-rw-r--r-- | src/server/authentication/Passport.ts | 6 |
15 files changed, 861 insertions, 199 deletions
diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 94ce42d8d..67a6e01e9 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, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Opt } from '../../fields/Doc'; @@ -6,12 +6,12 @@ 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<object> { +export class GoogleAuthenticationManager extends ObservableReactComponent<object> { // eslint-disable-next-line no-use-before-define public static Instance: GoogleAuthenticationManager; private authenticationLink: Opt<string> = undefined; @@ -23,6 +23,12 @@ export class GoogleAuthenticationManager extends React.Component<object> { @observable private credentials: { user_info: { name: string; picture: string }; access_token: string } | undefined = undefined; private disposer: Opt<IReactionDisposer>; + constructor(props: object) { + super(props); + makeObservable(this); + GoogleAuthenticationManager.Instance = this; + } + private set isOpen(value: boolean) { runInAction(() => (this.openState = value)); } @@ -35,44 +41,51 @@ export class GoogleAuthenticationManager extends React.Component<object> { this.openState && this.resetState(0, 0); } - public fetchOrGenerateAccessToken = async (displayIfFound = false) => { + public fetchOrGenerateAccessToken = async (): Promise<string | undefined> => { 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; - return new Promise<string>(resolve => { - this.disposer?.(); - this.disposer = reaction( - () => this.authenticationCode, - async authenticationCode => { - if (authenticationCode && /\d{1}\/[\w-]{55}/.test(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 }; - }); - this.resetState(); - resolve((response2 as { access_token: string }).access_token); - } - } - ); + + // 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 { + 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')) { + if (window.confirm('Authorize Dash to access your Google account?')) { + window.open(response)?.focus(); + return undefined; + } + } + + throw new Error('Unable to fetch Google access token.'); } + }; - // 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) { + public fetchAccessTokenSilently = async (): Promise<string | undefined> => { + const response = await Networking.FetchFromServer('/readGoogleAccessToken'); + + 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 { + // Do nothing — just return undefined silently + return undefined; } - return (response2 as { access_token: string }).access_token; }; + resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => { if (!visibleForMS && !fadesOutInMS) { @@ -107,11 +120,6 @@ export class GoogleAuthenticationManager extends React.Component<object> { } }); - constructor(props: object) { - super(props); - GoogleAuthenticationManager.Instance = this; - } - private get renderPrompt() { return ( <div className={'authorize-container'}> @@ -127,7 +135,7 @@ export class GoogleAuthenticationManager extends React.Component<object> { </button> ) : null} {this.showPasteTargetState ? <input className={'paste-target'} onChange={action(e => (this.authenticationCode = e.currentTarget.value))} placeholder={prompt} /> : null} - {this.credentials ? ( + {this.credentials?.user_info?.picture ? ( <> <img className={'avatar'} src={this.credentials.user_info.picture} /> <span className={'welcome'}>Welcome to Dash, {this.credentials.user_info.name}</span> diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index aea636040..2289224cc 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -531,6 +531,9 @@ 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'); + $task_lastSyncedAt?: STRt = new StrInfo('last update time for task node'); + $task_deleted?: BoolInfo | boolean = new BoolInfo('whether task is deleted or not', /*filterable*/ false); _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); @@ -593,30 +596,6 @@ export namespace Docs { options: { acl: '' }, }, ], - - // AARAV ADD // - [ - DocumentType.JOURNAL, - { - layout: { view: EmptyBox, dataField: 'text' }, - options: { - title: 'Daily Journal', - acl_Guest: SharingPermissions.View, - }, - }, - ], - - [ - DocumentType.TASK, - { - layout: { view: EmptyBox, dataField: 'text' }, - options: { - title: 'Task', - acl_Guest: SharingPermissions.View, - }, - }, - ], - // AARAV ADD // ]); const suffix = 'Proto'; 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<object> { @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/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<MainViewOverlayProps> { 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..beee58697 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; @@ -31,7 +32,7 @@ resize: none; line-height: 1.4; resize: none; - flex-grow: 1 + flex-grow: 1; } .task-manager-checkboxes { @@ -40,7 +41,8 @@ gap: 16px; } -.task-manager-allday, .task-manager-complete { +.task-manager-allday, +.task-manager-complete { display: flex; align-items: center; gap: 6px; @@ -62,7 +64,7 @@ gap: 4px; } -input[type="datetime-local"] { +input[type='datetime-local'] { width: 100%; font-size: 0.9rem; padding: 6px 8px; @@ -70,3 +72,61 @@ input[type="datetime-local"] { border-radius: 6px; box-sizing: border-box; } + +.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 { + align-self: flex-start; + width: auto; + font-size: 0.85rem; + padding: 6px 12px; + border-radius: 6px; + 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 + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; + } +} + +.task-box-blur-wrapper { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + min-width: 0; +} + +.task-manager-button-row { + display: flex; + flex-direction: row; + gap: 8px; +} + +.task-manager-delete { + @extend .task-manager-google; + background-color: #182430; + + &:hover { + background-color: #000000; + } +} diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx index 9d59746f8..ed5982c55 100644 --- a/src/client/views/nodes/TaskBox.tsx +++ b/src/client/views/nodes/TaskBox.tsx @@ -1,30 +1,64 @@ -import { action, makeObservable, IReactionDisposer, reaction } from 'mobx'; +import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { DateField } from '../../../fields/DateField'; +import { BoolCast, DateCast, DocCast, NumCast, StrCast } from '../../../fields/Types'; +import { GoogleAuthenticationManager } from '../../apis/GoogleAuthenticationManager'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; -import { FieldView } from './FieldView'; -import { DateField } from '../../../fields/DateField'; -import { Doc } from '../../../fields/Doc'; - +import { ViewBoxBaseComponent } from '../DocComponent'; +import { FieldView, FieldViewProps } from './FieldView'; import './TaskBox.scss'; - -/** - * Props (reference to document) for Task Box - */ - -interface TaskBoxProps { - Document: Doc; -} +import { DocumentDecorations } from '../DocumentDecorations'; +import { Doc } from '../../../fields/Doc'; +import { DocumentView } from './DocumentView'; /** * TaskBox class for adding task information + completing tasks */ @observer -export class TaskBox extends React.Component<TaskBoxProps> { +export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() { + _googleTaskCreateDisposer?: IReactionDisposer; + _heightDisposer?: IReactionDisposer; + _widthDisposer?: IReactionDisposer; + @observable _needsSync = false; // Whether the task needs to be synced with Google Tasks + @observable _syncing = false; // Whether the task is currently syncing with Google Tasks + private _isFocused = false; // Whether the task box is currently focused + + // contains the last synced task information + private _lastSyncedTask: { + title: string; + text: string; + due?: string; + completed: boolean; + deleted?: boolean; + } = { + title: '', + text: '', + due: '', + completed: false, + deleted: false, + }; + + /** + * Getter for needsSync + */ + get needsSync() { + return this._needsSync; + } + + /** + * Constructor for the task box + * @param props - props containing the document reference + */ + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + /** - * Method to reuturn the - * @param fieldStr + * Return the JSX string that will create this component + * @param fieldStr the Doc field that contains the primary data for this component * @returns */ public static LayoutString(fieldStr: string) { @@ -35,10 +69,9 @@ export class TaskBox extends React.Component<TaskBoxProps> { * Method to update the task description * @param e - event of changing the description box input */ - @action updateText = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { - this.props.Document.text = e.target.value; + this.Document[this.fieldKey] = e.target.value; }; /** @@ -47,7 +80,7 @@ export class TaskBox extends React.Component<TaskBoxProps> { */ @action updateTitle = (e: React.ChangeEvent<HTMLInputElement>) => { - this.props.Document.title = e.target.value; + this.Document.title = e.target.value; }; /** @@ -56,11 +89,11 @@ export class TaskBox extends React.Component<TaskBoxProps> { */ @action updateAllDay = (e: React.ChangeEvent<HTMLInputElement>) => { - this.props.Document.$task_allDay = e.target.checked; + this.Document.$task_allDay = e.target.checked; if (e.target.checked) { - delete this.props.Document.$task_startTime; - delete this.props.Document.$task_endTime; + delete this.Document.$task_startTime; + delete this.Document.$task_endTime; } this.setTaskDateRange(); @@ -74,16 +107,16 @@ export class TaskBox extends React.Component<TaskBoxProps> { updateStart = (e: React.ChangeEvent<HTMLInputElement>) => { const newStart = new Date(e.target.value); - this.props.Document.$task_startTime = new DateField(newStart); + this.Document.$task_startTime = new DateField(newStart); - const endDate = this.props.Document.$task_endTime instanceof DateField ? this.props.Document.$task_endTime.date : undefined; + const endDate = this.Document.$task_endTime instanceof DateField ? this.Document.$task_endTime.date : undefined; if (endDate && newStart > endDate) { // Alert user alert('Start time cannot be after end time. End time has been adjusted.'); // Fix end time const adjustedEnd = new Date(newStart.getTime() + 60 * 60 * 1000); - this.props.Document.$task_endTime = new DateField(adjustedEnd); + this.Document.$task_endTime = new DateField(adjustedEnd); } this.setTaskDateRange(); @@ -97,16 +130,16 @@ export class TaskBox extends React.Component<TaskBoxProps> { updateEnd = (e: React.ChangeEvent<HTMLInputElement>) => { const newEnd = new Date(e.target.value); - this.props.Document.$task_endTime = new DateField(newEnd); + this.Document.$task_endTime = new DateField(newEnd); - const startDate = this.props.Document.$task_startTime instanceof DateField ? this.props.Document.$task_startTime.date : undefined; + const startDate = this.Document.$task_startTime instanceof DateField ? this.Document.$task_startTime.date : undefined; if (startDate && newEnd < startDate) { // Alert user alert('End time cannot be before start time. Start time has been adjusted.'); // Fix start time const adjustedStart = new Date(newEnd.getTime() - 60 * 60 * 1000); - this.props.Document.$task_startTime = new DateField(adjustedStart); + this.Document.$task_startTime = new DateField(adjustedStart); } this.setTaskDateRange(); @@ -117,10 +150,10 @@ export class TaskBox extends React.Component<TaskBoxProps> { */ @action setTaskDateRange() { - const doc = this.props.Document; + const doc = this.Document; if (doc.$task_allDay) { - const range = typeof doc.$task_dateRange === 'string' ? doc.$task_dateRange.split('|') : []; + const range = StrCast(doc.$task_dateRange).split('|'); const dateStr = range[0] ?? new Date().toISOString().split('T')[0]; // default to today doc.$task_dateRange = `${dateStr}|${dateStr}`; @@ -145,53 +178,365 @@ export class TaskBox extends React.Component<TaskBoxProps> { @action toggleComplete = (e: React.ChangeEvent<HTMLInputElement>) => { - this.props.Document.$task_completed = e.target.checked; + this.Document.$task_completed = e.target.checked; }; /** - * Constructor for the task box - * @param props - props containing the document reference + * Computes due date for the task (for Google Tasks API) + * @returns - a string representing the due date in ISO format, or undefined if no valid date is found */ + private computeDueDate(): string | undefined { + const doc = this.Document; + let due: string | undefined; + const allDay = !!doc.$task_allDay; - constructor(props: TaskBoxProps) { - super(props); - makeObservable(this); + if (allDay) { + const rawRange = StrCast(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; + } + + return due; } - _heightDisposer?: IReactionDisposer; - _widthDisposer?: IReactionDisposer; + /** + * Builds the body for the Google Tasks API request + * @returns - an object containing the task details + */ + + private buildGoogleTaskBody(): Record<string, string | boolean | undefined> { + const doc = this.Document; + const title = StrCast(doc.title, 'Untitled Task'); + const notes = StrCast(doc[this.fieldKey]); + const due = this.computeDueDate(); + const completed = !!doc.$task_completed; + + const body: Record<string, string | boolean | undefined> = { + title, + notes, + due, + status: completed ? 'completed' : 'needsAction', + completed: completed ? new Date().toISOString() : undefined, + }; + + if (doc.$dashDeleted === true) { + body.deleted = true; + } else if (doc.$dashDeleted === false) { + body.deleted = false; + } + + return body; + } + + /** + * Handles the focus event for the task box (for auto-syncing) + */ + handleFocus = () => { + if (!this._isFocused) { + this._isFocused = true; + this.syncWithGoogleTaskBidirectional(true); // silent sync + } + }; + + /** + * Handles the blur event for the task box (for auto-syncing) + * @param e - the focus event + */ + handleBlur = (e: React.FocusEvent<HTMLDivElement>) => { + // Check if focus is moving outside this component + if (!e.currentTarget.contains(e.relatedTarget)) { + this._isFocused = false; + this.syncWithGoogleTaskBidirectional(true); + } + }; + + /** + * Method to sync the task with Google Tasks bidirectionally + * (update Dash from Google and vice versa, based on which is newer) + * @param silent - whether to suppress UI prompts to connect to Google (default: false) + * @returns - a promise that resolves to true if sync was successful, false otherwise + */ + + syncWithGoogleTaskBidirectional = async (silent = false): Promise<boolean> => { + const doc = this.Document; + let token: string | undefined; + try { + token = silent ? await GoogleAuthenticationManager.Instance.fetchAccessTokenSilently() : await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); + } catch (err) { + console.warn('Google auth failed:', err); + return false; + } + + if (!token) { + if (!silent) { + const listener = () => { + window.removeEventListener('focusin', listener); + if (confirm('✅ Authorization complete. Try syncing the task again?')) { + // try syncing again + this.syncWithGoogleTaskBidirectional(); + } + window.removeEventListener('focusin', listener); + }; + setTimeout(() => window.addEventListener('focusin', listener), 100); + } + return false; + } + + if (!doc.$googleTaskId) return false; + + runInAction(() => { + this._syncing = true; + }); + + try { + // Fetch current version of Google Task + const response = await fetch(`/googleTasks/${doc.$googleTaskId}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + credentials: 'include', + }); + + const googleTask = await response.json(); + const googleUpdated = new Date(googleTask.updated); + const dashUpdated = new Date(StrCast(doc.$task_lastSyncedAt)); + + const dashChanged = + StrCast(doc.title) !== this._lastSyncedTask.title || + StrCast(doc[this.fieldKey]) !== this._lastSyncedTask.text || + this.computeDueDate() !== this._lastSyncedTask.due || + !!doc.$task_completed !== this._lastSyncedTask.completed || + !!doc.$dashDeleted !== this._lastSyncedTask.deleted; + + if (googleUpdated > dashUpdated && !dashChanged) { + // Google version is newer — update Dash + runInAction(() => { + doc.title = googleTask.title ?? doc.title; + doc[this.fieldKey] = googleTask.notes ?? doc[this.fieldKey]; + doc.$task_completed = googleTask.status === 'completed'; + + if (googleTask.due && googleTask.due.split('T')[0] !== this.computeDueDate()?.split('T')[0]) { + const dueDate = new Date(googleTask.due); + doc.$task_allDay = true; + doc.$task_dateRange = `${dueDate.toISOString().split('T')[0]}|${dueDate.toISOString().split('T')[0]}`; + } + + doc.$task_lastSyncedAt = googleTask.updated; + this._lastSyncedTask = { + title: StrCast(doc.title), + text: StrCast(doc[this.fieldKey]), + due: this.computeDueDate(), + completed: !!doc.$task_completed, + deleted: !!doc.$dashDeleted, + }; + this._needsSync = false; + }); + + console.log('Pulled newer version from Google'); + return true; + } else if (googleUpdated <= dashUpdated && !dashChanged) { + console.log('No changes to sync'); + return true; + } else { + // Dash version is newer — push update to Google + const body = this.buildGoogleTaskBody(); + const res = await fetch(`/googleTasks/${doc.$googleTaskId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + credentials: 'include', + body: JSON.stringify(body), + }); + + const result = await res.json(); + if (result?.id) { + doc.$task_lastSyncedAt = new Date().toISOString(); + this._lastSyncedTask = { + title: StrCast(doc.title), + text: StrCast(doc[this.fieldKey]), + due: this.computeDueDate(), + completed: !!doc.$task_completed, + deleted: !!doc.$dashDeleted, + }; + this._needsSync = false; + console.log('Pushed newer version to Google'); + return true; + } else { + console.warn('❌ Push failed:', result); + return false; + } + } + } catch (err) { + console.error('❌ Sync error:', err); + return false; + } finally { + runInAction(() => { + this._syncing = false; + }); + } + }; + + /** + * Method to set up the task box on mount + */ componentDidMount() { this.setTaskDateRange(); + const doc = this.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 = this.buildGoogleTaskBody(); + + 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.warn('❌ Error creating Google Task:', err); + } + } else if (doc.$googleTaskId) { + await this.syncWithGoogleTaskBidirectional(); + } + })(); - const doc = this.props.Document; this._heightDisposer = reaction( - () => Number(doc._height), + () => NumCast(doc._height), height => { - const minHeight = Number(doc.height_min ?? 0); - if (!isNaN(height) && height < minHeight) { + const minHeight = NumCast(doc.height_min); + if (height < minHeight) { doc._height = minHeight; } } ); this._widthDisposer = reaction( - () => Number(doc._width), + () => NumCast(doc._width), width => { - const minWidth = Number(doc.width_min ?? 0); - if (!isNaN(width) && width < minWidth) { + const minWidth = NumCast(doc.width_min); + if (width < minWidth) { doc._width = minWidth; } } ); + + runInAction(() => { + const completed = BoolCast(doc.$task_completed); + const due = this.computeDueDate(); + + this._lastSyncedTask = { + title: StrCast(doc.title), + text: StrCast(doc[this.fieldKey]), + due, + completed, + deleted: !!doc.$dashDeleted, + }; + this._needsSync = false; + }); + + if (this.Document.$dashDeleted) { + runInAction(() => { + this.Document.$dashDeleted = false; + }); + } + + this._googleTaskCreateDisposer = reaction( + () => { + const completed = BoolCast(doc.$task_completed); + const due = this.computeDueDate(); + const dashDeleted = !!doc.$dashDeleted; + + return { title: StrCast(doc.title), text: StrCast(doc[this.fieldKey]), completed, due, dashDeleted }; + }, + ({ title, text, completed, due, dashDeleted }) => { + this._needsSync = title !== this._lastSyncedTask.title || text !== this._lastSyncedTask.text || due !== this._lastSyncedTask.due || completed !== this._lastSyncedTask.completed || dashDeleted !== this._lastSyncedTask.deleted; + }, + { fireImmediately: true } + ); } + /** + * Method to clean up the task box on unmount + */ componentWillUnmount() { + const doc = this.Document; + this._googleTaskCreateDisposer?.(); this._heightDisposer?.(); this._widthDisposer?.(); } /** + * Method to handle task deletion + * @returns - a promise that resolves when the task is deleted + */ + handleDeleteTask = async () => { + const doc = this.Document; + if (!doc.$googleTaskId) return; + if (!window.confirm('Are you sure you want to delete this task?')) return; + + doc.$dashDeleted = true; + this._needsSync = true; + + try { + const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); + if (!token) return; + + await fetch(`/googleTasks/${doc.$googleTaskId}`, { + method: 'DELETE', + credentials: 'include', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + const view = DocumentView.getDocumentView(this.Document); + if (view) { + DocumentView.SelectView(view, false); // select document + DocumentDecorations.Instance?.onCloseClick?.(true); // simulate clicking the close button + } + + // Remove the task from the recently closed list + Doc.MyRecentlyClosed && Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, this.Document); + console.log(`✅ Deleted Google Task ${doc.$googleTaskId}`); + } catch (err) { + console.warn('❌ Failed to delete Google Task:', err); + } + }; + + /** * Method to render the task box * @returns - HTML with taskbox components */ @@ -202,71 +547,104 @@ export class TaskBox extends React.Component<TaskBoxProps> { return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes()); } - const doc = this.props.Document; + const doc = this.Document; - const taskDesc = typeof doc.text === 'string' ? doc.text : ''; - const taskTitle = typeof doc.title === 'string' ? doc.title : ''; + const taskDesc = StrCast(doc[this.fieldKey]); + const taskTitle = StrCast(doc.title); const allDay = !!doc.$task_allDay; - const isCompleted = !!this.props.Document.$task_completed; + const due = this.computeDueDate(); + const isCompleted = !!this.Document.$task_completed; + + const startTime = DateCast(doc.$task_startTime) ? toLocalDateTimeString(DateCast(doc.$task_startTime)!.date) : ''; + const endTime = DateCast(doc.$task_endTime) ? toLocalDateTimeString(DateCast(doc.$task_endTime)!.date) : ''; - const startTime = doc.$task_startTime instanceof DateField && doc.$task_startTime.date instanceof Date ? toLocalDateTimeString(doc.$task_startTime.date) : ''; + const handleGoogleTaskSync = async () => { + const success = await this.syncWithGoogleTaskBidirectional(); - const endTime = doc.$task_endTime instanceof DateField && doc.$task_endTime.date instanceof Date ? toLocalDateTimeString(doc.$task_endTime.date) : ''; + if (success) { + alert('✅ Task successfully synced!'); + } else { + alert('❌ Task sync failed. Try reloading.'); + } + }; return ( - <div className="task-manager-container"> - <input className="task-manager-title" type="text" placeholder="Task Title" value={taskTitle} onChange={this.updateTitle} disabled={isCompleted} style={{ opacity: isCompleted ? 0.7 : 1 }} /> - - <textarea className="task-manager-description" placeholder="What’s your task?" value={taskDesc} onChange={this.updateText} disabled={isCompleted} style={{ opacity: isCompleted ? 0.7 : 1 }} /> - - <div className="task-manager-checkboxes"> - <label className="task-manager-allday" style={{ opacity: isCompleted ? 0.7 : 1 }}> - <input type="checkbox" checked={allDay} onChange={this.updateAllDay} disabled={isCompleted} /> - All day - {allDay && ( - <input - type="date" - value={(() => { - const rawRange = doc.$task_dateRange; - if (typeof rawRange !== 'string') return ''; - const datePart = rawRange.split('|')[0]; - if (!datePart) return ''; - const d = new Date(datePart); - return !isNaN(d.getTime()) ? d.toISOString().split('T')[0] : ''; - })()} - onChange={e => { - const newDate = new Date(e.target.value); - if (!isNaN(newDate.getTime())) { - const dateStr = e.target.value; - if (dateStr) { - doc.$task_dateRange = `${dateStr}T00:00:00|${dateStr}T00:00:00`; + <div className="task-box-blur-wrapper" tabIndex={0} onBlur={this.handleBlur} onFocus={this.handleFocus}> + <div className="task-manager-container"> + <input className="task-manager-title" type="text" placeholder="Task Title" value={taskTitle} onChange={this.updateTitle} disabled={isCompleted} style={{ opacity: isCompleted ? 0.7 : 1 }} /> + + <textarea className="task-manager-description" placeholder="What’s your task?" value={taskDesc} onChange={this.updateText} disabled={isCompleted} style={{ opacity: isCompleted ? 0.7 : 1 }} /> + + <div className="task-manager-checkboxes"> + <label className="task-manager-allday" style={{ opacity: isCompleted ? 0.7 : 1 }}> + <input type="checkbox" checked={allDay} onChange={this.updateAllDay} disabled={isCompleted} /> + All day + {allDay && ( + <input + type="date" + value={(() => { + const datePart = StrCast(doc.$task_dateRange).split('|')[0]; + if (!datePart) return ''; + const d = new Date(datePart); + return !isNaN(d.getTime()) ? d.toISOString().split('T')[0] : ''; + })()} + onChange={e => { + const newDate = new Date(e.target.value); + if (!isNaN(newDate.getTime())) { + const dateStr = e.target.value; + if (dateStr) { + doc.$task_dateRange = `${dateStr}T00:00:00|${dateStr}T00:00:00`; + } } - } - }} - disabled={isCompleted} - style={{ marginLeft: '8px' }} - /> - )} - </label> - - <label className="task-manager-complete"> - <input type="checkbox" checked={isCompleted} onChange={this.toggleComplete} /> - Complete - </label> - </div> - - {!allDay && ( - <div className="task-manager-times" style={{ opacity: isCompleted ? 0.7 : 1 }}> - <label> - Start: - <input type="datetime-local" value={startTime} onChange={this.updateStart} disabled={isCompleted} /> + }} + disabled={isCompleted} + style={{ marginLeft: '8px' }} + /> + )} </label> - <label> - End: - <input type="datetime-local" value={endTime} onChange={this.updateEnd} disabled={isCompleted} /> + + <label className="task-manager-complete"> + <input type="checkbox" checked={isCompleted} onChange={this.toggleComplete} /> + Complete </label> </div> - )} + + <div className="task-manager-button-row"> + <button + className="task-manager-google" + onClick={event => { + event.preventDefault(); + handleGoogleTaskSync(); + }}> + {this._syncing ? 'Syncing...' : this.needsSync ? 'Push Updates' : 'Sync Task'} + </button> + + <button + className="task-manager-delete" + onClick={event => { + event.preventDefault(); + this.handleDeleteTask(); + }}> + <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="12" height="12" viewBox="0 0 24 24" style={{ fill: 'white', marginRight: '6px', verticalAlign: 'middle', marginTop: '-2px' }}> + <path d="M 10 2 L 9 3 L 5 3 C 4.4 3 4 3.4 4 4 C 4 4.6 4.4 5 5 5 L 7 5 L 17 5 L 19 5 C 19.6 5 20 4.6 20 4 C 20 3.4 19.6 3 19 3 L 15 3 L 14 2 L 10 2 z M 5 7 L 5 20 C 5 21.1 5.9 22 7 22 L 17 22 C 18.1 22 19 21.1 19 20 L 19 7 L 5 7 z M 9 9 C 9.6 9 10 9.4 10 10 L 10 19 C 10 19.6 9.6 20 9 20 C 8.4 20 8 19.6 8 19 L 8 10 C 8 9.4 8.4 9 9 9 z M 15 9 C 15.6 9 16 9.4 16 10 L 16 19 C 16 19.6 15.6 20 15 20 C 14.4 20 14 19.6 14 19 L 14 10 C 14 9.4 14.4 9 15 9 z"></path> + </svg> + Delete + </button> + </div> + + {!allDay && ( + <div className="task-manager-times" style={{ opacity: isCompleted ? 0.7 : 1 }}> + <label> + Start: + <input type="datetime-local" value={startTime} onChange={this.updateStart} disabled={isCompleted} /> + </label> + <label> + End: + <input type="datetime-local" value={endTime} onChange={this.updateEnd} disabled={isCompleted} /> + </label> + </div> + )} + </div> </div> ); } 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 26aed72c3..a4183a11a 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) => { @@ -284,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 ( <div key={this.calendarViewType} className={`calendarBox${this._props.isContentActive() ? '-interactive' : ''}`} + style={{ + width: scaledWidth, + height: scaledHeight, + overflow: 'hidden', + position: 'relative', + }} + ref={r => { + this.createDashEventsTarget(r); + this.fixWheelEvents(r, this._props.isContentActive); + }} onPointerDown={e => { setTimeout( action(() => { @@ -303,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); }}> + <div + style={{ + transform: `scale(${scale})`, + transformOrigin: 'top left', + width: scaledWidth / scale, + height: scaledHeight / scale, + }} + > {this.renderCalendar} + </div> </div> ); } 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 12913b1ef..110701418 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -3,6 +3,7 @@ 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<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([ ['create', (api, params) => api.create(params)], @@ -61,5 +62,139 @@ export default class GeneralGoogleManager extends ApiManager { res.send(undefined); }, }); + + // Task Creation + register({ + method: Method.POST, + subscription: new RouteSubscriber('googleTasks').add('create'), + 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 { title, notes, due, status, completed, deleted } = req.body; + const result = await tasks.tasks.insert({ + tasklist: '@default', + requestBody: { title, notes, due, status, completed, deleted }, + }); + + res.status(200).send(result.data); + } catch (err) { + console.error('Google Tasks error:', err); + res.status(500).send('Failed to create task.'); + } + }, + }); + + // 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, deleted } = req.body; + + const result = await tasks.tasks.patch({ + tasklist: '@default', + task: taskId, + requestBody: { title, notes, due, status, completed, deleted}, + }); + + 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.'); + } + }, + }); + + // Google Account Linking + + register({ + method: Method.GET, + subscription: '/refreshGoogle', + secureHandler: async ({ user, req, res }) => + new Promise<void>(resolve => + GoogleApiServerUtils.processNewUser(user.id, req.query.code as string) + .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'); + }) + .finally(resolve) + ), + }); + + // Task Retrieval + register({ + method: Method.GET, + 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; + + const result = await tasks.tasks.get({ + tasklist: '@default', + task: taskId, + }); + + res.status(200).send(result.data); + } catch (err) { + console.error('Google Tasks retrieval error:', err); + res.status(500).send('Failed to retrieve task.'); + } + }, + }); } + } 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: } } diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 7373df473..ad0f0e580 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -13,7 +13,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}` ); @@ -181,7 +181,7 @@ export namespace GoogleApiServerUtils { * @returns the newly generated url to the authentication landing page */ export function generateAuthenticationUrl(): string { - return worker.generateAuthUrl({ scope, access_type: 'offline' }); + return worker.generateAuthUrl({ scope, access_type: 'offline', prompt: 'consent' }); } /** @@ -203,12 +203,12 @@ export namespace GoogleApiServerUtils { */ export async function processNewUser(userId: string, authenticationCode: string): Promise<EnrichedCredentials> { const credentials = await new Promise<Credentials>((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); @@ -304,6 +304,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(userId, access_token, expiry_date); diff --git a/src/server/apis/google/google_project_credentials.json b/src/server/apis/google/google_project_credentials.json index 955c5a3c1..438e5157e 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": "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": "w8KIFSc0MQpmUYHed4qEzn8b", - "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"] + "client_secret": "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", + "redirect_uris": ["http://localhost:1050/refreshGoogle"] } }
\ No newline at end of file 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<any, any>((req, user, done) => { - done(undefined, (user as any)?.id); + done(undefined, (user as DashUserModel)?.id); }); passport.deserializeUser<any, any>((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)); }) ); |