mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 09:34:50 +00:00
feat: update auth flow (#5790)
* Backend routes for choosing username in OAuth flow * fix up oauth flow routes * improve URL-related OAuth code * Use user-provided callback addr instead of SELF_ADDR * Revert "Use user-provided callback addr instead of SELF_ADDR" This reverts commit7ea0635d86. * fix flow * fix: backend response for create oauth account * feat: new auth flow (#5840) * update auth with new designs * refactor: auth.js to auth.ts * refactor: componentize auth pages * fix: auth pages height * feat: initial implementation of new sign-in oauth * fix create account flow * fix checkbox * remove hard coded username * implement create user validation endpoint and add more specific error responses * feat: implement under 13 DOB guard and email/password validation route * fix: TOCTOU issue * refactor: pnpm prepr * fix: make sure staging uses staging * fix: hcaptcha styles * fix: copy * remove: auth/welcome page as its no longer used * refactor: bring root page card styles into individual components and use tailwind * fix: account settings modals to use new modal and fix lots of bad styles * refactor: pnpm prepr * feat: implement last signed in indicator * fix: append number when generated name from email is taken * refactor: pnpm prepr * fix: last sign in badge color * fix: qa issues * refactor: pnpm prepr * fix: hover effect on native date picker * chore: temp staging undo * Revert "chore: temp staging undo" This reverts commitcad6bd4f92. * feat: handle app create account * fix: last signed in style * fix: add initOnMounted for SSR race * refactor: use typescript * refactor: pnpm prepr * refactor: use typescript for reset-password * refactor: convert verify-email to use typescript * refactor: convert authorize.vue to use typescript * fix: authorize.vue error states * feat: small style updates * feat: implement date picker component * feat: improve UX and styles for range select * refactor: pnpm prepr * fix: range select border styles * feat: implement date picker component in create account * feat: implement preserve date for date picker * update rust toolchain * increase recursion limit * fix: date picker can be null * fix: calculate age based on user's timezone * fix: number input icons color * fix: date picker icons * feat: improve styles * fix: add width on date * fix: hover color bad on number input * fix lints * feat: add default date open view * fmt * fix: account.vue * fix: remove default date to open 13 years ago * fix: edit copy on info banner * fix: cannot hover over project card tooltip items (#6071) fix: cannot hover over project cards * feat: improve add dependency flow (#6075) * fix: shadow on nav * feat: improve add dependency flow * feat: update suggested dependency style * feat: update dependency rows to use version number and update styles * feat: implement combobox select searched text on focus * feat: add Tabs.vue * feat: update nav tabs to use tabs * feat: improve project search dropdown * fix: dependency search not clearing inbound query * fix: combobox no options open state bug * feat: improve dependency project and version search * fix: open modrinth project links in the app (#6072) * pin tanstack versions + set pnpm min age to 7 days * squash commits * fix: 2 factor auth enter code screen styles * update copy * update copy * improve reset password * feat: update sign in screen * fix: unused import * Merge branch 'main' into boris/dev-908-backend-changes * Revert "Merge branch 'main' into boris/dev-908-backend-changes" This reverts commitb9b03796e3. * fix: add stroke * feat: add passkey support (#6375) * feat: add passkey backend * feat: passkey frontend * invalidate sessions on compromised passkey * chore: run sqlx prepare * fix: make passkey button use both collumns to prevent empty space * fix: correctly verify max passkeys in finish route * fix: use structs for response * fix: add rp name default * style: use web::Json * fmt * feat: improve manage passkeys UI * fix copy * pnpm prepr --------- Co-authored-by: tdgao <mr.trumgao@gmail.com> Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com> Co-authored-by: Michael H. <michael@iptables.sh> Co-authored-by: Calum H. (IMB11) <contact@cal.engineer> Co-authored-by: Calum H. <calum@modrinth.com> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com> Co-authored-by: DeDiamondPro <67508414+DeDiamondPro@users.noreply.github.com>
This commit is contained in:
co-authored by
tdgao
Truman Gao
Michael H.
Calum H.
Calum H.
Prospector
DeDiamondPro
parent
6fc741f7c0
commit
ef4044534f
@@ -1,179 +0,0 @@
|
||||
function normalizeAuthToken(value) {
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export const useAuth = async (oldToken = null) => {
|
||||
const auth = useState('auth', () => ({
|
||||
user: null,
|
||||
token: '',
|
||||
headers: {},
|
||||
}))
|
||||
|
||||
if (!auth.value.user || oldToken) {
|
||||
auth.value = await initAuth(oldToken)
|
||||
}
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
export const initAuth = async (oldToken = null) => {
|
||||
const auth = {
|
||||
user: null,
|
||||
token: '',
|
||||
}
|
||||
|
||||
if (oldToken === 'none') {
|
||||
return auth
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const config = useRuntimeConfig()
|
||||
const authCookie = useCookie('auth-token', {
|
||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||
sameSite: 'lax',
|
||||
secure: config.public.cookieSecure,
|
||||
httpOnly: false,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
if (oldToken) {
|
||||
const normalized = normalizeAuthToken(oldToken)
|
||||
if (normalized) {
|
||||
authCookie.value = normalized
|
||||
}
|
||||
}
|
||||
|
||||
const oauthCode = normalizeAuthToken(route.query.code)
|
||||
if (oauthCode && !route.fullPath.includes('new_account=true')) {
|
||||
authCookie.value = oauthCode
|
||||
}
|
||||
|
||||
if (route.fullPath.includes('new_account=true') && route.path !== '/auth/welcome') {
|
||||
const redirect = route.path.startsWith('/auth/') ? null : route.fullPath
|
||||
|
||||
await navigateTo(
|
||||
`/auth/welcome?authToken=${oauthCode}${
|
||||
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''
|
||||
}`,
|
||||
)
|
||||
}
|
||||
|
||||
const tokenStr = normalizeAuthToken(authCookie.value)
|
||||
|
||||
if (authCookie.value != null && tokenStr === '') {
|
||||
authCookie.value = null
|
||||
} else if (tokenStr) {
|
||||
auth.token = tokenStr
|
||||
|
||||
if (!auth.token.startsWith('mra_')) {
|
||||
return auth
|
||||
}
|
||||
|
||||
try {
|
||||
auth.user = await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
apiVersion: 3,
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth.user && auth.token && typeof auth.token === 'string') {
|
||||
try {
|
||||
const session = await useBaseFetch(
|
||||
'session/refresh',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
auth.token = normalizeAuthToken(session.session)
|
||||
if (auth.token) {
|
||||
authCookie.value = auth.token
|
||||
auth.user = await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
apiVersion: 3,
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
authCookie.value = null
|
||||
auth.token = ''
|
||||
}
|
||||
} catch {
|
||||
authCookie.value = null
|
||||
}
|
||||
}
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
export const getSignInRedirectPath = (route) => {
|
||||
const fullPath = route.fullPath
|
||||
if (fullPath === '/auth' || fullPath.startsWith('/auth/')) {
|
||||
return '/dashboard'
|
||||
}
|
||||
return fullPath
|
||||
}
|
||||
|
||||
export const getSignInRouteObj = (route, redirectOverride) => ({
|
||||
path: '/auth/sign-in',
|
||||
query: {
|
||||
redirect: redirectOverride ?? getSignInRedirectPath(route),
|
||||
},
|
||||
})
|
||||
|
||||
export const getAuthUrl = (provider, redirect = '/dashboard') => {
|
||||
const config = useRuntimeConfig()
|
||||
const route = useNativeRoute()
|
||||
|
||||
const fullURL = route.query.launcher
|
||||
? getLauncherRedirectUrl(route)
|
||||
: `${config.public.siteUrl}/auth/sign-in?redirect=${encodeURIComponent(redirect)}`
|
||||
|
||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`
|
||||
}
|
||||
|
||||
export const removeAuthProvider = async (provider) => {
|
||||
startLoading()
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
await useBaseFetch('auth/provider', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
provider,
|
||||
},
|
||||
})
|
||||
|
||||
await useAuth(auth.value.token)
|
||||
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
export const getLauncherRedirectUrl = (route) => {
|
||||
const usesLocalhostRedirectionScheme =
|
||||
['4', '6'].includes(route.query.ipver) && Number(route.query.port) < 65536
|
||||
|
||||
return usesLocalhostRedirectionScheme
|
||||
? `http://${route.query.ipver === '4' ? '127.0.0.1' : '[::1]'}:${route.query.port}`
|
||||
: `https://launcher-files.modrinth.com`
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import type { LocationQueryValue, RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
import type { CookieOptions } from '#app'
|
||||
|
||||
type AuthState = {
|
||||
user: Labrinth.Users.v2.User | null
|
||||
token: string
|
||||
}
|
||||
|
||||
type QueryValue = LocationQueryValue | LocationQueryValue[] | undefined
|
||||
type FullPathRoute = Pick<RouteLocationNormalizedLoaded, 'fullPath'>
|
||||
type LauncherRoute = Pick<RouteLocationNormalizedLoaded, 'query'>
|
||||
|
||||
export const LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY = 'auth-last-sign-in-oauth-provider'
|
||||
export const PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY = 'auth-pending-sign-in-oauth-provider'
|
||||
|
||||
const AUTH_COOKIE_OPTIONS = {
|
||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||
sameSite: 'lax',
|
||||
httpOnly: false,
|
||||
path: '/',
|
||||
} satisfies CookieOptions<string | null>
|
||||
|
||||
const normalizeAuthToken = (value: unknown) => {
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const getQueryString = (value: QueryValue) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0] ?? null
|
||||
}
|
||||
return value ?? null
|
||||
}
|
||||
|
||||
export const useAuth = async (oldToken: string | null | undefined = null) => {
|
||||
const auth = useState<AuthState>('auth', () => ({
|
||||
user: null,
|
||||
token: '',
|
||||
}))
|
||||
|
||||
if (!auth.value.user || oldToken) {
|
||||
auth.value = await initAuth(oldToken)
|
||||
}
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
export const initAuth = async (oldToken: string | null | undefined = null) => {
|
||||
const auth: AuthState = {
|
||||
user: null,
|
||||
token: '',
|
||||
}
|
||||
|
||||
if (oldToken === 'none') {
|
||||
return auth
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const config = useRuntimeConfig()
|
||||
const authCookie = useCookie<string | null>('auth-token', {
|
||||
...AUTH_COOKIE_OPTIONS,
|
||||
secure: config.public.cookieSecure,
|
||||
})
|
||||
|
||||
if (oldToken) {
|
||||
const normalized = normalizeAuthToken(oldToken)
|
||||
if (normalized) {
|
||||
authCookie.value = normalized
|
||||
}
|
||||
}
|
||||
|
||||
const oauthCode = normalizeAuthToken(route.query.code)
|
||||
if (oauthCode && !route.fullPath.includes('new_account=true')) {
|
||||
authCookie.value = oauthCode
|
||||
}
|
||||
|
||||
if (route.fullPath.includes('new_account=true') && route.path !== '/auth/welcome') {
|
||||
const redirect = route.path.startsWith('/auth/') ? null : route.fullPath
|
||||
|
||||
await navigateTo(
|
||||
`/auth/welcome?authToken=${oauthCode}${
|
||||
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''
|
||||
}`,
|
||||
)
|
||||
}
|
||||
|
||||
const tokenStr = normalizeAuthToken(authCookie.value)
|
||||
|
||||
if (authCookie.value != null && tokenStr === '') {
|
||||
authCookie.value = null
|
||||
} else if (tokenStr) {
|
||||
auth.token = tokenStr
|
||||
|
||||
if (!auth.token.startsWith('mra_')) {
|
||||
return auth
|
||||
}
|
||||
|
||||
try {
|
||||
auth.user = (await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
apiVersion: 3,
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)) as Labrinth.Users.v2.User
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth.user && auth.token) {
|
||||
try {
|
||||
const session = (await useBaseFetch(
|
||||
'session/refresh',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)) as { session: unknown }
|
||||
|
||||
auth.token = normalizeAuthToken(session.session)
|
||||
if (auth.token) {
|
||||
authCookie.value = auth.token
|
||||
auth.user = (await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
apiVersion: 3,
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)) as Labrinth.Users.v2.User
|
||||
} else {
|
||||
authCookie.value = null
|
||||
auth.token = ''
|
||||
}
|
||||
} catch {
|
||||
authCookie.value = null
|
||||
}
|
||||
}
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
export const getSignInRedirectPath = (route: FullPathRoute) => {
|
||||
const fullPath = route.fullPath
|
||||
if (fullPath === '/auth' || fullPath.startsWith('/auth/')) {
|
||||
return '/dashboard'
|
||||
}
|
||||
return fullPath
|
||||
}
|
||||
|
||||
export const getSignInRouteObj = (route: FullPathRoute, redirectOverride?: string | null) => ({
|
||||
path: '/auth/sign-in',
|
||||
query: {
|
||||
redirect: redirectOverride ?? getSignInRedirectPath(route),
|
||||
},
|
||||
})
|
||||
|
||||
export const getAuthUrl = (provider: string, redirect = '/dashboard') => {
|
||||
const config = useRuntimeConfig()
|
||||
const route = useNativeRoute()
|
||||
const launcher = getQueryString(route.query.launcher)
|
||||
|
||||
const fullURL = launcher
|
||||
? (() => {
|
||||
const callbackUrl = new URL('/auth/sign-in', config.public.siteUrl)
|
||||
callbackUrl.searchParams.set('launcher', launcher)
|
||||
|
||||
const ipver = getQueryString(route.query.ipver)
|
||||
const port = getQueryString(route.query.port)
|
||||
|
||||
if (ipver) {
|
||||
callbackUrl.searchParams.set('ipver', ipver)
|
||||
}
|
||||
|
||||
if (port) {
|
||||
callbackUrl.searchParams.set('port', port)
|
||||
}
|
||||
|
||||
return callbackUrl.toString()
|
||||
})()
|
||||
: `${config.public.siteUrl}/auth/sign-in?redirect=${encodeURIComponent(redirect)}`
|
||||
|
||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`
|
||||
}
|
||||
|
||||
export const promotePendingSignInOAuthProvider = () => {
|
||||
if (!import.meta.client) return
|
||||
const pending = useStorage<string | null>(
|
||||
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
null,
|
||||
undefined,
|
||||
{ initOnMounted: true },
|
||||
)
|
||||
if (!pending.value) return
|
||||
const last = useStorage<string | null>(LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY, null, undefined, {
|
||||
initOnMounted: true,
|
||||
})
|
||||
last.value = pending.value
|
||||
pending.value = null
|
||||
}
|
||||
|
||||
export const removeAuthProvider = async (provider: string) => {
|
||||
startLoading()
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
await useBaseFetch('auth/provider', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
provider,
|
||||
},
|
||||
})
|
||||
|
||||
await useAuth(auth.value.token)
|
||||
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
export const getLauncherRedirectUrl = (route: LauncherRoute) => {
|
||||
const ipver = getQueryString(route.query.ipver)
|
||||
const port = Number(getQueryString(route.query.port))
|
||||
const usesLocalhostRedirectionScheme = ['4', '6'].includes(ipver ?? '') && port < 65536
|
||||
|
||||
return usesLocalhostRedirectionScheme
|
||||
? `http://${ipver === '4' ? '127.0.0.1' : '[::1]'}:${port}`
|
||||
: 'https://launcher-files.modrinth.com'
|
||||
}
|
||||
Reference in New Issue
Block a user