Skip to content

Commit

Permalink
feat: GitHub authenticaion log out. (#229)
Browse files Browse the repository at this point in the history
Ability to log out of GitHub authenticated project and make the login work across tabs using local storage.
  • Loading branch information
Zoramite committed Aug 12, 2021
1 parent 496525b commit d793eb1
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 36 deletions.
2 changes: 0 additions & 2 deletions src/sass/parts/_notifications.sass
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
.le__part__notifications
margin: 0 $le-space-medium

&--errors
color: var(--color-extreme)

Expand Down
9 changes: 3 additions & 6 deletions src/sass/parts/_overview.sass
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
align-items: center
background: var(--color-primary)
color: var(--color-on-primary)
column-gap: $le-space-medium
display: flex
flex-flow: row
justify-content: space-between
padding: 0 $le-space-medium

a
color: var(--color-on-primary)
Expand All @@ -14,16 +16,11 @@
font-weight: var(--font-weight-bold)
padding: $le-space-medium 0

.le__part__overview__menu
padding: 0 $le-space-medium

.le__part__overview__workspace
font-size: $le-font-size-small
margin: $le-space-medium 0 $le-space-medium $le-space-medium
margin: $le-space-medium 0

.le__part__overview__publish
margin-left: $le-space-medium

button
min-width: $le-space-xxlarge

Expand Down
10 changes: 9 additions & 1 deletion src/sass/parts/onboarding/_toolbar.sass
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
display: flex
flex-flow: row
justify-content: space-between
padding: 0 $le-space-medium

a
color: var(--color-on-primary)

.le__part__onboarding__toolbar__title
flex-grow: 1
font-weight: var(--font-weight-bold)
padding: $le-space-medium
padding: $le-space-medium $le-space-medium $le-space-medium 0

.le__part__onboarding__breadcrumb
align-items: center
Expand All @@ -38,3 +39,10 @@

.le__part__onboarding__breadcrumb__item
padding: $le-space-medium

.le__part__onboarding__actions
align-items: center
column-gap: $le-space-medium
display: flex
flex-flow: row
justify-content: space-between
24 changes: 23 additions & 1 deletion src/ts/editor/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface LiveEditorApiComponent {
/**
* Verify that the authentication for services that require auth.
*
* @returns True if the auth checks out.
* @returns True if the authentication is complete.
*/
checkAuth(): boolean;

Expand All @@ -35,6 +35,12 @@ export interface LiveEditorApiComponent {
*/
checkOnboarding(): Promise<OnboardingInfo>;

/**
* Clean any current authentication and log out the user of any
* accounts.
*/
clearAuth(): Promise<void>;

/**
* Copy a file.
*
Expand Down Expand Up @@ -68,6 +74,12 @@ export interface LiveEditorApiComponent {
*/
deleteFile(file: FileData): Promise<EmptyData>;

/**
* Retrieve information about the account requirements for the
* project.
*/
getAuthentication(): Promise<AuthenticationData>;

/**
* Retrieve the devices used for previews.
*/
Expand Down Expand Up @@ -174,6 +186,16 @@ export interface LiveEditorApiComponent {
uploadFile(file: File, options?: MediaOptions): Promise<MediaFileData>;
}

/**
* Information about the account requirements.
*/
export interface AuthenticationData {
/**
* Are there accounts in use?
*/
usesAccounts: boolean;
}

export interface ApiProjectTypes {
amagaki: AmagakiProjectTypeApi;
grow: GrowProjectTypeApi;
Expand Down
78 changes: 63 additions & 15 deletions src/ts/editor/state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ApiError,
ApiErrorCode,
AuthenticationData,
DeviceData,
EditorFileData,
EditorPreviewSettings,
Expand Down Expand Up @@ -55,6 +56,10 @@ export class EditorState extends ListenersMixin(Base) {
* API for retrieving data for the editor.
*/
api: LiveEditorApiComponent;
/**
* Information about the authentication for account management.
*/
authentication?: AuthenticationData | null;
/**
* Array of devices supported for previews.
*/
Expand Down Expand Up @@ -218,6 +223,21 @@ export class EditorState extends ListenersMixin(Base) {
});
}

/**
* Lazy load of authentication data.
*
* Understands the null state when there is an error requesting.
*/
authenticationOrGetAuthentication(): AuthenticationData | undefined | null {
if (
this.authentication === undefined &&
!this.inProgress(StatePromiseKeys.GetAuthentication)
) {
this.getAuthentication();
}
return this.authentication;
}

checkOnboarding(
callback?: (info: OnboardingInfo) => void,
callbackError?: (error: ApiError) => void
Expand Down Expand Up @@ -425,23 +445,30 @@ export class EditorState extends ListenersMixin(Base) {
}

/**
* When uploading a file the local field is allowed to override the default
* remote configuration. If the `remote` config is undefined no options are
* specified and can use the global configurations to determine which
* configuration should be used.
* Get the authentication information to know how to handle accounts.
*
* Used to understand when and how to show account information.
*/
getMediaOptions(useRemote?: boolean): MediaOptions | undefined {
if (useRemote === true) {
return this.project?.media?.remote;
} else if (useRemote === false) {
return this.project?.media?.options;
}

if (this.project?.media?.remote?.isDefault) {
return this.project?.media?.remote;
getAuthentication(
callback?: (devices: Array<DeviceData>) => void,
callbackError?: (error: ApiError) => void
): AuthenticationData | undefined | null {
const promiseKey = StatePromiseKeys.GetAuthentication;
this.delayCallbacks(promiseKey, callback, callbackError);
if (this.inProgress(promiseKey)) {
return this.authentication;
}

return this.project?.media?.options;
this.promises[promiseKey] = this.api
.getAuthentication()
.then(data => {
this.authentication = data;
this.handleDataAndCleanup(promiseKey, this.authentication);
})
.catch((error: ApiError) => {
this.authentication = null;
this.handleErrorAndCleanup(promiseKey, error);
});
return this.authentication;
}

getDevices(
Expand Down Expand Up @@ -594,6 +621,26 @@ export class EditorState extends ListenersMixin(Base) {
return this.files;
}

/**
* When uploading a file the local field is allowed to override the default
* remote configuration. If the `remote` config is undefined no options are
* specified and can use the global configurations to determine which
* configuration should be used.
*/
getMediaOptions(useRemote?: boolean): MediaOptions | undefined {
if (useRemote === true) {
return this.project?.media?.remote;
} else if (useRemote === false) {
return this.project?.media?.options;
}

if (this.project?.media?.remote?.isDefault) {
return this.project?.media?.remote;
}

return this.project?.media?.options;
}

getPreviewConfig(
callback?: (previewSettings: PreviewSettings | null) => void,
callbackError?: (error: ApiError) => void
Expand Down Expand Up @@ -1084,6 +1131,7 @@ export enum StatePromiseKeys {
CreateFile = 'CreateFile',
CreateWorkspace = 'CreateWorkspace',
DeleteFile = 'DeleteFile',
GetAuthentication = 'GetAuthentication',
GetDevices = 'GetDevices',
GetFile = 'GetFile',
GetFiles = 'GetFiles',
Expand Down
34 changes: 33 additions & 1 deletion src/ts/editor/ui/parts/onboarding/toolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,39 @@ export class ToolbarOnboardingPart extends BasePart implements UiPartComponent {
}
)}
</div>
${this.config.editor.ui.partNotifications.template()}
<div class="le__part__onboarding__actions">
${this.config.editor.ui.partNotifications.template()}
${this.templateAccount()}
</div>
</div>`;
}

templateAccount(): TemplateResult {
const authentication =
this.config.editor.state.authenticationOrGetAuthentication();

if (!authentication?.usesAccounts || !this.config.editor.api.checkAuth()) {
return html``;
}

const handleAccountClick = () => {
this.config.editor.api
.clearAuth()
.then(() => {
// Redirect to homepage after signing out.
window.location.href = '/';
})
.catch(err => {
console.error('Unable to sign out!', err);
});
};

return html`<div
class="le__part__onboarding__account le__clickable le__tooltip--bottom-left"
@click=${handleAccountClick}
data-tip="Sign out"
>
<span class="material-icons">logout</span>
</div>`;
}
}
30 changes: 30 additions & 0 deletions src/ts/editor/ui/parts/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,36 @@ export class OverviewPart extends BasePart implements UiPartComponent {
${this.templateMenu()} ${this.templateProject()} ${this.templateIcon()}
${this.templateWorkspace()} ${this.templatePublish()}
${this.config.editor.ui.partNotifications.template()}
${this.templateAccount()}
</div>`;
}

templateAccount(): TemplateResult {
const authentication =
this.config.state.authenticationOrGetAuthentication();

if (!authentication?.usesAccounts) {
return html``;
}

const handleAccountClick = () => {
this.config.editor.api
.clearAuth()
.then(() => {
// Redirect to homepage after signing out.
window.location.href = '/';
})
.catch(err => {
console.error('Unable to sign out!', err);
});
};

return html`<div
class="le__part__overview__account le__clickable le__tooltip--bottom-left"
@click=${handleAccountClick}
data-tip="Sign out"
>
<span class="material-icons">logout</span>
</div>`;
}

Expand Down
47 changes: 47 additions & 0 deletions src/ts/example/exampleApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ApiError,
ApiErrorCode,
ApiProjectTypes,
AuthenticationData,
DeviceData,
EditorFileData,
EditorPreviewSettings,
Expand Down Expand Up @@ -1423,6 +1424,29 @@ export class ExampleApi implements LiveEditorApiComponent {
});
}

async clearAuth(): Promise<void> {
return new Promise<void>((resolve, reject) => {
const methodName = 'clearAuth';
console.log(`API: ${methodName}`);

if (this.errorController.shouldError(methodName)) {
reject({
message: 'Failed to clear authentication.',
description: 'Api is set to always return an error.',
} as ApiError);
return;
}

console.info('Example authentication cleared!');

simulateNetwork(resolve, null, {
// Do not need to slow down the normal example loading.
// But may be useful later?
noNetworkSimulation: true,
});
});
}

async copyFile(originalPath: string, path: string): Promise<FileData> {
return new Promise<FileData>((resolve, reject) => {
const methodName = 'copyFile';
Expand Down Expand Up @@ -1523,6 +1547,29 @@ export class ExampleApi implements LiveEditorApiComponent {
});
}

async getAuthentication(): Promise<AuthenticationData> {
return new Promise<AuthenticationData>((resolve, reject) => {
const methodName = 'getAuthentication';
console.log(`API: ${methodName}`);

if (this.errorController.shouldError(methodName)) {
reject({
message: 'Failed to get the devices.',
description: 'Api is set to always return an error.',
} as ApiError);
return;
}

simulateNetwork(
resolve,
{
usesAccounts: true,
} as AuthenticationData,
this.options
);
});
}

async getDevices(): Promise<Array<DeviceData>> {
return new Promise<Array<DeviceData>>((resolve, reject) => {
const methodName = 'getDevices';
Expand Down

0 comments on commit d793eb1

Please sign in to comment.