diff --git a/src/App.vue b/src/App.vue index 6ec3916..8a37dfb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -7,62 +7,70 @@ diff --git a/src/components/navigation/LoginCard.vue b/src/components/navigation/LoginCard.vue index 3715fa7..2d2d874 100644 --- a/src/components/navigation/LoginCard.vue +++ b/src/components/navigation/LoginCard.vue @@ -1,52 +1,50 @@ diff --git a/src/components/navigation/TheUserMenu.vue b/src/components/navigation/TheUserMenu.vue index 67458e9..e79edf7 100644 --- a/src/components/navigation/TheUserMenu.vue +++ b/src/components/navigation/TheUserMenu.vue @@ -17,17 +17,17 @@ fa-user-circle - Sign in / Sign out + {{ $t('user.profile') }} - - Logout + + {{ $t('navigation.login.logout') }} - - Login - - + + {{ $t('navigation.login.title') }} + + - User Profile - - Logout + {{ $t('user.profile') }} + + {{ $t('navigation.login.logout') }} - - Login - - + + {{ $t('navigation.login.title') }} + + void; - @authNamespace.Action(AuthenticationActions.LOGOUT) logout!: () => void + @authNamespace.Action(AuthenticationActions.LOGOUT) onClickLogout!: () => void - dialogOpen = false - doOpen () { - setTimeout(() => { this.dialogOpen = true }) - } - userMenuItems: UserMenuItem[] = [] + @Prop() private mobile!: boolean - // eslint-disable-next-line - userMenuItemsClickHandler (item: UserMenuItem) { - // do stuff - } + isDialogOpen = false + + openLoginDialog () { + setTimeout(() => { + this.isDialogOpen = true + }) + } + + onClickCloseLoginDialog () { + this.isDialogOpen = false + } + + userMenuItems: UserMenuItem[] = [] + + // eslint-disable-next-line + userMenuItemsClickHandler(item: UserMenuItem) { + // do stuff + } } diff --git a/src/components/navigation/login/ProviderBlock.vue b/src/components/navigation/login/ProviderBlock.vue new file mode 100644 index 0000000..71badd0 --- /dev/null +++ b/src/components/navigation/login/ProviderBlock.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/plugins/vuetify.ts b/src/plugins/vuetify.ts index 6ea799d..0cc46fc 100644 --- a/src/plugins/vuetify.ts +++ b/src/plugins/vuetify.ts @@ -2,58 +2,59 @@ import Vue from 'vue' import '@mdi/font/css/materialdesignicons.css' import '@fortawesome/fontawesome-free/css/all.css' import colours from '@/assets/scss/_variables.scss' -import { DashedLine, NormalLine, TBarHead, DashedLineActive, NormalLineActive, TBarHeadActive } from '@/assets/tool-icons' +import { DashedLine, DashedLineActive, NormalLine, NormalLineActive, TBarHead, TBarHeadActive } from '@/assets/tool-icons' import Vuetify, { VApp, - VAvatar, - VRadio, - VRadioGroup, VAutocomplete, + VAvatar, VBadge, - VRow, - VImg, - VSwitch, - VDivider, - VToolbar, - VItemGroup, - VContainer, - VSpacer, VBtn, - VIcon, + VBtnToggle, + VCard, + VCardActions, VCardSubtitle, - VTooltip, + VCardText, + VCardTitle, + VChip, + VChipGroup, VCol, - VBtnToggle, - VMenu, + VColorPicker, + VContainer, + VContent, + VDialog, + VDivider, + VIcon, + VImg, + VInput, + VItemGroup, VList, VListItem, VListItemAction, VListItemAvatar, VListItemContent, - VListItemTitle, VListItemIcon, - VSubheader, - VContent, + VListItemTitle, + VMenu, + VNavigationDrawer, VOverlay, - VCard, - VCardActions, - VSheet, - VColorPicker, - VSlider, - VDialog, + VRadio, + VRadioGroup, + VRow, VSelect, - VNavigationDrawer, - VTextField, - VChipGroup, - VChip, + VSheet, VSkeletonLoader, - VInput, - VCardTitle, - VTreeview, + VSlider, + VSpacer, + VSubheader, + VSwitch, VTab, + VTabs, VTabsSlider, - VTabs + VTextField, + VToolbar, + VTooltip, + VTreeview } from 'vuetify/lib' Vue.use(Vuetify, { @@ -91,6 +92,7 @@ Vue.use(Vuetify, { VContent, VSubheader, VCard, + VCardText, VCardActions, VSheet, VColorPicker, @@ -108,7 +110,7 @@ Vue.use(Vuetify, { VTabsSlider, VTabs }, - directives: { } + directives: {} }) // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/store/modules/authentication.ts b/src/store/modules/authentication.ts index 530d64b..b07b9f5 100644 --- a/src/store/modules/authentication.ts +++ b/src/store/modules/authentication.ts @@ -1,6 +1,7 @@ import { ActionContext, Module } from 'vuex' import axios from 'axios' import { verify } from 'jsonwebtoken' +import { mapProviders, Providers } from '@/util/ProvidersUtil' export const JWT_KEY = 'jsonwebtoken' @@ -12,19 +13,6 @@ export enum JWTRegion { SA = 'sa', } -export function regionDomain (region: JWTRegion): string { - switch (region) { - case JWTRegion.EU: - return 'eu' - case JWTRegion.NA: - return 'na' - case JWTRegion.RU: - return 'ru' - case JWTRegion.SA: - return 'asia' - } -} - export interface JWT { iss: string; // Who issued it. Example: GameTactic. aud: string; // For what. Example: GameTactic. @@ -42,6 +30,7 @@ export interface ExtendedJWT extends JWT { export interface AuthenticationState { jwt: ExtendedJWT | null; + providers: Providers; } export enum AuthenticationActions { @@ -49,16 +38,21 @@ export enum AuthenticationActions { CHECK_TOKEN_EXPIRY = 'checkTokenExpiry', LOGIN_WG = 'auth_wg', LOGOUT = 'logout', - STORE_TOKEN = 'storeToken' + STORE_TOKEN = 'storeToken', + LOAD_PROVIDERS = 'loadProviders' } export enum AuthenticationMutation { - SET_AUTHENTICATION_TOKEN = 'SET_AUTHENTICATION_TOKEN' + SET_AUTHENTICATION_TOKEN = 'SET_AUTHENTICATION_TOKEN', + SET_PROVIDERS = 'SET_PROVIDERS' } export enum AuthenticationGetters { JWT = 'jwt', - IS_AUTH = 'authenticated' + IS_AUTH = 'authenticated', + PROVIDER_NAMES = 'providers', + PROVIDER = 'provider', + PROVIDERS = 'providers' } type AuthenticationActionContext = ActionContext @@ -67,22 +61,27 @@ const AuthenticationModule: Module = { namespaced: true, state () { return { - jwt: null + jwt: null, + providers: {} } }, getters: { [AuthenticationGetters.JWT]: state => state.jwt, - [AuthenticationGetters.IS_AUTH]: state => state.jwt !== null + [AuthenticationGetters.IS_AUTH]: state => state.jwt !== null, + [AuthenticationGetters.PROVIDER_NAMES]: state => Object.getOwnPropertyNames(state.providers), + [AuthenticationGetters.PROVIDERS]: state => state.providers, + [AuthenticationGetters.PROVIDER]: state => (name: string) => state.providers[name] }, mutations: { [AuthenticationMutation.SET_AUTHENTICATION_TOKEN] (state: AuthenticationState, payload: ExtendedJWT) { state.jwt = payload + }, + [AuthenticationMutation.SET_PROVIDERS] (state: AuthenticationState, payload: Providers) { + state.providers = payload } }, actions: { async [AuthenticationActions.AUTHENTICATE] (context: AuthenticationActionContext, token: string) { - // TODO: This is just placeholder logic. Please check it works. -Niko - const response = await axios.get((process.env.VUE_APP_MS_AUTH as string)) if (response.status !== 200) { throw Error('Could not reach authentication server.') @@ -91,8 +90,6 @@ const AuthenticationModule: Module = { const jwt = verify(token, response.data.publicKey) as JWT const extended: ExtendedJWT = { ...jwt, ...{ encoded: token } } - // TODO: You probably want put this into the `state`. -Niko - // Im not sure how this should be done, so I did it as I know. context.commit('SET_AUTHENTICATION_TOKEN', extended) return extended }, @@ -122,14 +119,25 @@ const AuthenticationModule: Module = { [AuthenticationActions.STORE_TOKEN] (context: AuthenticationActionContext, token: string) { localStorage.setItem(JWT_KEY, token) }, - [AuthenticationActions.LOGIN_WG] (context: AuthenticationActionContext, region: JWTRegion) { - // TODO: This is just placeholder logic. Please check it works. -Niko - const returnUrl = process.env.VUE_APP_MS_AUTH + `/connect/wargaming/${regionDomain(region)}/${window.location.href}` + [AuthenticationActions.LOGIN_WG] (context: AuthenticationActionContext, endpoint: string) { + const returnUrl = `${process.env.VUE_APP_MS_AUTH}${endpoint}/${window.location.href}` location.assign(returnUrl) }, [AuthenticationActions.LOGOUT] (context: AuthenticationActionContext) { localStorage.removeItem(JWT_KEY) context.commit(AuthenticationMutation.SET_AUTHENTICATION_TOKEN, null) + }, + async [AuthenticationActions.LOAD_PROVIDERS] (context: AuthenticationActionContext) { + if (process.env.VUE_APP_MS_AUTH) { + const response = await axios.get(process.env.VUE_APP_MS_AUTH) + if (response.status !== 200 || !(response.data?.providers)) { + throw Error('Could not fetch authentication providers') + } + const providers = response.data.providers + context.commit(AuthenticationMutation.SET_PROVIDERS, mapProviders(providers)) + } else { + throw Error('Authentication URI is not set') + } } } } diff --git a/src/store/modules/types.ts b/src/store/modules/types.ts index 31ec6cf..c868f12 100644 --- a/src/store/modules/types.ts +++ b/src/store/modules/types.ts @@ -23,7 +23,7 @@ export interface Game { api: Api[]; } -export enum RoleTypes { +export enum RoleTypes { ROON_OWNER = 'roomOwner', ADMIN = 'admin' } @@ -85,4 +85,4 @@ export interface PresentationPayload { export interface Presentation { enabledBy: string | undefined; tacticId: string | undefined; -} \ No newline at end of file +} diff --git a/src/util/ProvidersUtil.ts b/src/util/ProvidersUtil.ts new file mode 100644 index 0000000..6fa025c --- /dev/null +++ b/src/util/ProvidersUtil.ts @@ -0,0 +1,37 @@ +import { Indexed } from '@/util/TypeUtils' + +export interface ProviderEntry { + key: string; + endpoint: string; +} + +export interface Provider { + entries: ProviderEntry[]; +} + +export type Providers = Indexed; + +export type ProviderInput = Indexed>; + +export function mapProvider (input: {[key: string]: string}) { + const provider: Provider = { + entries: [] + } + for (const entry in input) { + provider.entries.push({ + key: entry, + endpoint: input[entry] + }) + } + return provider +} + +export function mapProviders (input: ProviderInput): Providers { + const providers: Providers = {} + + for (const provider in input) { + if (input.hasOwnProperty(provider)) { providers[provider] = mapProvider(input[provider]) } + } + + return providers +} diff --git a/src/util/TypeUtils.ts b/src/util/TypeUtils.ts new file mode 100644 index 0000000..cf32011 --- /dev/null +++ b/src/util/TypeUtils.ts @@ -0,0 +1,4 @@ + +export interface Indexed { + [key: string]: T; +}