Skip to content
This repository was archived by the owner on Dec 16, 2024. It is now read-only.

Commit eeb10b1

Browse files
committed
fix(login): improve login flow
Allows for login without 3rd party cookies
1 parent f042205 commit eeb10b1

File tree

10 files changed

+175
-137
lines changed

10 files changed

+175
-137
lines changed

src/ActorLogic.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import { GetEmoteSet, GetEmoteSetMin } from "@/apollo/query/emote-set.query";
66
import { useObjectSubscription } from "@/composables/useObjectSub";
77
import { GetCurrentUser } from "./apollo/query/user-self.query";
88
import { GetUser } from "./apollo/query/user.query";
9+
import { useStore } from "./store/main";
910
import { ObjectKind } from "./structures/Common";
1011
import { EmoteSet } from "./structures/EmoteSet";
1112
import { User } from "./structures/User";
1213

1314
export function setupActor(refreshAuth: Ref<boolean>) {
1415
const actor = useActor();
16+
const store = useStore();
1517
const { user } = storeToRefs(actor);
1618
const { watchObject } = useObjectSubscription();
1719

@@ -36,6 +38,7 @@ export function setupActor(refreshAuth: Ref<boolean>) {
3638
const usr = res.data.user;
3739
if (!usr) {
3840
actor.setUser(null);
41+
store.setAuthToken(null, false);
3942
return;
4043
}
4144

src/App.vue

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ import { useWorker } from "@/composables/useWorker";
7979
import Nav from "@/components/Nav.vue";
8080
import ModalViewport from "@/components/modal/ModalViewport.vue";
8181
import ContextMenu from "@/components/overlay/ContextMenu.vue";
82+
import { log } from "./Logger";
8283
import Icon from "./components/utility/Icon.vue";
84+
import { WindowSelfMessage } from "./views/window.messages";
8385
import { setupActor } from "@/ActorLogic";
8486
8587
const store = useStore();
@@ -100,13 +102,32 @@ const theme = computed(() => {
100102
const { createWorker } = useWorker();
101103
createWorker();
102104
105+
setupActor(refreshAuth);
106+
103107
// Set up client user
104108
provideApolloClient(apolloClient);
105109
106110
// Set up the actor user
107-
setupActor(refreshAuth);
111+
window.addEventListener("message", (e) => {
112+
if (e.origin === location.origin) {
113+
const data = e.data as WindowSelfMessage;
114+
115+
log.info("Received message from popup", `${JSON.stringify(data)}`);
116+
117+
if (data.event === "LOGIN_TOKEN") {
118+
store.setAuthToken(data.token);
119+
} else if (data.event === "LOGIN_FAILED") {
120+
store.setAuthToken(null);
121+
} else if (data.event === "LOGIN_LINKED") {
122+
store.refreshAuth = true;
123+
} else if (data.event === "LOGOUT_SUCCESS") {
124+
store.setAuthToken(null);
125+
}
126+
}
127+
});
108128
109129
const { onResult: onClientRequiredData } = useQuery<AppState>(GetAppState);
130+
110131
onClientRequiredData((res) => {
111132
if (!res.data) {
112133
return;

src/apollo/apollo.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,53 @@
1-
import { ApolloClient, ApolloLink, InMemoryCache, createHttpLink } from "@apollo/client/core";
1+
import { useStore } from "@/store/main";
2+
import { ApolloClient, ApolloLink, InMemoryCache } from "@apollo/client/core";
3+
import { BatchHttpLink } from "@apollo/client/link/batch-http";
24

35
// HTTP connection to the API
4-
const httpLink = createHttpLink({
6+
const httpLink = new BatchHttpLink({
57
// You should use an absolute URL here
68
uri: `${import.meta.env.VITE_APP_API_GQL}`,
79
credentials: "include",
10+
batchMax: 20,
11+
batchInterval: 20,
812
});
913

10-
const link = ApolloLink.from([httpLink]);
14+
let cycleDetected = false;
15+
16+
const authLink = new ApolloLink((operation, forward) => {
17+
const store = useStore();
18+
19+
// Use the setContext method to set the HTTP headers.
20+
operation.setContext({
21+
headers: store.authToken
22+
? {
23+
authorization: `Bearer ${store.authToken}`,
24+
// Allows to ignore auth failures and still get a response
25+
"x-ignore-auth-failure": "true",
26+
}
27+
: {},
28+
});
29+
30+
// Call the next link in the middleware chain.
31+
return forward(operation).map((response) => {
32+
const { response: resp } = operation.getContext();
33+
34+
if (resp?.headers) {
35+
const authFailure = resp.headers.get("x-auth-failure") === "true";
36+
if (authFailure) {
37+
if (!cycleDetected) {
38+
store.setAuthToken(null);
39+
cycleDetected = true;
40+
}
41+
} else {
42+
cycleDetected = false;
43+
}
44+
}
45+
46+
return response;
47+
});
48+
});
49+
50+
const link = ApolloLink.from([authLink, httpLink]);
1151

1252
// Cache implementation
1353
const cache = new InMemoryCache({

src/components/utility/LoginButton.vue

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,12 @@
2121
import { ref } from "vue";
2222
import { useI18n } from "vue-i18n";
2323
import { useActor } from "@/store/actor";
24-
import { useStore } from "@/store/main";
2524
import { User } from "@/structures/User";
2625
import { useAuth } from "@/composables/useAuth";
2726
import { useContextMenu } from "@/composables/useContextMenu";
2827
import Icon from "./Icon.vue";
2928
import LoginButtonPlaformVue from "./LoginButtonPlaform.vue";
3029
31-
const store = useStore();
3230
const actor = useActor();
3331
3432
const { t } = useI18n();
@@ -38,9 +36,7 @@ const platform = ref<User.UserConnectionPlatform>("TWITCH");
3836
3937
/** Request the user to authorize with a third party platform */
4038
const oauth2Authorize = () => {
41-
auth.prompt(platform.value).then(() => {
42-
store.refreshAuth = true;
43-
});
39+
auth.prompt(platform.value, false);
4440
};
4541
4642
const { open } = useContextMenu();

src/composables/useAuth.ts

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,51 @@
1+
import { useStore } from "@/store/main";
12
import { User } from "@/structures/User";
2-
import { log } from "@/Logger";
33

4-
export function useAuth() {
5-
function prompt(provider: User.Connection.Platform, token?: string | null): Promise<void> {
6-
const w = window.open(
7-
`${import.meta.env.VITE_APP_API_REST}/auth?platform=${provider.toLowerCase()}` +
8-
(token ? `&token=${token}` : ""),
9-
"seventv-oauth2",
10-
"_blank, width=850, height=650, menubar=no, location=no",
11-
);
4+
let authWindow = null as WindowProxy | null;
5+
6+
function popup(route: string): Promise<void> {
7+
if (authWindow && !authWindow.closed) {
8+
authWindow.close();
9+
}
1210

13-
return new Promise((resolve) => {
14-
const i = setInterval(async () => {
15-
if (!w?.closed) {
16-
return;
17-
}
11+
const popup = window.open(
12+
`${import.meta.env.VITE_APP_API_REST}/${route}`,
13+
"seventv-oauth2",
14+
"_blank, width=850, height=650, menubar=no, location=no",
15+
);
16+
17+
authWindow = popup;
18+
19+
if (!popup) {
20+
return Promise.reject("Failed to open window");
21+
}
1822

19-
clearInterval(i);
20-
resolve();
21-
}, 100);
22-
});
23+
return new Promise((resolve) => {
24+
const i = setInterval(async () => {
25+
if (!popup.closed) {
26+
return;
27+
}
28+
29+
clearInterval(i);
30+
resolve();
31+
}, 100);
32+
});
33+
}
34+
35+
export function useAuth() {
36+
function prompt(provider: User.Connection.Platform, isLink: boolean): Promise<void> {
37+
const store = useStore();
38+
39+
return popup(
40+
`auth?platform=${provider.toLowerCase()}` +
41+
(store.authToken ? `&token=${store.authToken}` : "") +
42+
(isLink ? "&link_connection=true" : ""),
43+
);
2344
}
2445

25-
function logout() {
26-
fetch(import.meta.env.VITE_APP_API_REST + "/auth/logout", {
27-
method: "POST",
28-
credentials: "include",
29-
})
30-
.then(() => {
31-
log.info("Signed out");
32-
})
33-
.catch((err) => log.error("failed to sign out", err.message));
46+
function logout(): Promise<void> {
47+
const store = useStore();
48+
return popup("auth/logout" + (store.authToken ? `?token=${store.authToken}` : ""));
3449
}
3550

3651
return {

src/store/lskeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export enum LocalStorageKeys {
55
LOCALE = "7tv-locale",
66
IDENTITY = "7tv-identity",
77
DEFAULT_SET = "7tv-default-set",
8+
AUTH_TOKEN = "7tv-auth-token",
89
}

src/store/main.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ import { correctLocale } from "@/i18n";
44
import { EmoteSet } from "@/structures/EmoteSet";
55
import { Role } from "@/structures/Role";
66

7-
export const SEASONAL_THEME_START = 1669852320734;
8-
97
export interface State {
108
refreshAuth: boolean;
9+
authToken: string | null;
1110
theme: Theme;
1211
seasonalTheme: boolean;
1312
themeTimestamp: number;
@@ -37,6 +36,7 @@ export const useStore = defineStore("main", {
3736
state: () =>
3837
({
3938
refreshAuth: true,
39+
authToken: window.localStorage.getItem(LocalStorageKeys.AUTH_TOKEN) || null,
4040
theme: (localStorage.getItem(LocalStorageKeys.THEME) || "dark") as Theme,
4141
seasonalTheme: false,
4242
themeTimestamp: parseInt(localStorage.getItem(LocalStorageKeys.THEME_TIMESTAMP ?? "0") as string),
@@ -60,6 +60,15 @@ export const useStore = defineStore("main", {
6060
roleList: (state): Role[] => Object.keys(state.roles).map((k) => state.roles[k]),
6161
},
6262
actions: {
63+
setAuthToken(token: string | null, refresh = true) {
64+
this.authToken = token;
65+
this.refreshAuth = refresh;
66+
if (token) {
67+
localStorage.setItem(LocalStorageKeys.AUTH_TOKEN, token);
68+
} else {
69+
localStorage.removeItem(LocalStorageKeys.AUTH_TOKEN);
70+
}
71+
},
6372
setTheme(newTheme: Theme) {
6473
const now = Date.now();
6574
this.noTransitions = true;

0 commit comments

Comments
 (0)