feat(auth): Google SSO via @adonisjs/ally
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 55s
Build & Deploy API / build-and-deploy (push) Successful in 1m35s

Backend
- @adonisjs/ally installé + provider Google configuré (config/ally.ts)
  scopes: userinfo.email + userinfo.profile (non-sensibles, validation
  auto par Google)
- Migration : ajoute google_id (nullable unique) sur users + rend password
  nullable (un user créé via Google n'a pas de mdp en base, il pourra
  l'activer plus tard via "mot de passe oublié")
- AuthGoogleController.redirect : entrée OAuth (le bouton SPA pointe ici)
- AuthGoogleController.callback : matche par google_id puis email,
  crée org+plans+user si nouveau, pose le refresh cookie httpOnly,
  redirige le browser vers le SPA /auth/google/complete?next=...
  (next = / pour user complet, /onboarding/entreprise pour nouveau)
- Routes : GET /api/v1/auth/google/{redirect,callback}
- Env : GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URL

Frontend
- Composant GoogleButton réutilisable (full-page redirect, pas fetch —
  OAuth nécessite navigation pour les cookies cross-origin Google)
- AuthDivider "ou" entre SSO et formulaire email/password
- Boutons ajoutés sur /login et /signup
- Route /auth/google/complete : appelle POST /api/v1/auth/refresh (le
  cookie posé par la callback est auto-envoyé), stocke access token +
  user dans authStore, navigue vers `next`. Échec → /login + toast.
- Toast d'erreur sur /login si on revient avec ?google=denied|error|...

K3s
- ConfigMap rubis-api-config : ajout GOOGLE_CALLBACK_URL prod
- Secret rubis-app-secrets : ajout GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET
  (posés via kubectl, pas dans le manifest)

Doc
- .claude/deploy-memory.md mis à jour avec la procédure Google Cloud
  Console (créer OAuth client, redirect URIs, écran de consentement)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-05-07 09:24:27 +02:00
parent 27771ed538
commit ea539cd1d4
16 changed files with 462 additions and 5 deletions

View File

@ -87,9 +87,30 @@ kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
--from-literal=RESEND_API_KEY=... \
--from-literal=MISTRAL_API_KEY=... \
--from-literal=REDIS_PASSWORD="" \
--from-literal=GOOGLE_CLIENT_ID=... \
--from-literal=GOOGLE_CLIENT_SECRET=... \
--dry-run=client -o yaml | kubectl apply -f -
```
### Google SSO — setup Google Cloud Console
Si la clé OAuth est perdue ou qu'on doit la régénérer :
1. https://console.cloud.google.com/apis/credentials → projet courant
2. **Create Credentials****OAuth client ID** → type **Web application**
3. **Authorized JavaScript origins** :
- `https://app.rubis.arthurbarre.fr`
- `http://localhost:5173` (dev SPA)
4. **Authorized redirect URIs** :
- `https://app.rubis.arthurbarre.fr/api/v1/auth/google/callback`
- `http://localhost:3333/api/v1/auth/google/callback` (dev API)
5. Copier `Client ID` + `Client secret` → mettre dans `apps/api/.env` (dev)
et `rubis-app-secrets` (prod, snippet ci-dessus).
L'écran de consentement OAuth doit être configuré au moins en mode "Testing"
avec l'email du user courant ajouté en testeur. Pour la prod (n'importe qui
peut se connecter), il faut passer en "In production" (vérification Google
si scopes sensibles ; les nôtres `userinfo.email` + `userinfo.profile` sont
non-sensibles, validation auto).
### Mise à jour
Push git → un (ou les deux) workflow(s) CI se déclenchent selon les paths
modifiés. Build+rollout indépendants.

View File

@ -77,4 +77,14 @@ ACCESS_TOKEN_TTL_MINUTES=30
REFRESH_TOKEN_TTL_DAYS=30
COOKIE_DOMAIN=
COOKIE_SECURE=false
#--------------------------------------------------------------------
# Google SSO (Ally) — créer un OAuth Client ID web sur Google Cloud
# Console, puis ajouter les redirect URIs :
# - http://localhost:3333/api/v1/auth/google/callback (dev)
# - https://app.rubis.arthurbarre.fr/api/v1/auth/google/callback (prod)
#--------------------------------------------------------------------
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=http://localhost:3333/api/v1/auth/google/callback
LIMITER_STORE=redis

View File

@ -59,7 +59,8 @@ export default defineConfig({
() => import('@adonisjs/limiter/limiter_provider'),
() => import('@adonisjs/mail/mail_provider'),
() => import('@adonisjs/drive/drive_provider'),
() => import('@adonisjs/static/static_provider')
() => import('@adonisjs/static/static_provider'),
() => import('@adonisjs/ally/ally_provider'),
],
/*

View File

@ -0,0 +1,128 @@
import User from '#models/user'
import Organization from '#models/organization'
import { provisionDefaultPlans } from '#services/default_plans'
import { issueRefreshToken } from '#services/refresh_token'
import env from '#start/env'
import db from '@adonisjs/lucid/services/db'
import logger from '@adonisjs/core/services/logger'
import type { HttpContext } from '@adonisjs/core/http'
import crypto from 'node:crypto'
/**
* Google SSO via @adonisjs/ally.
*
* Flow :
* 1. Le SPA navigate vers GET /api/v1/auth/google/redirect
* 2. Ally redirige vers Google avec state + scopes
* 3. Google redirige vers GET /api/v1/auth/google/callback?code=...
* 4. Backend matche/crée l'user, pose le refresh cookie (httpOnly)
* 5. Redirige le browser vers le SPA sur /auth/google/complete?next=...
* 6. Le SPA appelle POST /api/v1/auth/refresh (cookie auto-envoyé) reçoit
* un access token, navigue vers `next`.
*
* On NE retourne PAS d'access token en JSON ici car la callback est un
* redirect server-side : pas de body lisible par le SPA.
*/
export default class AuthGoogleController {
/**
* GET /api/v1/auth/google/redirect entrée du flow OAuth.
* Le bouton "Continuer avec Google" pointe directement ici (pas un fetch).
*/
async redirect(ctx: HttpContext) {
return ctx.ally.use('google').redirect()
}
/**
* GET /api/v1/auth/google/callback retour de Google.
*
* Stratégie de matching :
* 1. Existing user with `google_id` ? log in
* 2. Existing user with same `email` ? link `google_id` + log in
* 3. New user crée org + plans par défaut + user (password null)
*/
async callback(ctx: HttpContext) {
const google = ctx.ally.use('google')
const webUrl = env.get('WEB_URL', 'http://localhost:5173')
// Erreurs côté Google (canceled, mismatch, etc.)
if (google.accessDenied()) {
return ctx.response.redirect(`${webUrl}/login?google=denied`)
}
if (google.stateMisMatch()) {
return ctx.response.redirect(`${webUrl}/login?google=state_mismatch`)
}
if (google.hasError()) {
logger.warn({ err: google.getError() }, 'google sso error')
return ctx.response.redirect(`${webUrl}/login?google=error`)
}
const googleUser = await google.user()
if (!googleUser.email) {
logger.warn({ id: googleUser.id }, 'google sso : email manquant')
return ctx.response.redirect(`${webUrl}/login?google=no_email`)
}
// 1. Lookup par google_id (canonique)
let user = await User.findBy('googleId', googleUser.id)
let isNewUser = false
// 2. Fallback email — première connexion d'un user email/password
// via Google, on lie son google_id à son compte existant.
if (!user) {
user = await User.findBy('email', googleUser.email.toLowerCase())
if (user) {
user.googleId = googleUser.id
if (!user.fullName && googleUser.name) {
user.fullName = googleUser.name
}
await user.save()
}
}
// 3. Création
if (!user) {
isNewUser = true
user = await db.transaction(async (trx) => {
const org = await Organization.create({ name: '' }, { client: trx })
await provisionDefaultPlans(org.id, trx)
return User.create(
{
email: googleUser.email!.toLowerCase(),
fullName: googleUser.name ?? null,
// password requis dans le schéma Lucid (string), mais nullable
// en DB. On stocke un random unguessable que personne ne peut
// utiliser pour login email/password (User.verifyCredentials
// hash-comparera et échouera). Le user pourra plus tard
// activer email/password via "mot de passe oublié".
password: crypto.randomBytes(48).toString('base64url'),
googleId: googleUser.id,
organizationId: org.id,
},
{ client: trx }
)
})
}
// Pose le refresh cookie httpOnly (path /api/v1/auth)
await issueRefreshToken(user, ctx)
// Décide où renvoyer l'utilisateur.
// Org name vide = onboarding entreprise pas terminé → on saute "compte"
// (Google nous a déjà donné nom + email) et on enchaîne sur entreprise.
let next = '/'
if (isNewUser) {
next = '/onboarding/entreprise'
} else {
const org = user.organizationId
? await Organization.find(user.organizationId)
: null
if (!org || !org.name) {
next = '/onboarding/entreprise'
} else if (!user.signature) {
next = '/onboarding/signature'
}
}
return ctx.response.redirect(`${webUrl}/auth/google/complete?next=${encodeURIComponent(next)}`)
}
}

36
apps/api/config/ally.ts Normal file
View File

@ -0,0 +1,36 @@
import env from '#start/env'
import { defineConfig, services } from '@adonisjs/ally'
/**
* Configuration des providers OAuth (Ally).
*
* V1 : Google uniquement (cf. CLAUDE.md Auth). Les autres viendront
* plus tard si pertinent (Microsoft pour les TPE qui utilisent O365 ?).
*
* Le callback URL pointe vers l'API en interne (/api/v1/auth/google/callback).
* En prod, le reverse proxy nginx (rubis-web) achemine /api/* vers ce
* service, donc la même URL fonctionne pour le browser et pour Google.
*/
const allyConfig = defineConfig({
google: services.google({
clientId: env.get('GOOGLE_CLIENT_ID', ''),
clientSecret: env.get('GOOGLE_CLIENT_SECRET', ''),
callbackUrl: env.get(
'GOOGLE_CALLBACK_URL',
'http://localhost:3333/api/v1/auth/google/callback'
),
// Scopes minimaux : on a juste besoin de l'email + nom + photo (avatar
// optionnel V2). Pas de Drive/Calendar : on ne touche pas aux données
// Google de l'utilisateur, on s'en sert juste comme provider d'identité.
scopes: ['userinfo.email', 'userinfo.profile'],
prompt: 'select_account',
}),
})
export default allyConfig
declare module '@adonisjs/ally/types' {
interface SocialProviders extends InferSocialProviders<typeof allyConfig> {}
}
import type { InferSocialProviders } from '@adonisjs/ally/types'

View File

@ -0,0 +1,28 @@
import { BaseSchema } from '@adonisjs/lucid/schema'
/**
* Ajoute le support Google SSO :
* - `google_id` : sub OAuth Google (stable, unique). On match les retours
* d'auth dessus en priorité, fallback email si non rempli (cas d'un user
* email/password qui ajoute Google plus tard).
* - Rend `password` nullable : un user créé via Google n'a pas de password
* en base. Il pourra en définir un plus tard via "mot de passe oublié".
*/
export default class extends BaseSchema {
protected tableName = 'users'
async up() {
this.schema.alterTable(this.tableName, (table) => {
table.string('google_id', 64).nullable().unique()
table.string('password').nullable().alter()
})
}
async down() {
this.schema.alterTable(this.tableName, (table) => {
table.dropColumn('google_id')
// On ne re-passe pas password à NOT NULL en down — risquerait de
// casser les rows SSO existantes.
})
}
}

View File

@ -302,7 +302,7 @@ export class RelanceTaskSchema extends BaseModel {
}
export class UserSchema extends BaseModel {
static $columns = ['createdAt', 'email', 'fullName', 'id', 'organizationId', 'password', 'signature', 'updatedAt'] as const
static $columns = ['createdAt', 'email', 'fullName', 'googleId', 'id', 'organizationId', 'password', 'signature', 'updatedAt'] as const
$columns = UserSchema.$columns
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@ -310,12 +310,14 @@ export class UserSchema extends BaseModel {
declare email: string
@column()
declare fullName: string | null
@column()
declare googleId: string | null
@column({ isPrimary: true })
declare id: string
@column()
declare organizationId: string | null
@column({ serializeAs: null })
declare password: string
declare password: string | null
@column()
declare signature: string | null
@column.dateTime({ autoCreate: true, autoUpdate: true })

View File

@ -59,6 +59,7 @@
"youch": "^4.1.1"
},
"dependencies": {
"@adonisjs/ally": "^6.3.0",
"@adonisjs/auth": "^10.1.0",
"@adonisjs/bouncer": "^4.0.0",
"@adonisjs/core": "^7.3.1",

View File

@ -68,6 +68,11 @@ export default await Env.create(new URL('../', import.meta.url), {
COOKIE_DOMAIN: Env.schema.string.optional(),
COOKIE_SECURE: Env.schema.boolean.optional(),
// Google SSO (Ally)
GOOGLE_CLIENT_ID: Env.schema.string.optional(),
GOOGLE_CLIENT_SECRET: Env.schema.string.optional(),
GOOGLE_CALLBACK_URL: Env.schema.string.optional({ format: 'url', tld: false }),
/*
|----------------------------------------------------------
| Variables for configuring the limiter package

View File

@ -30,12 +30,19 @@ router
/**
* Auth public. /refresh utilise le cookie httpOnly `rubis_refresh`
* posé par signup/login pour émettre une nouvelle AuthSession.
* /google/* SSO via Ally, callback pose aussi le refresh cookie.
*/
router
.group(() => {
router.post('signup', [controllers.NewAccount, 'store']).as('signup')
router.post('login', [controllers.AccessTokens, 'store']).as('login')
router.post('refresh', [controllers.Refresh, 'handle']).as('refresh')
router
.get('google/redirect', [controllers.AuthGoogle, 'redirect'])
.as('google.redirect')
router
.get('google/callback', [controllers.AuthGoogle, 'callback'])
.as('google.callback')
})
.prefix('auth')
.as('auth')

View File

@ -0,0 +1,77 @@
import { cn } from "@/lib/utils";
/**
* Bouton "Continuer avec Google".
*
* IMPORTANT c'est un `<a href>`, PAS un bouton fetch :
* OAuth nécessite un full-page redirect (le browser doit naviguer vers
* l'écran de consentement Google). Un fetch ne peut pas suivre les
* redirections cross-origin avec cookies.
*
* L'URL est relative nginx (rubis-web) proxy /api/* vers rubis-api,
* donc même origine pour le browser cookie refresh posé par la
* callback est lisible côté SPA.
*/
export function GoogleButton({
label = "Continuer avec Google",
className,
}: {
label?: string;
className?: string;
}) {
return (
<a
href="/api/v1/auth/google/redirect"
className={cn(
"inline-flex items-center justify-center gap-2.5 w-full",
"h-11 px-5 rounded-default border border-line bg-white",
"font-sans text-[14px] font-semibold text-ink",
"transition-[transform,background,box-shadow] duration-150",
"hover:bg-cream-2 hover:-translate-y-px hover:shadow-soft",
"active:translate-y-0",
"focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-rubis-glow",
className,
)}
>
<GoogleLogo aria-hidden="true" />
{label}
</a>
);
}
/** Logo Google officiel — 4 couleurs, taille fixe 18px. */
function GoogleLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg width="18" height="18" viewBox="0 0 18 18" {...props}>
<path
fill="#4285F4"
d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.875 2.684-6.615z"
/>
<path
fill="#34A853"
d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.836.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z"
/>
<path
fill="#FBBC05"
d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z"
/>
<path
fill="#EA4335"
d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z"
/>
</svg>
);
}
/** Séparateur "ou" entre le bouton SSO et le formulaire email/password. */
export function AuthDivider({ label = "ou" }: { label?: string }) {
return (
<div className="flex items-center gap-3 my-4">
<div className="h-px flex-1 bg-line" />
<span className="text-[11px] uppercase tracking-[0.1em] text-ink-3">
{label}
</span>
<div className="h-px flex-1 bg-line" />
</div>
);
}

View File

@ -0,0 +1,69 @@
import { useEffect, useRef } from "react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { z } from "zod";
import { toast } from "sonner";
import type { AuthSession } from "@rubis/shared";
import { api } from "@/lib/api";
import { authStore } from "@/lib/auth";
import { Gem } from "@/components/brand/Gem";
/**
* Callback Google côté SPA se charge :
* 1. d'appeler POST /api/v1/auth/refresh (le cookie httpOnly posé par
* /api/v1/auth/google/callback est auto-envoyé)
* 2. de stocker l'access token + user dans authStore
* 3. de naviguer vers `?next=...` (envoyé par le backend selon l'état
* d'onboarding : "/" pour user complet, "/onboarding/entreprise"
* pour un nouveau)
*
* En cas d'échec du refresh, on renvoie sur /login avec un toast.
*/
const searchSchema = z.object({
next: z.string().default("/"),
});
export const Route = createFileRoute("/auth/google/complete")({
validateSearch: searchSchema,
component: GoogleCompletePage,
});
function GoogleCompletePage() {
const { next } = Route.useSearch();
const navigate = useNavigate();
// Strict-mode protect : avoid double-firing the refresh in dev.
const triggered = useRef(false);
useEffect(() => {
if (triggered.current) return;
triggered.current = true;
(async () => {
try {
const session = await api.post<AuthSession>(
"/api/v1/auth/refresh",
undefined,
{ anonymous: true },
);
authStore.setSession(session.accessToken, session.user);
const firstName = session.user.fullName?.split(" ")[0];
toast.success(firstName ? `Bonjour ${firstName}.` : "Connecté.");
// Sécurité : si `next` n'est pas un chemin relatif, on renvoie sur "/".
const target = next.startsWith("/") && !next.startsWith("//") ? next : "/";
void navigate({ to: target });
} catch {
toast.error("Connexion Google échouée. Réessayez.");
void navigate({ to: "/login" });
}
})();
}, [next, navigate]);
// UI minimale pendant l'aller-retour réseau (max 1-2s en pratique).
return (
<main className="min-h-screen bg-cream flex items-center justify-center">
<div className="flex flex-col items-center gap-3 text-ink-2">
<Gem size={28} className="animate-pulse" />
<p className="text-[14px] font-medium">Connexion en cours</p>
</div>
</main>
);
}

View File

@ -1,3 +1,4 @@
import { useEffect } from "react";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { useForm } from "@tanstack/react-form";
import { useMutation } from "@tanstack/react-query";
@ -15,11 +16,20 @@ import { Field } from "@/components/ui/Field";
import { Eyebrow } from "@/components/ui/Eyebrow";
import { Brand } from "@/components/brand/Brand";
import { Gem } from "@/components/brand/Gem";
import { GoogleButton, AuthDivider } from "@/components/auth/GoogleButton";
const searchSchema = z.object({
redirect: z.string().optional(),
google: z.enum(["denied", "state_mismatch", "error", "no_email"]).optional(),
});
const GOOGLE_ERROR_MESSAGES: Record<string, string> = {
denied: "Connexion Google annulée.",
state_mismatch: "Session expirée, réessayez la connexion Google.",
error: "Connexion Google impossible. Réessayez dans un instant.",
no_email: "Votre compte Google n'a pas d'email associé.",
};
export const Route = createFileRoute("/login")({
validateSearch: searchSchema,
component: LoginPage,
@ -29,6 +39,13 @@ function LoginPage() {
const navigate = useNavigate();
const search = Route.useSearch();
// Toast d'erreur si on revient d'un échec Google SSO (?google=denied|...).
useEffect(() => {
if (search.google && GOOGLE_ERROR_MESSAGES[search.google]) {
toast.error(GOOGLE_ERROR_MESSAGES[search.google]!);
}
}, [search.google]);
const loginMutation = useMutation({
mutationFn: async (input: LoginInput) =>
api.post<AuthSession>("/api/v1/auth/login", input, { anonymous: true }),
@ -121,13 +138,18 @@ function LoginPage() {
</Link>
</p>
<div className="mt-7">
<GoogleButton />
<AuthDivider />
</div>
<form
noValidate
onSubmit={(e) => {
e.preventDefault();
void form.handleSubmit();
}}
className="mt-7 flex flex-col gap-5"
className="flex flex-col gap-5"
>
<form.Field name="email">
{(field) => (

View File

@ -19,6 +19,7 @@ import { Card } from "@/components/ui/Card";
import { Eyebrow } from "@/components/ui/Eyebrow";
import { Brand } from "@/components/brand/Brand";
import { Gem } from "@/components/brand/Gem";
import { GoogleButton, AuthDivider } from "@/components/auth/GoogleButton";
export const Route = createFileRoute("/signup")({
component: SignupPage,
@ -127,13 +128,18 @@ function SignupPage() {
</Link>
</p>
<div className="mt-7">
<GoogleButton label="S'inscrire avec Google" />
<AuthDivider />
</div>
<form
noValidate
onSubmit={(e) => {
e.preventDefault();
void form.handleSubmit();
}}
className="mt-7 flex flex-col gap-5"
className="flex flex-col gap-5"
>
<form.Field name="fullName">
{(field) => (

View File

@ -130,3 +130,8 @@ data:
ACCESS_TOKEN_TTL_MINUTES: '30'
REFRESH_TOKEN_TTL_DAYS: '30'
# Google SSO — GOOGLE_CLIENT_ID/SECRET sont dans rubis-app-secrets.
# Le callback URL doit matcher EXACTEMENT ce qui est configuré dans
# Google Cloud Console (OAuth Client → Authorized redirect URIs).
GOOGLE_CALLBACK_URL: 'https://app.rubis.arthurbarre.fr/api/v1/auth/google/callback'

39
pnpm-lock.yaml generated
View File

@ -32,6 +32,9 @@ importers:
apps/api:
dependencies:
'@adonisjs/ally':
specifier: ^6.3.0
version: 6.3.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/session@8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)))
'@adonisjs/auth':
specifier: ^10.1.0
version: 10.1.0(cb463dcab987fc365459355e33b96486)
@ -314,6 +317,25 @@ packages:
peerDependencies:
youch: ^4.1.0-beta.11 || ^4.1.0
'@adonisjs/ally@6.3.0':
resolution: {integrity: sha512-yxw9mpQHexRwKx3qpR651yn/a+gYYHne0Cc6fjMxVQ8rOaBZS2e1025wmuhHg7gProLCv8lpSbqF7I9Hk7qWAQ==}
engines: {node: '>=20.6.0'}
peerDependencies:
'@adonisjs/assembler': ^8.0.0
'@adonisjs/core': ^7.0.0-next.8 || ^7.0.0
'@adonisjs/i18n': ^3.0.0
'@adonisjs/inertia': ^4.2.0
'@adonisjs/session': ^8.0.0
peerDependenciesMeta:
'@adonisjs/assembler':
optional: true
'@adonisjs/i18n':
optional: true
'@adonisjs/inertia':
optional: true
'@adonisjs/session':
optional: true
'@adonisjs/application@9.0.0':
resolution: {integrity: sha512-iQpq/JRJsnrqOMHfu72CYjmlkH5FwT28DhUKEOjktccmFh8OLdVZ2Sieb8b2/qNv4c+w8Yo7keOGEzOYUrU+kA==}
engines: {node: '>=24.0.0'}
@ -1539,6 +1561,10 @@ packages:
'@poppinss/multiparty@3.0.0':
resolution: {integrity: sha512-z9jchUzsv7E+7sa4tWHb0+95Byx7w0ydlPGxg3nzyb7h3QlRdeW8/QkU9SexUY4lsT12do93AfNBAhSuOoVqjA==}
'@poppinss/oauth-client@7.2.0':
resolution: {integrity: sha512-LUCd/fIm2oOeTltnZRSJIxZL9uy9+qHpgtNtxIO+wp8E/1Dd7ogbqIWUFYMufCxTekOoYyQlWOcOQ9rmUw5D5g==}
engines: {node: '>=24.0.0'}
'@poppinss/object-builder@1.1.0':
resolution: {integrity: sha512-FOrOq52l7u8goR5yncX14+k+Ewi5djnrt1JwXeS/FvnwAPOiveFhiczCDuvXdssAwamtrV2hp5Rw9v+n2T7hQg==}
engines: {node: '>=20.6.0'}
@ -5754,6 +5780,14 @@ snapshots:
yargs-parser: 22.0.0
youch: 4.1.1
'@adonisjs/ally@6.3.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/session@8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)))':
dependencies:
'@adonisjs/core': 7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1)
'@poppinss/oauth-client': 7.2.0
optionalDependencies:
'@adonisjs/assembler': 8.4.0(typescript@6.0.3)
'@adonisjs/session': 8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0))
'@adonisjs/application@9.0.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0)':
dependencies:
'@adonisjs/config': 6.1.0
@ -7204,6 +7238,11 @@ snapshots:
dependencies:
http-errors: 2.0.1
'@poppinss/oauth-client@7.2.0':
dependencies:
'@poppinss/exception': 1.2.3
ky: 1.14.3
'@poppinss/object-builder@1.1.0': {}
'@poppinss/prompts@3.1.6':