Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Twitch authentication #107

Merged
merged 1 commit into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"recommendations": [
"aaron-bond.better-comments",
"antfu.iconify",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"github.copilot",
"github.copilot-chat",
"github.vscode-github-actions",
"ms-azuretools.vscode-docker",
"mtxr.sqltools",
"mtxr.sqltools-driver-pg",
"pflannery.vscode-versionlens",
"ritwickdey.liveserver",
"tamasfe.even-better-toml",
"visualstudioexptteam.intellicode-api-usage-examples",
"visualstudioexptteam.vscodeintellicode",
"vue.volar",
"vunguyentuan.vscode-css-variables",
"yoavbls.pretty-ts-errors"
]
}
2 changes: 1 addition & 1 deletion app/composables/use-equipment-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { EquipmentItem } from '~/components/equipment/EquipmentTable.vue';
interface EquipmentData {
readonly id: string;
readonly name: string | null;
readonly weight: number | null;
readonly weight: number;
readonly createdAt: string;
}

Expand Down
6 changes: 5 additions & 1 deletion app/middleware/0.user.global.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export default defineNuxtRouteMiddleware(async () => {
export default defineNuxtRouteMiddleware(async (to) => {
if (shouldSkipAuth(to)) {
return
}

if (import.meta.server) {
const { getUser, user } = useUserStore()

Expand Down
4 changes: 4 additions & 0 deletions app/middleware/1.auth.global.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { startPagePath } from '~~/constants';

export default defineNuxtRouteMiddleware(async (to) => {
if (shouldSkipAuth(to)) {
return
}

const { isAuthenticated } = useUserStore()

if (isAuthenticated.value && to.path === '/login') {
Expand Down
1 change: 1 addition & 0 deletions app/models/oauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type OAuthProvider = 'twitch'
25 changes: 25 additions & 0 deletions app/models/twitch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export interface TwitchUser {
readonly id: string;
readonly login: string;
readonly display_name: string;
readonly type: '' | 'staff' | 'admin' | 'global_mod';
readonly broadcaster_type: '' | 'affiliate' | 'partner';
readonly description: string;
readonly profile_image_url: string;
readonly offline_image_url: string;
readonly created_at: string;
// "user:read:email" scope required
readonly email?: string;
}

export interface TwitchOAuthTokenResponse {
readonly access_token: string;
readonly expires_in: number;
readonly refresh_token: string;
readonly token_type: string;
readonly scope?: string[];
}

export interface TwitchUsersResponse {
readonly data: TwitchUser[];
}
109 changes: 109 additions & 0 deletions app/pages/auth/twitch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<template>
<div :class="$style.component">
<div :class="$style.content">
<div :class="$style.progress">
<div
v-if="isFailed"
:class="$style.icon"
>
💀
</div>

<CircleSpinner
v-else
/>
</div>

<div v-if="isFailed">
Failed to connect <strong>Twitch</strong>
</div>

<div v-else>
Connecting <strong>Twitch</strong>...
</div>

<PerdLink
v-if="isFailed"
to="/"
>
Return to Home
</PerdLink>
</div>
</div>
</template>

<script lang="ts" setup>
import { startPagePath } from '~~/constants'
import CircleSpinner from '~/components/CircleSpinner.vue'
import PerdLink from '~/components/PerdLink.vue';

definePageMeta({
layout: false,
skipAuth: true
})

const isFailed = useState(() => false)
const route = useRoute()
const { user } = useUserStore()

async function handleConnect() {
try {
const result = await $fetch('/api/oauth/twitch', {
method: 'POST',

body: JSON.stringify({
code: route.query.code
})
})

if (result === undefined) {
throw new Error('Failed to connect Twitch')
}

user.value.userId = result.userId
user.value.isAdmin = result.isAdmin
user.value.hasData = true

await navigateTo(startPagePath, {
replace: true
})
} catch (error) {
isFailed.value = true
}
}

onBeforeMount(() => {
handleConnect()
})
</script>

<style module>
.component {
width: 100vw;
height: 100vh;
display: flex;
overflow: hidden;
padding: var(--spacing-24);
}

.content {
display: grid;
row-gap: var(--spacing-16);
margin: auto;
justify-items: center;
text-align: center;
text-wrap: balance;
}

.progress {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
}

.icon {
font-size: 32px;
}
</style>
37 changes: 13 additions & 24 deletions app/pages/login.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
<template>
<div :class="$style.component">
<PerdButton @click="handleSignInClick">
Sign in anonymously
<PerdButton @click="signUp">
Sign up anonymously
</PerdButton>

<PerdButton @click="handleSignInAdminClick">
Sign in as admin
</PerdButton>
<PerdLink
to="/api/oauth/twitch"
external
>
Sign in with Twitch
</PerdLink>
</div>
</template>

<script lang="ts" setup>
import PerdButton from '~/components/PerdButton.vue';
import PerdLink from '~/components/PerdLink.vue';

definePageMeta({
layout: 'page'
Expand All @@ -20,23 +24,15 @@
const { user } = useUserStore()
const route = useRoute()

async function signIn(isAdmin: boolean) {
async function signUp() {
const response = await $fetch('/api/auth/create-session', {
method: 'POST',

body: {
isAdmin
}
method: 'POST'
})

if (typeof response.userId === 'string') {
user.value.userId = response.userId
}

if (response.isAdmin === true) {
user.value.isAdmin = true
}

const redirectPath =
typeof route.query.redirectTo === 'string'
? route.query.redirectTo
Expand All @@ -46,20 +42,13 @@
replace: true
})
}

async function handleSignInClick() {
await signIn(false)
}

async function handleSignInAdminClick() {
await signIn(true)
}
</script>

<style module>
.component {
display: flex;
display: grid;
justify-content: center;
text-align: center;
gap: var(--spacing-16);
padding-top: var(--spacing-32);
}
Expand Down
25 changes: 0 additions & 25 deletions app/utils/fetch.ts

This file was deleted.

5 changes: 5 additions & 0 deletions app/utils/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { RouteLocationNormalized } from 'vue-router'

export function shouldSkipAuth(to: RouteLocationNormalized) {
return to.meta.skipAuth === true
}
4 changes: 3 additions & 1 deletion constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ export const sessionSecret = 'e2353edd-0a91-4d60-bf3a-715fab5f6c00'
export const startPagePath = '/'

export const publicApiPaths = [
'/auth/create-session'
'/auth/create-session',
'/oauth',
'/oauth/twitch'
]

export const limits = {
Expand Down
35 changes: 34 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,44 @@ services:
POSTGRES_PASSWORD: perd
POSTGRES_USER: perd
POSTGRES_DB: perd
command: postgres -c max_connections=1000
command: -c max_connections=1000
restart: unless-stopped
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: [ 'CMD-SHELL', 'pg_isready -U perd' ]
interval: 10s
timeout: 5s
retries: 5

# https://github.com/neondatabase/neon/issues/4989
# https://github.com/neondatabase/serverless/issues/33#issuecomment-1634853042
# https://github.com/TimoWilhelm/local-neon-http-proxy/tree/main
# https://readme.localtest.me/
neon-proxy:
image: ghcr.io/timowilhelm/local-neon-http-proxy:main
environment:
- PG_CONNECTION_STRING=postgres://perd:perd@postgres:5432/perd
restart: unless-stopped
ports:
- '4444:4444'
depends_on:
postgres:
condition: service_healthy

neon-wsproxy:
image: ghcr.io/neondatabase/wsproxy:latest
environment:
APPEND_PORT: postgres:5432
ALLOW_ADDR_REGEX: .*
LOG_TRAFFIC: true
ports:
- "5433:80"
depends_on:
postgres:
condition: service_healthy

volumes:
postgres_data:
4 changes: 4 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export default defineNuxtConfig({
viewTransition: true
},

typescript: {
strict: true
},

devtools: {
enabled: true
},
Expand Down
Loading