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 commit 7ea0635d86.

* 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 commit cad6bd4f92.

* 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 commit b9b03796e3.

* 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:
aecsocket
2026-06-25 20:36:19 +00:00
committed by GitHub
co-authored by tdgao Truman Gao Michael H. Calum H. Calum H. Prospector DeDiamondPro
parent 6fc741f7c0
commit ef4044534f
125 changed files with 5170 additions and 2828 deletions
@@ -57,6 +57,39 @@ export class LabrinthAuthV2Module extends AbstractModule {
})
}
/**
* Validate email/password inputs for account creation without creating an account.
*
* @param data - Prospective account credentials
*/
public async validateCreateAccount(
data: Labrinth.Auth.v2.ValidateCreateAccountRequest,
): Promise<void> {
return this.client.request(`/auth/create/validate`, {
api: 'labrinth',
version: 2,
method: 'POST',
body: data,
})
}
/**
* Create a new account from an OAuth callback flow state
*
* @param data - OAuth account creation data
* @returns Promise resolving to a session response
*/
public async createOAuthAccount(
data: Labrinth.Auth.v2.CreateOAuthAccountRequest,
): Promise<Labrinth.Auth.v2.CreateOAuthAccountResponse> {
return this.client.request<Labrinth.Auth.v2.CreateOAuthAccountResponse>(`/auth/create/oauth`, {
api: 'labrinth',
version: 2,
method: 'POST',
body: data,
})
}
/**
* Begin a password reset flow by sending a recovery email
*
@@ -84,4 +117,116 @@ export class LabrinthAuthV2Module extends AbstractModule {
body: data,
})
}
/**
* List the current user's registered passkeys
*
* @returns A promise that resolves to a list of the user's registered passkeys
*/
public async listPasskeys(): Promise<Labrinth.Auth.v2.Passkey[]> {
return this.client.request<Labrinth.Auth.v2.Passkey[]>(`/auth/passkey`, {
api: 'labrinth',
version: 2,
method: 'GET',
})
}
/**
* Begin registering a new passkey, returning the WebAuthn creation options and a flow
*
* @returns A promise that resolves to the WebAuthn creation options and flow
*/
public async registerPasskeyStart(): Promise<Labrinth.Auth.v2.PasskeyRegisterStartResponse> {
return this.client.request<Labrinth.Auth.v2.PasskeyRegisterStartResponse>(
`/auth/passkey/register/start`,
{
api: 'labrinth',
version: 2,
method: 'POST',
},
)
}
/**
* Complete passkey registration with the created credential
*
* @param data The credential data and flow to complete registration with
* @returns A promise that resolves to the newly registered passkey
*/
public async registerPasskeyFinish(
data: Labrinth.Auth.v2.PasskeyRegisterFinishRequest,
): Promise<Labrinth.Auth.v2.Passkey> {
return this.client.request<Labrinth.Auth.v2.Passkey>(`/auth/passkey/register/finish`, {
api: 'labrinth',
version: 2,
method: 'POST',
body: data,
})
}
/**
* Begin a passkey authentication flow, returning the WebAuthn request options and a flow
*
* @returns A promise that resolves to the WebAuthn request options and a flow
*/
public async authenticatePasskeyStart(): Promise<Labrinth.Auth.v2.PasskeyAuthenticateStartResponse> {
return this.client.request<Labrinth.Auth.v2.PasskeyAuthenticateStartResponse>(
`/auth/passkey/start`,
{
api: 'labrinth',
version: 2,
method: 'POST',
skipAuth: true,
},
)
}
/**
* Complete a passkey authentication flow, returning the new session
*
* @param data The credential data and flow to complete authentication with
* @returns A promise that resolves to the new session
*/
public async authenticatePasskeyFinish(
data: Labrinth.Auth.v2.PasskeyAuthenticateFinishRequest,
): Promise<Labrinth.Sessions.v2.Session> {
return this.client.request<Labrinth.Sessions.v2.Session>(`/auth/passkey/finish`, {
api: 'labrinth',
version: 2,
method: 'POST',
body: data,
skipAuth: true,
})
}
/**
* Rename a passkey
*
* @param id The ID of the passkey to rename
* @param data The new name for the passkey
*/
public async renamePasskey(
id: string,
data: Labrinth.Auth.v2.PasskeyRenameRequest,
): Promise<void> {
return this.client.request(`/auth/passkey/${id}`, {
api: 'labrinth',
version: 2,
method: 'PATCH',
body: data,
})
}
/**
* Delete a passkey
*
* @param id The ID of the passkey to delete
*/
public async deletePasskey(id: string): Promise<void> {
return this.client.request(`/auth/passkey/${id}`, {
api: 'labrinth',
version: 2,
method: 'DELETE',
})
}
}
@@ -695,6 +695,23 @@ export namespace Labrinth {
session: string
}
export type ValidateCreateAccountRequest = {
username: string
password: string
email: string
}
export type CreateOAuthAccountRequest = {
username: string
state: string
challenge: string
sign_up_newsletter: boolean
}
export type CreateOAuthAccountResponse = {
session: string
}
export type ResetPasswordRequest = {
username: string
challenge: string
@@ -705,6 +722,38 @@ export namespace Labrinth {
old_password?: string
new_password?: string
}
export type Passkey = {
id: string
name: string
created_at: string
last_used: string | null
}
export type PasskeyRegisterStartResponse = {
options: Record<string, unknown>
flow: string
}
export type PasskeyRegisterFinishRequest = {
flow: string
name: string
credential: unknown
}
export type PasskeyAuthenticateStartResponse = {
options: Record<string, unknown>
flow: string
}
export type PasskeyAuthenticateFinishRequest = {
flow: string
credential: unknown
}
export type PasskeyRenameRequest = {
name: string
}
}
}
+2
View File
@@ -418,6 +418,7 @@ import _UpdatedIcon from './icons/updated.svg?component'
import _UploadIcon from './icons/upload.svg?component'
import _UserIcon from './icons/user.svg?component'
import _UserCogIcon from './icons/user-cog.svg?component'
import _UserKeyIcon from './icons/user-key.svg?component'
import _UserPlusIcon from './icons/user-plus.svg?component'
import _UserRoundIcon from './icons/user-round.svg?component'
import _UserSearchIcon from './icons/user-search.svg?component'
@@ -848,6 +849,7 @@ export const UpdatedIcon = _UpdatedIcon
export const UploadIcon = _UploadIcon
export const UserIcon = _UserIcon
export const UserCogIcon = _UserCogIcon
export const UserKeyIcon = _UserKeyIcon
export const UserPlusIcon = _UserPlusIcon
export const UserRoundIcon = _UserRoundIcon
export const UserSearchIcon = _UserSearchIcon
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-key-icon lucide-user-key"><path d="M20 11v6"/><path d="M20 13h2"/><path d="M3 21v-2a4 4 0 0 1 4-4h6a4 4 0 0 1 2.072.578"/><circle cx="10" cy="7" r="4"/><circle cx="20" cy="19" r="2"/></svg>

After

Width:  |  Height:  |  Size: 396 B

+1 -1
View File
@@ -14,7 +14,7 @@
@click="toggle"
>
<span
class="w-5 h-5 rounded-md flex items-center justify-center border-[1px] border-solid shrink-0"
class="w-5 h-5 aspect-square rounded-md flex shrink-0 items-center justify-center border-[1px] border-solid"
:class="{
'bg-brand border-button-border text-brand-inverted': modelValue,
'bg-surface-2 border-surface-5 text-primary': !modelValue,
@@ -16,7 +16,7 @@
<slot name="title" />
</div>
<div class="grid-area-[description] flex flex-col gap-[var(--gap-md)]">
<div class="grid-area-[description] flex flex-col gap-3">
<slot name="description" />
</div>
@@ -2,7 +2,7 @@
<div
data-pyro-server-list-root
class="relative mx-auto mb-6 flex w-full flex-col p-6"
:class="serverList.length ? 'min-h-screen' : 'min-h-[calc(100vh-4.5rem)]'"
:class="serverList.length ? 'min-h-screen' : 'min-h-[calc(100vh-14.5rem)]'"
>
<ServersGuestPlanModal
ref="guestPlanModal"
+1 -1
View File
@@ -237,7 +237,7 @@
"defaultMessage": "Re-install modpack"
},
"button.remove": {
"defaultMessage": "Remove"
"defaultMessage": "Delete passkey"
},
"button.remove-image": {
"defaultMessage": "Remove image"
+1 -1
View File
@@ -245,7 +245,7 @@ export const commonMessages = defineMessages({
},
removeButton: {
id: 'button.remove',
defaultMessage: 'Remove',
defaultMessage: 'Delete passkey',
},
removeImageButton: {
id: 'button.remove-image',