diff --git a/houdini.config.js b/houdini.config.js
index e86aa667dd22a62cc00862421e320a05ec723991..29e2b4dea248c43302e656de962c38c4197097c6 100644
--- a/houdini.config.js
+++ b/houdini.config.js
@@ -17,24 +17,9 @@ export default {
 		}
 	},
 	scalars: {
-		Date: {
-			type: 'string'
-		},
-		DateTime: {
-			type: 'string'
-		},
 		Url: {
 			type: 'string'
 		},
-		SmolStr: {
-			type: 'string'
-		},
-		UUID: {
-			type: 'string'
-		},
-		Identifier: {
-			type: 'string'
-		},
 		Base64: {
 			type: 'string'
 		}
diff --git a/src/lib/data.ts b/src/lib/data.ts
index a9cf2921b6209adffe5ca7c0913a27cc48bcd7d3..e482317c77006c9769fd8196a79fd8dfa4a02cc5 100644
--- a/src/lib/data.ts
+++ b/src/lib/data.ts
@@ -1,32 +1,50 @@
-// import { GetPromotionStore, UserDetailsStore } from '$houdini';
-// import { pageIterator } from '$lib/graphql/query';
+import { GetPromotionStore } from '$houdini';
+import { pageIterator } from '$lib/graphql/query';
+import type { RequestEvent } from '@sveltejs/kit';
 
-// type Promotion = unknown[];
+type UserId = string;
+type PromoCache = {
+	lastUpdateMs: number;
+	promo: Set<UserId>;
+};
 
-// const cache = new Map<number, Promotion>();
+const cache = new Map<number, PromoCache>();
+// 1 hour
+const CACHE_DURATION_MS = 1000 * 60 * 60;
 
-// function fetchPromotion(promotion: number) {
-// 	return Array.fromAsync(pageIterator(event, GetPromotionStore, { promotion }));
-// }
+async function fetchPromotion(promotion: number, event: RequestEvent): Promise<PromoCache> {
+	const array = await Array.fromAsync(pageIterator(GetPromotionStore, { promotion }, event));
+	return {
+		lastUpdateMs: new Date().getTime(),
+		promo: new Set(array.map((node) => node.id))
+	};
+}
 
-// export async function getPromotion(promo: number): Promise<Promotion> {
-// 	if (cache.has(promo)) {
-// 		return cache.get(promo)!;
-// 	}
-// 	const promotion = await fetchPromotion(promo);
-// 	cache.set(promo, promotion);
-// 	return promotion;
-// }
+export async function getPromotion(promo: number, event: RequestEvent): Promise<Set<UserId>> {
+	if (cache.has(promo)) {
+		const data = cache.get(promo)!;
+		if (new Date().getTime() - data.lastUpdateMs < CACHE_DURATION_MS) return data.promo;
+	}
+	const freshData = await fetchPromotion(promo, event);
+	cache.set(promo, freshData);
+	return freshData.promo;
+}
 
-// export async function* promotionIterator(
-// 	min: number,
-// 	max: number
-// ): AsyncGenerator<Promotion, void, unknown> {
-// 	for (let i = min; i <= max; i++) {
-// 		yield getPromotion(i);
-// 	}
-// }
+export async function* promotionIterator(
+	min: number,
+	max: number,
+	event: RequestEvent
+): AsyncGenerator<UserId, void, undefined> {
+	for (let i = min; i <= max; i++) {
+		const promo = await getPromotion(i, event);
+		yield* promo;
+	}
+}
 
-// export function getPromotionRange(min: number, max: number): Promise<Promotion> {
-// 	return Array.fromAsync(promotionIterator(min, max));
-// }
+export function getPromotionRange(
+	min: number,
+	max: number,
+	event: RequestEvent
+): Promise<Set<UserId>> {
+	return Array.fromAsync(promotionIterator(min, max, event)).then((array) => new Set(array));
+}
diff --git a/src/lib/graphql/query.ts b/src/lib/graphql/query.ts
index 7619ffb411e3d7eed205e946c1a333f07396aa57..b6dac16843f2eef2c9cdca405962a1a4ed40f726 100644
--- a/src/lib/graphql/query.ts
+++ b/src/lib/graphql/query.ts
@@ -22,9 +22,9 @@ export async function* pageIterator<
 	Data extends Page<GraphQLValue>,
 	Input extends PageArgs | GraphQLVariables
 >(
-	event: RequestEvent,
 	queryStore: new () => QueryStore<Data, Input>,
 	variables: Omit<Input, 'first' | 'after'>,
+	event: RequestEvent,
 	pageSize = 100
 ): AsyncGenerator<NodeType<Data>, void, undefined> {
 	const store = new queryStore();
diff --git a/src/lib/graphql/schema.gql b/src/lib/graphql/schema.gql
index 2bfded716899b2699ed44ca056eab67a07628e29..b601941fffcf88ca5a600728a1b603f94810faca 100644
--- a/src/lib/graphql/schema.gql
+++ b/src/lib/graphql/schema.gql
@@ -20,167 +20,137 @@ directive @oneOf on INPUT_OBJECT
 union Account = Group | User
 
 input AccountId {
-	id: SmolStr
-	uuid: UUID
+  id: SmolStr
+  uuid: UUID
 }
 
 type Address {
-	"""
-	# **ADMIN ONLY**
-	## UUID interne de l'adresse
-
-	Par exemple, `72c612f8-1d07-432e-8f71-60584be51f40`
-	"""
-	accountUuid: UUID!
-
-	"""
-	## Pays de l'adresse
-	"""
-	country: String!
-
-	"""
-	## Adresse postale complète de l'adresse
-	Formatée pour être affichée ou utilisée sur une étiquette d'envoi.
-	Ce champ PEUT contenir plusieurs lignes, séparées par des retours à la ligne.
-	Les nouvelles lignes peuvent être représentées soit par une paire retour
-	chariot/saut de ligne ("\r\n"), soit par un seul caractère de saut de ligne ("\n").
-	"""
-	formatted: String!
-
-	"""
-	## Ville ou localité de l'adresse
-	"""
-	locality: String!
-
-	"""
-	## Nom de l'adresse
-	Le nom usuel de l'adresse s'il existe. Limite de **64** caractères.
-
-	Par exemple, `Pyroloc` ou `Résidence "Les Estudines du Parc"`.
-	"""
-	name: String
-
-	"""
-	## Code postal de l'adresse
-	"""
-	postalCode: String!
-
-	"""
-	## Région de l'adresse
-	Province, de la préfecture ou région.
-	"""
-	region: String
-
-	"""
-	## Rue de l'adresse
-	Composante complète de l'adresse de la rue, qui PEUT inclure le numéro de la maison,
-	le nom de la rue, la boîte postale et des informations sur plusieurs lignes concernant
-	l'adresse de la rue. Ce champ PEUT contenir plusieurs lignes, séparées par des retours
-	à la ligne. Les nouvelles lignes peuvent être représentées soit par une paire retour
-	chariot/saut de ligne ("\r\n"), soit par un seul caractère de saut de ligne ("\n").
-	"""
-	streetAddress: String!
-}
-
-"""
-Base64 [RFC4648](https://www.rfc-editor.org/rfc/rfc4648)
-"""
+  """
+  # **ADMIN ONLY**
+  ## UUID interne de l'adresse
+  
+  Par exemple, `72c612f8-1d07-432e-8f71-60584be51f40`
+  """
+  accountUuid: UUID!
+
+  """## Pays de l'adresse"""
+  country: String!
+
+  """
+  ## Adresse postale complète de l'adresse
+  Formatée pour être affichée ou utilisée sur une étiquette d'envoi.
+  Ce champ PEUT contenir plusieurs lignes, séparées par des retours à la ligne.
+  Les nouvelles lignes peuvent être représentées soit par une paire retour
+  chariot/saut de ligne ("\r\n"), soit par un seul caractère de saut de ligne ("\n").
+  """
+  formatted: String!
+
+  """## Ville ou localité de l'adresse"""
+  locality: String!
+
+  """
+  ## Nom de l'adresse
+  Le nom usuel de l'adresse s'il existe. Limite de **64** caractères.
+  
+  Par exemple, `Pyroloc` ou `Résidence "Les Estudines du Parc"`.
+  """
+  name: String
+
+  """## Code postal de l'adresse"""
+  postalCode: String!
+
+  """
+  ## Région de l'adresse
+  Province, de la préfecture ou région.
+  """
+  region: String
+
+  """
+  ## Rue de l'adresse
+  Composante complète de l'adresse de la rue, qui PEUT inclure le numéro de la maison,
+  le nom de la rue, la boîte postale et des informations sur plusieurs lignes concernant
+  l'adresse de la rue. Ce champ PEUT contenir plusieurs lignes, séparées par des retours
+  à la ligne. Les nouvelles lignes peuvent être représentées soit par une paire retour
+  chariot/saut de ligne ("\r\n"), soit par un seul caractère de saut de ligne ("\n").
+  """
+  streetAddress: String!
+}
+
+"""Base64 [RFC4648](https://www.rfc-editor.org/rfc/rfc4648)"""
 scalar Base64
 
 input BooleanFilter {
-	eq: Boolean!
+  eq: Boolean!
 }
 
 type BuildInfo {
-	ref: String
-	sha: String
+  ref: String
+  sha: String
 }
 
-"""
-Rôles dans un group
-"""
+"""Rôles dans un group"""
 enum ClaimAccess {
-	"""
-	Création
-	"""
-	CREATE
+  """Création"""
+  CREATE
 
-	"""
-	Suppression
-	"""
-	DELETE
+  """Suppression"""
+  DELETE
 
-	"""
-	Aucun
-	"""
-	NONE
+  """Aucun"""
+  NONE
 
-	"""
-	Lecture
-	"""
-	READ
+  """Lecture"""
+  READ
 
-	"""
-	Écriture
-	"""
-	WRITE
+  """Écriture"""
+  WRITE
 }
 
 input CreateGroup {
-	id: Identifier!
-	name: String!
-	type: GroupType
+  id: Identifier!
+  name: String!
+  type: GroupType
 }
 
 input CreateToken {
-	allowedIps: [String!]
-	claims: [NewTokenClaim!]
-	description: String!
-	globalAccess: ClaimAccess
-	id: String!
+  allowedIps: [String!]
+  claims: [NewTokenClaim!]
+  description: String!
+  globalAccess: ClaimAccess
+  id: String!
 }
 
 input CreateTraining {
-	fullName: String
-	id: Identifier!
-	name: String!
+  fullName: String
+  id: Identifier!
+  name: String!
 }
 
 input CreateUnixAccount {
-	vhost: String
+  vhost: String
 }
 
 input CreateUser {
-	familyName: String!
-	givenName: String!
-	password: Password
-	promotion: Int!
-	trainingId: Identifier!
+  familyName: String!
+  givenName: String!
+  password: Password
+  promotion: Int!
+  trainingId: Identifier!
 }
 
-"""
-Rôles dans un groupe
-"""
+"""Rôles dans un groupe"""
 enum CurrentGroupRole {
-	"""
-	Administrateur·trice
-	"""
-	ADMIN
+  """Administrateur·trice"""
+  ADMIN
 
-	"""
-	Membre du bureau
-	"""
-	BOARD_MEMBER
+  """Membre du bureau"""
+  BOARD_MEMBER
 
-	"""
-	Membre ordinaire
-	"""
-	MEMBER
+  """Membre ordinaire"""
+  MEMBER
 
-	"""
-	Président·e
-	"""
-	OWNER
+  """Président·e"""
+  OWNER
 }
 
 """
@@ -205,332 +175,292 @@ format, but it is always normalized to the UTC (Z) offset, e.g.
 scalar DateTime
 
 input DateTimeFilter {
-	eq: [LocalDateTime!]
-	gt: LocalDateTime
-	lt: LocalDateTime
+  eq: [LocalDateTime!]
+  gt: LocalDateTime
+  lt: LocalDateTime
 }
 
 enum FromNull {
-	BOOLEAN
-	LIST
-	NUMBER
-	OBJECT
-	STRING
+  BOOLEAN
+  LIST
+  NUMBER
+  OBJECT
+  STRING
 }
 
-"""
-Genre d'une personne
-"""
+"""Genre d'une personne"""
 enum Gender {
-	"""
-	Féminin
-	"""
-	FEMALE
+  """Féminin"""
+  FEMALE
 
-	"""
-	Masculin
-	"""
-	MALE
+  """Masculin"""
+  MALE
 
-	"""
-	Non binaire
-	"""
-	NON_BINARY
+  """Non binaire"""
+  NON_BINARY
 
-	"""
-	Inconnu
-	"""
-	UNKNOWN
+  """Inconnu"""
+  UNKNOWN
 }
 
 input GenderFilter {
-	isIn: [Gender!]!
+  isIn: [Gender!]!
 }
 
 type GlobalValues {
-	evenSemester: Boolean!
-	integration: Boolean!
-	schoolYear: Int!
+  evenSemester: Boolean!
+  integration: Boolean!
+  schoolYear: Int!
 }
 
 type Group {
-	"""
-	## Groupe actif
-	**VRAI** si le group est encore actif.
-	Se traduit par une déclaration lors d'une réunion publique de l'AEIIE s'il
-	s'agit d'un group BdE. Les listes candidates à l'élection BdE sont actives le
-	temps de la campagne.
-	"""
-	active: Boolean!
-
-	"""
-	## URL de la photo de profil du groupe
-	Cette URL fait référence à un fichier image (par exemple, un fichier image PNG,
-	JPEG ou GIF), plutôt qu'à une page Web contenant une image.
-
-	Par exemple, `https://api.iiens.net/picture/acier2020` ou `https://api.iiens.net/picture/dupontj2042`
-	"""
-	backgroundImage: Url!
-
-	"""
-	Hash de l'image d'arrière plan du groupe
-	"""
-	backgroundImageThumbnailHash: Base64!
-
-	"""
-	## Date de la création du compte du group
-	Sa valeur est un string JSON [RFC3339] représentant le temps  entre 1970-01-01T0:0:0Z,
-	mesuré en UTC, et la date/heure.
-	"""
-	createdAt: DateTime!
-
-	"""
-	## Description du group
-	Une description plus complète que `short_description`, sans limite de caractères.
-	"""
-	description: String
-
-	"""
-	## Adresse électronique préférée de l'utilisateur.
-	Sa valeur DOIT être conforme à la syntaxe addr-spec de la RFC 5322 [RFC5322].
-
-	Par exemple, `arise@iiens.net` ou `bde@iiens.net`.
-	"""
-	email: String!
-
-	"""
-	# **ADMIN ONLY**
-	## Compte caché
-	Le compte du group sera invisible de toutes les requêtes, sauf admin si le filtre
-	de la requête le demande.
-	"""
-	hidden: Boolean!
-
-	"""
-	## Identifiant du group
-	Le nom du group normalisé (sans accents, caractères spéciaux ni espaces).
-	Ne peut contenir que des caractères ASCII et éventuellement des tirets pour
-	remplacer les espaces.
-
-	Par exemple, `arise` ou `aeiie`.
-	"""
-	id: Identifier!
-
-	"""
-	## Nom du group
-	Le nom usuel du group, présenté sous forme d'acronyme ou sous forme longue.
-
-	Par exemple, `ARISE` ou `BdE`.
-	"""
-	name: String!
-
-	"""
-	## URL de la photo de profil du groupe
-	Cette URL fait référence à un fichier image (par exemple, un fichier image PNG,
-	JPEG ou GIF), plutôt qu'à une page Web contenant une image.
-
-	Par exemple, `https://api.iiens.net/picture/acier2020` ou `https://api.iiens.net/picture/dupontj2042`
-	"""
-	picture: Url!
-
-	"""
-	Hash de la photo de profil du groupe
-	"""
-	pictureThumbnailHash: Base64!
-
-	"""
-	## URL de la page de profil du group
-	Page de profil sur [www.iiens.net].
-
-	Par exemple, `https://www.iiens.net/eleve/acier2020` ou `https://www.iiens.net/eleve/dupontj2042`
-	"""
-	profile: Url!
-
-	"""
-	## Description courte du group
-	64 caractères maximum, de quoi présenter le group succinctement.
-	"""
-	shortDescription: String
-
-	"""
-	## Type de groupe
-	Club, Association 1901, etc.
-	"""
-	type: GroupType!
-	unixAccount: UnixAccount
-
-	"""
-	## Date de la dernière mise à jour des informations relatives au group
-	Sa valeur est un string JSON [RFC3339] représentant le temps  entre 1970-01-01T0:0:0Z,
-	mesuré en UTC, et la date/heure.
-	"""
-	updatedAt: DateTime!
-
-	"""
-	# **ADMIN ONLY**
-	## UUID interne du group
-
-	Par exemple, `72c612f8-1d07-432e-8f71-60584be51f40`
-	"""
-	uuid: UUID!
-
-	"""
-	## URL du site perso du groupe
-	"""
-	website: Url
+  """
+  ## Groupe actif
+  **VRAI** si le group est encore actif.
+  Se traduit par une déclaration lors d'une réunion publique de l'AEIIE s'il
+  s'agit d'un group BdE. Les listes candidates à l'élection BdE sont actives le
+  temps de la campagne.
+  """
+  active: Boolean!
+
+  """
+  ## URL de la photo de profil du groupe
+  Cette URL fait référence à un fichier image (par exemple, un fichier image PNG,
+  JPEG ou GIF), plutôt qu'à une page Web contenant une image.
+  
+  Par exemple, `https://api.iiens.net/picture/acier2020` ou `https://api.iiens.net/picture/dupontj2042`
+  """
+  backgroundImage: Url!
+
+  """Hash de l'image d'arrière plan du groupe"""
+  backgroundImageThumbnailHash: Base64!
+
+  """
+  ## Date de la création du compte du group
+  Sa valeur est un string JSON [RFC3339] représentant le temps  entre 1970-01-01T0:0:0Z,
+  mesuré en UTC, et la date/heure.
+  """
+  createdAt: DateTime!
+
+  """
+  ## Description du group
+  Une description plus complète que `short_description`, sans limite de caractères.
+  """
+  description: String
+
+  """
+  ## Adresse électronique préférée de l'utilisateur.
+  Sa valeur DOIT être conforme à la syntaxe addr-spec de la RFC 5322 [RFC5322].
+  
+  Par exemple, `arise@iiens.net` ou `bde@iiens.net`.
+  """
+  email: String!
+
+  """
+  # **ADMIN ONLY**
+  ## Compte caché
+  Le compte du group sera invisible de toutes les requêtes, sauf admin si le filtre
+  de la requête le demande.
+  """
+  hidden: Boolean!
+
+  """
+  ## Identifiant du group
+  Le nom du group normalisé (sans accents, caractères spéciaux ni espaces).
+  Ne peut contenir que des caractères ASCII et éventuellement des tirets pour
+  remplacer les espaces.
+  
+  Par exemple, `arise` ou `aeiie`.
+  """
+  id: Identifier!
+
+  """
+  ## Nom du group
+  Le nom usuel du group, présenté sous forme d'acronyme ou sous forme longue.
+  
+  Par exemple, `ARISE` ou `BdE`.
+  """
+  name: String!
+
+  """
+  ## URL de la photo de profil du groupe
+  Cette URL fait référence à un fichier image (par exemple, un fichier image PNG,
+  JPEG ou GIF), plutôt qu'à une page Web contenant une image.
+  
+  Par exemple, `https://api.iiens.net/picture/acier2020` ou `https://api.iiens.net/picture/dupontj2042`
+  """
+  picture: Url!
+
+  """Hash de la photo de profil du groupe"""
+  pictureThumbnailHash: Base64!
+
+  """
+  ## URL de la page de profil du group
+  Page de profil sur [www.iiens.net].
+  
+  Par exemple, `https://www.iiens.net/eleve/acier2020` ou `https://www.iiens.net/eleve/dupontj2042`
+  """
+  profile: Url!
+
+  """
+  ## Description courte du group
+  64 caractères maximum, de quoi présenter le group succinctement.
+  """
+  shortDescription: String
+
+  """
+  ## Type de groupe
+  Club, Association 1901, etc.
+  """
+  type: GroupType!
+  unixAccount: UnixAccount
+
+  """
+  ## Date de la dernière mise à jour des informations relatives au group
+  Sa valeur est un string JSON [RFC3339] représentant le temps  entre 1970-01-01T0:0:0Z,
+  mesuré en UTC, et la date/heure.
+  """
+  updatedAt: DateTime!
+
+  """
+  # **ADMIN ONLY**
+  ## UUID interne du group
+  
+  Par exemple, `72c612f8-1d07-432e-8f71-60584be51f40`
+  """
+  uuid: UUID!
+
+  """## URL du site perso du groupe"""
+  website: Url
 }
 
 type GroupConnection {
-	"""
-	A list of edges.
-	"""
-	edges: [GroupEdge!]!
+  """A list of edges."""
+  edges: [GroupEdge!]!
 
-	"""
-	A list of nodes.
-	"""
-	nodes: [Group!]!
+  """A list of nodes."""
+  nodes: [Group!]!
 
-	"""
-	Information to aid in pagination.
-	"""
-	pageInfo: PageInfo!
-	remainingCount: Int!
+  """Information to aid in pagination."""
+  pageInfo: PageInfo!
+  remainingCount: Int!
 }
 
-"""
-An edge in a connection.
-"""
+"""An edge in a connection."""
 type GroupEdge {
-	"""
-	A cursor for use in pagination
-	"""
-	cursor: String!
+  """A cursor for use in pagination"""
+  cursor: String!
 
-	"""
-	The item at the end of the edge
-	"""
-	node: Group!
-	remainingCount: Int!
+  """The item at the end of the edge"""
+  node: Group!
+  remainingCount: Int!
 }
 
 input GroupFilter {
-	accountUuid: UuidFilter
-	active: BooleanFilter
-	createdAt: DateTimeFilter
-	description: NullableTextFilter
-	email: TextFilter
-	emailVerified: BooleanFilter
-	hidden: BooleanFilter
-	id: TextFilter
-	name: TextFilter
-	or: [GroupFilter!]
-	shortDescription: NullableTextFilter
-	type: GroupTypeFilter
-	unixAccount: UnixAccountFilter
-	updatedAt: DateTimeFilter
-	uuid: UuidFilter
-	website: NullableTextFilter
+  accountUuid: UuidFilter
+  active: BooleanFilter
+  createdAt: DateTimeFilter
+  description: NullableTextFilter
+  email: TextFilter
+  emailVerified: BooleanFilter
+  hidden: BooleanFilter
+  id: TextFilter
+  name: TextFilter
+  or: [GroupFilter!]
+  shortDescription: NullableTextFilter
+  type: GroupTypeFilter
+  unixAccount: UnixAccountFilter
+  updatedAt: DateTimeFilter
+  uuid: UuidFilter
+  website: NullableTextFilter
 }
 
 input GroupId {
-	email: String
-	id: SmolStr
-	uuid: UUID
+  email: String
+  id: SmolStr
+  uuid: UUID
 }
 
 type GroupOfMember {
-	group: Group!
-	isAdmin: Boolean!
-	isBoardMember: Boolean!
-	isOwner: Boolean!
-	role: CurrentGroupRole!
-	since: Date!
+  group: Group!
+  isAdmin: Boolean!
+  isBoardMember: Boolean!
+  isOwner: Boolean!
+  role: CurrentGroupRole!
+  since: Date!
 }
 
 input GroupOfMemberFilter {
-	role: CurrentGroupRole
-	since: NeverDateFilter
-	strictRole: Boolean! = false
+  role: CurrentGroupRole
+  since: NeverDateFilter
+  strictRole: Boolean! = false
 }
 
-"""
-Rôles dans un groupe
-"""
+"""Rôles dans un groupe"""
 enum GroupRole {
-	"""
-	Administrateur·trice
-	"""
-	ADMIN
+  """Administrateur·trice"""
+  ADMIN
 
-	"""
-	Membre du bureau
-	"""
-	BOARD_MEMBER
+  """Membre du bureau"""
+  BOARD_MEMBER
 
-	"""
-	A quitté le groupe
-	"""
-	GONE
+  """A quitté le groupe"""
+  GONE
 
-	"""
-	Membre ordinaire
-	"""
-	MEMBER
+  """Membre ordinaire"""
+  MEMBER
 
-	"""
-	Président·e
-	"""
-	OWNER
+  """Président·e"""
+  OWNER
 }
 
 enum GroupType {
-	ASSOCIATION
-	CLUB
-	COMMISSION
-	HOME
-	LIST
-	OTHER
-	UNKNOWN
+  ASSOCIATION
+  CLUB
+  COMMISSION
+  HOME
+  LIST
+  OTHER
+  UNKNOWN
 }
 
 input GroupTypeFilter {
-	isIn: [GroupType!]!
+  isIn: [GroupType!]!
 }
 
 type Health {
-	db: String!
+  db: String!
 }
 
 type HistoricalGroupOfMember {
-	group: Group!
-	role: GroupRole!
-	since: Date!
+  group: Group!
+  role: GroupRole!
+  since: Date!
 }
 
 input HistoricalGroupOfMemberFilter {
-	role: GroupRole
-	since: NeverDateFilter
-	strictRole: Boolean! = false
+  role: GroupRole
+  since: NeverDateFilter
+  strictRole: Boolean! = false
 }
 
 type HistoricalMemberOfGroup {
-	role: GroupRole!
-	since: Date!
-	user: User!
+  role: GroupRole!
+  since: Date!
+  user: User!
 }
 
 scalar Identifier
 
 input Int16Filter {
-	eq: [Int!]
-	gt: Int
-	lt: Int
+  eq: [Int!]
+  gt: Int
+  lt: Int
 }
 
 input LevenshteinFilter {
-	threshold: Int
-	value: String!
+  threshold: Int
+  value: String!
 }
 
 """
@@ -542,161 +472,149 @@ subseconds. E.g. "2022-01-12T07:30:19.12345".
 scalar LocalDateTime
 
 type Mutation {
-	addClaims(claims: [NewTokenClaim!]!, token: TokenId!): Token!
-	addGroupMemberRole(group: GroupId!, role: GroupRole!, user: UserId!): HistoricalMemberOfGroup!
-	copyClaims(dstToken: TokenId!, srcToken: TokenId!): Token!
-
-	"""
-	Création d'un groupe
-	"""
-	createGroup(group: CreateGroup!): Group!
-	createGroupUnixAccount(account: CreateUnixAccount!, group: GroupId!): UnixAccount!
-	createToken(token: CreateToken!): PlainToken!
-	createTraining(training: CreateTraining!): Training!
-
-	"""
-	Création d'un utilisateur
-	### Erreurs possibles
-	- `NOT_FOUND`: La formation n'existe pas.
-	- `ID_GENERATION_FAILURE`: L'identifiant de l'utilisateur n'a pas pu être généré.
-	"""
-	createUser(user: CreateUser!): User!
-	createUserUnixAccount(account: CreateUnixAccount!, user: UserId!): UnixAccount!
-	deleteClaims(claims: [String!], token: TokenId!): Token!
-
-	"""
-	Suppression d'un groupe
-	### Erreurs possibles
-	- `NOT_FOUND`: Le groupe n'existe pas.
-	"""
-	deleteGroup(group: GroupId!): Boolean!
-	deleteToken(token: TokenId!): Int!
-	deleteTraining(training: TrainingId!): Int!
-
-	"""
-	Suppression d'un utilisateur
-	### Erreurs possibles
-	- `NOT_FOUND`: L'utilisateur n'existe pas.
-	"""
-	deleteUser(user: UserId!): Int!
-	endOfIntegration: Boolean!
-	evenSemester: Boolean!
-	newYear: Int!
-	regenerateToken(token: TokenId!): PlainToken!
-	status: Int!
-
-	"""
-	Mise à jour d'un groupe
-	### Erreurs possibles
-	- `NOT_FOUND`: Le groupe n'existe pas.
-	"""
-	updateGroup(group: GroupId!, update: UpdateGroup!): Group!
-	updateToken(token: TokenId!, update: UpdateToken!): Token!
-	updateTraining(training: TrainingId!, update: UpdateTraining!): Training!
-	updateUnixAccount(unixAccount: UnixAccountId!, update: UpdateUnixAccount!): UnixAccount!
-
-	"""
-	Mise à jour d'un utilisateur
-	### Erreurs possibles
-	- `NOT_FOUND`: L'utilisateur ou la formation n'existe pas.
-	"""
-	updateUser(update: UpdateUser!, user: UserId!): User!
+  addClaims(claims: [NewTokenClaim!]!, token: TokenId!): Token!
+  addGroupMemberRole(group: GroupId!, role: GroupRole!, user: UserId!): HistoricalMemberOfGroup!
+  copyClaims(dstToken: TokenId!, srcToken: TokenId!): Token!
+
+  """Création d'un groupe"""
+  createGroup(group: CreateGroup!): Group!
+  createGroupUnixAccount(account: CreateUnixAccount!, group: GroupId!): UnixAccount!
+  createToken(token: CreateToken!): PlainToken!
+  createTraining(training: CreateTraining!): Training!
+
+  """
+  Création d'un utilisateur
+  ### Erreurs possibles
+  - `NOT_FOUND`: La formation n'existe pas.
+  - `ID_GENERATION_FAILURE`: L'identifiant de l'utilisateur n'a pas pu être généré.
+  """
+  createUser(user: CreateUser!): User!
+  createUserUnixAccount(account: CreateUnixAccount!, user: UserId!): UnixAccount!
+  deleteClaims(claims: [String!], token: TokenId!): Token!
+
+  """
+  Suppression d'un groupe
+  ### Erreurs possibles
+  - `NOT_FOUND`: Le groupe n'existe pas.
+  """
+  deleteGroup(group: GroupId!): Boolean!
+  deleteToken(token: TokenId!): Int!
+  deleteTraining(training: TrainingId!): Int!
+
+  """
+  Suppression d'un utilisateur
+  ### Erreurs possibles
+  - `NOT_FOUND`: L'utilisateur n'existe pas.
+  """
+  deleteUser(user: UserId!): Int!
+  endOfIntegration: Boolean!
+  evenSemester: Boolean!
+  newYear: Int!
+  regenerateToken(token: TokenId!): PlainToken!
+  status: Int!
+
+  """
+  Mise à jour d'un groupe
+  ### Erreurs possibles
+  - `NOT_FOUND`: Le groupe n'existe pas.
+  """
+  updateGroup(group: GroupId!, update: UpdateGroup!): Group!
+  updateToken(token: TokenId!, update: UpdateToken!): Token!
+  updateTraining(training: TrainingId!, update: UpdateTraining!): Training!
+  updateUnixAccount(unixAccount: UnixAccountId!, update: UpdateUnixAccount!): UnixAccount!
+
+  """
+  Mise à jour d'un utilisateur
+  ### Erreurs possibles
+  - `NOT_FOUND`: L'utilisateur ou la formation n'existe pas.
+  """
+  updateUser(update: UpdateUser!, user: UserId!): User!
 }
 
 input NeverDateFilter {
-	eq: [Date!]
-	gt: Date
-	lt: Date
+  eq: [Date!]
+  gt: Date
+  lt: Date
 }
 
 input NewTokenClaim {
-	access: ClaimAccess!
-	claim: String!
+  access: ClaimAccess!
+  claim: String!
 }
 
 input NullableDateFilter {
-	day: Int
-	eq: [Date!]
-	gt: Date
-	lt: Date
-	month: Int
-	null: Boolean
+  day: Int
+  eq: [Date!]
+  gt: Date
+  lt: Date
+  month: Int
+  null: Boolean
 }
 
 input NullableDateTimeFilter {
-	eq: [LocalDateTime!]
-	gt: LocalDateTime
-	lt: LocalDateTime
-	null: Boolean
+  eq: [LocalDateTime!]
+  gt: LocalDateTime
+  lt: LocalDateTime
+  null: Boolean
 }
 
 input NullableFilter {
-	null: Boolean
+  null: Boolean
 }
 
 input NullableTextFilter {
-	levenshtein: [LevenshteinFilter!]
-	like: [String!]
-	notLike: [String!]
-	null: Boolean
-	similar: [SimilarFilter!]
+  levenshtein: [LevenshteinFilter!]
+  like: [String!]
+  notLike: [String!]
+  null: Boolean
+  similar: [SimilarFilter!]
 }
 
 enum OrderByGroup {
-	CREATED_AT
-	ID
-	NAME
-	UPDATED_AT
-	UUID
+  CREATED_AT
+  ID
+  NAME
+  UPDATED_AT
+  UUID
 }
 
 enum OrderByUser {
-	BIRTH_DATE
-	CREATED_AT
-	FAMILY_NAME
-	GIVEN_NAME
-	ID
-	LAST_USED_AT
-	NICKNAME
-	PROMOTION
-	UPDATED_AT
-	UUID
-	YEAR
-}
-
-"""
-Information about pagination in a connection
-"""
+  BIRTH_DATE
+  CREATED_AT
+  FAMILY_NAME
+  GIVEN_NAME
+  ID
+  LAST_USED_AT
+  NICKNAME
+  PROMOTION
+  UPDATED_AT
+  UUID
+  YEAR
+}
+
+"""Information about pagination in a connection"""
 type PageInfo {
-	"""
-	When paginating forwards, the cursor to continue.
-	"""
-	endCursor: String
+  """When paginating forwards, the cursor to continue."""
+  endCursor: String
 
-	"""
-	When paginating forwards, are there more items?
-	"""
-	hasNextPage: Boolean!
+  """When paginating forwards, are there more items?"""
+  hasNextPage: Boolean!
 
-	"""
-	When paginating backwards, are there more items?
-	"""
-	hasPreviousPage: Boolean!
+  """When paginating backwards, are there more items?"""
+  hasPreviousPage: Boolean!
 
-	"""
-	When paginating backwards, the cursor to continue.
-	"""
-	startCursor: String
+  """When paginating backwards, the cursor to continue."""
+  startCursor: String
 }
 
 input Password {
-	hash: String
-	plain: String
+  hash: String
+  plain: String
 }
 
 type PlainToken {
-	base64: String!
-	raw: String!
+  base64: String!
+  raw: String!
 }
 
 """
@@ -704,105 +622,91 @@ Ensemble des requêtes possibles pour l'API Arise.
 Certaines peuvent être cachées, en fonction des permissions courantes.
 """
 type Query {
-	account(account: AccountId!): Account
-	buildInfo: BuildInfo!
-	currentToken: Token!
-	globalsValues: GlobalValues!
-	group(group: GroupId!): Group
-	groups(
-		after: String
-		before: String
-		filter: GroupFilter
-		first: Int
-		last: Int
-		orderBy: [OrderByGroup!]! = []
-	): GroupConnection!
-	health: Health!
-	oAuthAppOwner: Account!
-	token(token: TokenId!): Token
-	tokens: [Token!]!
-	trainings: [Training!]!
-	user(user: UserId!): User
-	users(
-		after: String
-		before: String
-		filter: UserFilter
-		first: Int
-		last: Int
-		orderBy: [OrderByUser!]! = []
-	): UserConnection!
+  account(account: AccountId!): Account
+  buildInfo: BuildInfo!
+  currentToken: Token!
+  globalsValues: GlobalValues!
+  group(group: GroupId!): Group
+  groups(after: String, before: String, filter: GroupFilter, first: Int, last: Int, orderBy: [OrderByGroup!]! = []): GroupConnection!
+  health: Health!
+  oAuthAppOwner: Account!
+  token(token: TokenId!): Token
+  tokens: [Token!]!
+  trainings: [Training!]!
+  user(user: UserId!): User
+  users(after: String, before: String, filter: UserFilter, first: Int, last: Int, orderBy: [OrderByUser!]! = []): UserConnection!
 }
 
 input SimilarFilter {
-	threshold: Float
-	value: String!
+  threshold: Float
+  value: String!
 }
 
 scalar SmolStr
 
 input TextFilter {
-	levenshtein: [LevenshteinFilter!]
-	like: [String!]
-	notLike: [String!]
-	similar: [SimilarFilter!]
+  levenshtein: [LevenshteinFilter!]
+  like: [String!]
+  notLike: [String!]
+  similar: [SimilarFilter!]
 }
 
 type Token {
-	allowedIps: [String!]!
-	claims: [TokenClaim!]!
-
-	"""
-	# **ADMIN ONLY**
-	## Description du jeton
-	"""
-	description: String!
-	globalAccess: ClaimAccess
-
-	"""
-	# **ADMIN ONLY**
-	## Identifiant du jeton
-	"""
-	id: String!
-
-	"""
-	# **ADMIN ONLY**
-	## Hash du jeton
-	Standard actuel : BLAKE3
-
-	Par exemple, `$blake3$1ChQCR0BrfBO42AkRogZaw$+COVVpcK/ptUTSckqIdI/rFF1JdIkvk9V++z56kLNf4'`
-	"""
-	tokenHash: String!
-
-	"""
-	# **ADMIN ONLY**
-	## UUID interne du jeton
-
-	Par exemple, `72c612f8-1d07-432e-8f71-60584be51f40`
-	"""
-	uuid: UUID!
+  allowedIps: [String!]!
+  claims: [TokenClaim!]!
+
+  """
+  # **ADMIN ONLY**
+  ## Description du jeton
+  """
+  description: String!
+  globalAccess: ClaimAccess
+
+  """
+  # **ADMIN ONLY**
+  ## Identifiant du jeton
+  """
+  id: String!
+
+  """
+  # **ADMIN ONLY**
+  ## Hash du jeton
+  Standard actuel : BLAKE3
+  
+  Par exemple, `$blake3$1ChQCR0BrfBO42AkRogZaw$+COVVpcK/ptUTSckqIdI/rFF1JdIkvk9V++z56kLNf4'`
+  """
+  tokenHash: String!
+
+  """
+  # **ADMIN ONLY**
+  ## UUID interne du jeton
+  
+  Par exemple, `72c612f8-1d07-432e-8f71-60584be51f40`
+  """
+  uuid: UUID!
 }
 
 type TokenClaim {
-	access: ClaimAccess!
-	claim: String!
+  access: ClaimAccess!
+  claim: String!
 }
 
 input TokenId {
-	globalAccess: ClaimAccess
-	id: SmolStr
-	uuid: UUID
+  globalAccess: ClaimAccess
+  id: SmolStr
+  uuid: UUID
 }
 
 type Training {
-	fullName: String
-	id: Identifier!
-	name: String!
-	uuid: UUID!
+  fullName: String
+  id: Identifier!
+  name: String!
+  uuid: UUID!
 }
 
 input TrainingId {
-	id: SmolStr
-	uuid: UUID
+  id: SmolStr
+  uuid: UUID
 }
 
 """
@@ -818,89 +722,89 @@ entities without requiring a central allocating authority.
 scalar UUID
 
 type UnixAccount {
-	accountUuid: UUID!
-	uid: Int!
-	vhost: String
-
-	"""
-	URL du site personnel de l'utilisateur
+  accountUuid: UUID!
+  uid: Int!
+  vhost: String
 
-	Page "perso" de l'utilisateur hébergée sur les serveurs d'ARISE.
-
-	Par exemple, `https://acier.perso.iiens.net`
-	"""
-	website: Url
+  """
+  URL du site personnel de l'utilisateur
+  
+  Page "perso" de l'utilisateur hébergée sur les serveurs d'ARISE.
+  
+  Par exemple, `https://acier.perso.iiens.net`
+  """
+  website: Url
 }
 
 input UnixAccountFilter {
-	null: Boolean
-	uid: Int16Filter
-	uuid: UuidFilter
-	vhost: TextFilter
+  null: Boolean
+  uid: Int16Filter
+  uuid: UuidFilter
+  vhost: TextFilter
 }
 
 input UnixAccountId {
-	uid: Int
-	uuid: UUID
+  uid: Int
+  uuid: UUID
 }
 
 input UpdateGroup {
-	active: Boolean
-	description: String
-	email: String
-	emailVerified: Boolean
-	hidden: Boolean
-	id: Identifier
-	name: String
-	shortDescription: String
-	type: GroupType
-	website: Url
+  active: Boolean
+  description: String
+  email: String
+  emailVerified: Boolean
+  hidden: Boolean
+  id: Identifier
+  name: String
+  shortDescription: String
+  type: GroupType
+  website: Url
 }
 
 input UpdateToken {
-	allowedIps: [String!]
-	description: String
-	id: String
+  allowedIps: [String!]
+  description: String
+  id: String
 }
 
 input UpdateTraining {
-	fullName: String
-	name: String
+  fullName: String
+  name: String
 }
 
 input UpdateUnixAccount {
-	uid: Int
-	vhost: String
+  uid: Int
+  vhost: String
 }
 
 input UpdateUser {
-	aeiieMember: Boolean
-	birthdate: Date
-	diplomaYearDuration: Int
-	email: String
-	emailVerified: Boolean
-	extendedTrialPeriod: Boolean
-	familyName: String
-	gapYear: Int
-	gender: Gender
-	givenNameAtBirth: String
-	givenNameInUse: String
-	hidden: Boolean
-	id: Identifier
-	initialPromotion: Int
-	lastUsedAt: LocalDateTime
-	locale: String
-	middleName: String
-	nickname: String
-	password: Password
-	phoneNumber: String
-	phoneNumberVerified: Boolean
-	public: Boolean
-	schoolLogin: String
-	suspended: Boolean
-	trainingId: SmolStr
-	website: Url
-	zoneinfo: String
+  aeiieMember: Boolean
+  birthdate: Date
+  diplomaYearDuration: Int
+  email: String
+  emailVerified: Boolean
+  extendedTrialPeriod: Boolean
+  familyName: String
+  gapYear: Int
+  gender: Gender
+  givenNameAtBirth: String
+  givenNameInUse: String
+  hidden: Boolean
+  id: Identifier
+  initialPromotion: Int
+  lastUsedAt: LocalDateTime
+  locale: String
+  middleName: String
+  nickname: String
+  password: Password
+  phoneNumber: String
+  phoneNumberVerified: Boolean
+  public: Boolean
+  schoolLogin: String
+  suspended: Boolean
+  trainingId: SmolStr
+  website: Url
+  zoneinfo: String
 }
 
 """
@@ -911,569 +815,541 @@ scalar Url
 scalar Urn
 
 type User {
-	"""
-	Adresse de l'utilisateur
-	"""
-	address: Address
-
-	"""
-	Adhérent de l'AEIIE
-
-	**VRAI** si l'utilisateur est membre de l'AEIIE. **FAUX** à la création du compte.
-	"""
-	aeiieMember: Boolean!
-
-	"""
-	URL de l'image d'arrière plan de l'utilisateur
-
-	Cette URL fait référence à un fichier image (par exemple, un fichier image PNG,
-	JPEG ou GIF), plutôt qu'à une page Web contenant une image.
-
-	Par exemple, `https://api.iiens.net/picture/acier2020`
-	"""
-	backgroundImage: Url!
-
-	"""
-	Hash de l'image d'arrière plan de l'utilisateur
-	"""
-	backgroundImageThumbnailHash: Base64!
-
-	"""
-	Date de naissance de l'utilisateur
-	"""
-	birthdate: Date
-
-	"""
-	Nom complet administratif de l'utilisateur
-
-	Présenté sous une forme affichable, avec toutes les parties du nom, **SAUF** le surnom.
-
-	Par exemple, `Jean Dupont`
-	"""
-	civilName: String!
-
-	"""
-	Date de la création du compte de l'utilisateur
-
-	Sa valeur est un string JSON [RFC3339] représentant le temps  entre 1970-01-01T0:0:0Z,
-	mesuré en UTC, et la date/heure.
-	"""
-	createdAt: DateTime!
-
-	"""
-	Années d'études requises jusqu'au diplôme
-
-	Par défaut cette valeur est de 03.
-	"""
-	diplomaYearDuration: Int!
-
-	"""
-	Adresse électronique de l'utilisateur
-
-	Un alias de l'adresse électronique véritable de l'utilisateur.
-	À utiliser en priorité pour la création de comptes.
-
-	Par exemple, `acier2020.e2e01581919c@alias.iiens.net`.
-	"""
-	email: String!
-
-	"""
-	Adresse électronique réelle de l'utilisateur
-
-	Sa valeur DOIT être conforme à la syntaxe addr-spec de la RFC 5322 [RFC5322].
-
-	Par exemple, `foo.bar@ensiie.fr` ou `dupont.jean@gmail.com`.
-	"""
-	emailForwardAddress: String
-
-	"""
-	Adresse électronique de l'utilisateur vérifiée
-
-	Vrai si l'adresse électronique préférée de l'utilisateur a été vérifiée, sinon faux.
-	Lorsque cette valeur d'allégation est vraie, cela signifie qu'ARISE a pris des mesures
-	positives pour s'assurer que l'adresse électronique était contrôlée par l'utilisateur
-	au moment où la vérification a été effectuée.
-	"""
-	emailVerified: Boolean!
-
-	"""
-	Période d'essai étendue de l'utilisateur
-
-	**FAUX** lors de la création du compte.
-
-	Cette variable permet d'outrepasser le champ `restricted_access`.
-	"""
-	extendedTrialPeriod: Boolean!
-
-	"""
-	Nom(s) de famille de l'utilisateur
-
-	Notez que dans certaines cultures, les personnes peuvent avoir plusieurs noms de famille ou
-	aucun nom de famille ; tous peuvent être présents, les noms étant séparés par des caractères d'espacement.
-
-	Par exemple, `Leroy`
-	"""
-	familyName: String!
-
-	"""
-	Prénoms de l'utilisateur
-
-	Tous les prénoms de l'utilisateur concaténés ensembles.
-
-	Par exemple, `Jean Pierre-Jacques` ou `Robert`
-	"""
-	forenames: String!
-
-	"""
-	Années d'étude d'écart de l'utilisateur
-
-	Désigne des périodes de temps où un utilisateur prend une pause ou un écart par
-	rapport à la norme. Cela peut être causé par une année sabbatique, une année de
-	redoublement ou pour d'autres raisons.
-
-	Par défaut cette valeur est de 0.
-	"""
-	gapYear: Int!
-
-	"""
-	Genre de l'utilisateur
-
-	Par défaut à `UNKNOWN`.
-	"""
-	gender: Gender!
-
-	"""
-	Prénom(s) de l'utilisateur
-
-	Notez que dans certaines cultures, les personnes peuvent avoir plusieurs prénoms ;
-	tous peuvent être présents, les noms étant séparés par des caractères d'espacement.
-
-	Par exemple, `Jean` ou `Pierre Jacques`
-	"""
-	givenName: String!
-
-	"""
-	Prénom(s) de naissance de l'utilisateur
-
-	Notez que dans certaines cultures, les personnes peuvent avoir plusieurs prénoms ;
-	tous peuvent être présents, les noms étant séparés par des caractères d'espacement.
-
-	Par exemple, `Jean` ou `Pierre Jacques`
-	"""
-	givenNameAtBirth: String!
-
-	"""
-	Prénom(s) d'usage de l'utilisateur
-
-	Notez que dans certaines cultures, les personnes peuvent avoir plusieurs prénoms ;
-	tous peuvent être présents, les noms étant séparés par des caractères d'espacement.
-
-	Par exemple, `Jeanne` ou `Pierre Jacques`
-	"""
-	givenNameInUse: String
-
-	"""
-	Groupe de l'utilisateur
-	"""
-	group(id: GroupId!): GroupOfMember
-
-	"""
-	Historique des groupes de l'utilisateur
-
-	Cet historique retrace tous les rôles que l'utilisateur à pu prendre au
-	sein des groups de l'école.
-	"""
-	groupHistory(filter: HistoricalGroupOfMemberFilter): [HistoricalGroupOfMember!]!
-
-	"""
-	Groupes de l'utilisateur
-
-	Les rôles passés ne seront pas renvoyés dans cette requête.
-	Pour cela, se référer à `group_history.`
-	"""
-	groups(filter: GroupOfMemberFilter): [GroupOfMember!]!
-
-	"""
-	Compte caché
-
-	Le compte utilisateur sera invisible de toutes les requêtes, sauf admin si le filtre
-	de la requête le demande.
-	"""
-	hidden: Boolean!
-
-	"""
-	Identifiant de l'utilisateur
-
-	Plus communément appelé AriseID, il est composé de 8 caractères maximum formés du nom
-	et du prénom, et de 4 chiffre représentant l'année d'arrivée pour arriver à un identifiant unique.
-
-	Par exemple, `acier2020` ou `dupontj2042`.
-	"""
-	id: Identifier!
-
-	"""
-	Promotion d'origine de l'utilisateur
-
-	La promotion présupposée de l'utilisateur au moment où il rentre à l'école.
-
-	Habituellement `année d'entrée + 3`
-	"""
-	initialPromotion: Int!
-
-	"""
-	Date de la dernière activité via l'API de l'utilisateur
-
-	Sa valeur est un string JSON [RFC3339] représentant le temps entre 1970-01-01T0:0:0Z,
-	mesuré en UTC, et la date/heure.
-	"""
-	lastUsedAt: DateTime
-
-	"""
-	Langue
-
-	Les paramètres régionaux de l'utilisateur, représentés par une balise de
-	langue BCP47 [RFC5646]. Il s'agit généralement d'un code de langue ISO 639-1
-	Alpha-2 [ISO639-1] en minuscules et d'un code de pays ISO 3166-1 Alpha-2
-	[ISO3166-1] en majuscules, séparés par un tiret.
-
-	Par exemple, `en-US` ou `fr-CA`.
-	"""
-	locale: String!
-
-	"""
-	Deuxième(s) prénom(s) de l'utilisateur
-
-	Notez que dans certaines cultures, les personnes peuvent avoir plusieurs seconds prénoms ;
-	tous peuvent être présents, les noms étant séparés par des caractères d'espacement.
-	Notez également que dans certaines cultures, les seconds prénoms ne sont pas utilisés.
-
-	Par exemple, `Robert`
-	"""
-	middleName: String
-
-	"""
-	Nom complet de l'utilisateur
-
-	Présenté sous une forme affichable, avec toutes les parties du nom, **Y COMPRIS** le surnom.
-
-	Par exemple, `Jean "Foobar" Dupont` ou `Jean Dupont`
-	"""
-	name: String!
-
-	"""
-	Surnom de l'utilisateur
-
-	Le surnom qu'a choisi l'utilisateur.
-
-	Par exemple, `Foobar` ou `Jean`
-	"""
-	nickname: String
-
-	"""
-	Groupe courant de l'utilisateur
-
-	Retourne le propriétaire de l'application si ce dernier est un groupe.
-
-	Les rôles passés ne seront pas renvoyés dans cette requête.
-	Pour cela, se référer à `current_group_history.`
-	"""
-	oAuthGroup: GroupOfMember
-
-	"""
-	Hash du mot de passe de l'utilisateur
-
-	Standard actuel : Argon2id
-
-	Par exemple, `$argon2id$v=19$m=19456,t=2,p=1$XMBjfqZtpEyVxZbGNjKPCg$krFvSFSL3XUr6736galD8YVmGgXpqNSc02VyLqFesPY'`
-	"""
-	passwordHash: String
-
-	"""
-	Numéro de téléphone de l'utilisateur
-
-	E.164 [E.164] est le format adopté pour ce champ.
-
-	Par exemple, `+14255551212` ou `+5626872400`.
-	"""
-	phoneNumber: String
-
-	"""
-	Numéro de téléphone de l'utilisateur vérifié
-
-	Vrai si le numéro de téléphone de l'utilisateur a été vérifié, sinon faux.
-	Lorsque cette valeur d'allégation est vraie, cela signifie qu'ARISE a pris
-	des mesures positives pour s'assurer que ce numéro de téléphone était contrôlé
-	par l'utilisateur final au moment où la vérification a été effectuée. Les moyens
-	par lesquels un numéro de téléphone est vérifié sont spécifiques au contexte
-	et dépendent du cadre de confiance ou des accords contractuels dans lesquels
-	les parties opèrent.
-	"""
-	phoneNumberVerified: Boolean!
-
-	"""
-	URL de la photo de trombinoscope de l'utilisateur
-
-	Cette URL fait référence à un fichier image (par exemple, un fichier image PNG,
-	JPEG ou GIF), plutôt qu'à une page Web contenant une image.
-
-	Par exemple, `https://api.iiens.net/rest/v0/photo/acier2020`
-	"""
-	photo: Url
-
-	"""
-	Hash de la photo de trombinoscope de l'utilisateur
-	"""
-	photoThumbnailHash: Base64
-
-	"""
-	URL de l'image de profil de l'utilisateur
-
-	Cette URL fait référence à un fichier image (par exemple, un fichier image PNG,
-	JPEG ou GIF), plutôt qu'à une page Web contenant une image.
-
-	Par exemple, `https://api.iiens.net/picture/acier2020`
-	"""
-	picture: Url!
-
-	"""
-	Hash de la photo de profil de l'utilisateur
-	"""
-	pictureThumbnailHash: Base64!
-
-	"""
-	Pseudo préféré
-
-	Pseudo abrégé non unique par lequel l'utilisateur souhaite être désigné.
-
-	Surnom si renseigné par l'utilisateur, sinon nom civil par défaut.
-
-	Par exemple, `Foobar` ou `dupontj2042`
-	"""
-	preferredNickname: String!
-
-	"""
-	Nom d'utilisateur préféré
-
-	Nom abrégé non unique par lequel l'utilisateur souhaite être désigné.
-
-	Surnom si renseigné par l'utilisateur, sinon identifiant ARISE par défaut.
-
-	Par exemple, `Foobar` ou `dupontj2042`
-	"""
-	preferredUsername: String!
-
-	"""
-	URL de la page de profil de l'utilisateur
-
-	Page de profil sur [www.iiens.net](https://www.iiens.net).
-
-	Par exemple, `https://www.iiens.net/eleve/acier2020`
-	"""
-	profile: Url!
-
-	"""
-	Promotion effective de l'utilisateur
-
-	Promotion de l'utilisateur en tenant compte des redoublements et autres facteurs.
-	"""
-	promotion: Int!
-
-	"""
-	Accès restreint aux services d'ARISE
-
-	**VRAI** si la période d'essai est arrivée à terme (42 jours) et que l'utilisateur n'est pas
-	membre d'ARISE **ET** de l'AEIIE.
-	"""
-	restrictedAccess: Boolean!
-
-	"""
-	Adresse électronique scolaire de l'utilisateur
-
-	Sa valeur DOIT être conforme à la syntaxe addr-spec de la RFC 5322 [RFC5322].
-
-	Par exemple, `mael.acier@ensiie.eu` ou `dupont.jean@ensiie.eu`.
-	"""
-	schoolEmail: String!
-
-	"""
-	Identifiant de connexion scolaire de l'utilisateur
-
-	Habituellement sous la forme `prenom.nom`
-	"""
-	schoolLogin: String!
-
-	"""
-	Semestre actuel de l'utilisateur
-
-	Compteur de semestres en fonction de l'année de promotion effective.
-	Les redoublements sont pris en compte dans l'évaluation de cette valeur.
-
-	Par exemple, `S1`, `S2`, `S3` *(Le `S` n'est pas ajouté automatiquement)*
-	"""
-	semester: Int!
-
-	"""
-	Compte suspendu
-
-	Le compte utilisateur ne sera plus utilisable pour se connecter aux services d'ARISE.
-	"""
-	suspended: Boolean!
-
-	"""
-	## Parcours
-
-	Par exemple, "FISE", "FISA"
-	"""
-	training: Training!
-
-	"""
-	Nombre de jours restants sur la période d'essai de l'utilisateur
-
-	**42** lors de la création du compte.
-	"""
-	trialPeriodDaysLeft: Int!
-
-	"""
-	Compte UNIX
-	"""
-	unixAccount: UnixAccount
-
-	"""
-	Date de la dernière mise à jour des informations relatives à l'utilisateur
-
-	Sa valeur est un string JSON [RFC3339] représentant le temps entre 1970-01-01T0:0:0Z,
-	mesuré en UTC, et la date/heure.
-	"""
-	updatedAt: DateTime!
-
-	"""
-	UUID du compte de l'utilisateur
-	"""
-	uuid: UUID!
-
-	"""
-	URL du site Web ou du blog de l'utilisateur
-
-	Par exemple, `https://foo.com`
-	"""
-	website: Url
-
-	"""
-	Année en cours de l'utilisateur
-
-	Compteur d'années en fonction de l'année de promotion effective.
-	Les redoublements sont pris en compte dans l'évaluation de cette valeur.
-
-	Par exemple, `1A`, `2A`, `3A` *(Le `A` n'est pas ajouté automatiquement)*
-	"""
-	year: Int!
-
-	"""
-	Fuseau horaire
-
-	Chaîne de caractères provenant de la base de données des fuseaux horaires zoneinfo
-	[zoneinfo] représentant le fuseau horaire de l'utilisateur.
-
-	Par exemple, `Europe/Paris` ou `Amérique/Los_Angeles`.
-	"""
-	zoneinfo: String!
+  """Adresse de l'utilisateur"""
+  address: Address
+
+  """
+  Adhérent de l'AEIIE
+  
+  **VRAI** si l'utilisateur est membre de l'AEIIE. **FAUX** à la création du compte.
+  """
+  aeiieMember: Boolean!
+
+  """
+  URL de l'image d'arrière plan de l'utilisateur
+  
+  Cette URL fait référence à un fichier image (par exemple, un fichier image PNG,
+  JPEG ou GIF), plutôt qu'à une page Web contenant une image.
+  
+  Par exemple, `https://api.iiens.net/picture/acier2020`
+  """
+  backgroundImage: Url!
+
+  """Hash de l'image d'arrière plan de l'utilisateur"""
+  backgroundImageThumbnailHash: Base64!
+
+  """Date de naissance de l'utilisateur"""
+  birthdate: Date
+
+  """
+  Nom complet administratif de l'utilisateur
+  
+  Présenté sous une forme affichable, avec toutes les parties du nom, **SAUF** le surnom.
+  
+  Par exemple, `Jean Dupont`
+  """
+  civilName: String!
+
+  """
+  Date de la création du compte de l'utilisateur
+  
+  Sa valeur est un string JSON [RFC3339] représentant le temps  entre 1970-01-01T0:0:0Z,
+  mesuré en UTC, et la date/heure.
+  """
+  createdAt: DateTime!
+
+  """
+  Années d'études requises jusqu'au diplôme
+  
+  Par défaut cette valeur est de 03.
+  """
+  diplomaYearDuration: Int!
+
+  """
+  Adresse électronique de l'utilisateur
+  
+  Un alias de l'adresse électronique véritable de l'utilisateur.
+  À utiliser en priorité pour la création de comptes.
+  
+  Par exemple, `acier2020.e2e01581919c@alias.iiens.net`.
+  """
+  email: String!
+
+  """
+  Adresse électronique réelle de l'utilisateur
+  
+  Sa valeur DOIT être conforme à la syntaxe addr-spec de la RFC 5322 [RFC5322].
+  
+  Par exemple, `foo.bar@ensiie.fr` ou `dupont.jean@gmail.com`.
+  """
+  emailForwardAddress: String
+
+  """
+  Adresse électronique de l'utilisateur vérifiée
+  
+  Vrai si l'adresse électronique préférée de l'utilisateur a été vérifiée, sinon faux.
+  Lorsque cette valeur d'allégation est vraie, cela signifie qu'ARISE a pris des mesures
+  positives pour s'assurer que l'adresse électronique était contrôlée par l'utilisateur
+  au moment où la vérification a été effectuée.
+  """
+  emailVerified: Boolean!
+
+  """
+  Période d'essai étendue de l'utilisateur
+  
+  **FAUX** lors de la création du compte.
+  
+  Cette variable permet d'outrepasser le champ `restricted_access`.
+  """
+  extendedTrialPeriod: Boolean!
+
+  """
+  Nom(s) de famille de l'utilisateur
+  
+  Notez que dans certaines cultures, les personnes peuvent avoir plusieurs noms de famille ou
+  aucun nom de famille ; tous peuvent être présents, les noms étant séparés par des caractères d'espacement.
+  
+  Par exemple, `Leroy`
+  """
+  familyName: String!
+
+  """
+  Prénoms de l'utilisateur
+  
+  Tous les prénoms de l'utilisateur concaténés ensembles.
+  
+  Par exemple, `Jean Pierre-Jacques` ou `Robert`
+  """
+  forenames: String!
+
+  """
+  Années d'étude d'écart de l'utilisateur
+  
+  Désigne des périodes de temps où un utilisateur prend une pause ou un écart par
+  rapport à la norme. Cela peut être causé par une année sabbatique, une année de
+  redoublement ou pour d'autres raisons.
+  
+  Par défaut cette valeur est de 0.
+  """
+  gapYear: Int!
+
+  """
+  Genre de l'utilisateur
+  
+  Par défaut à `UNKNOWN`.
+  """
+  gender: Gender!
+
+  """
+  Prénom(s) de l'utilisateur
+  
+  Notez que dans certaines cultures, les personnes peuvent avoir plusieurs prénoms ;
+  tous peuvent être présents, les noms étant séparés par des caractères d'espacement.
+  
+  Par exemple, `Jean` ou `Pierre Jacques`
+  """
+  givenName: String!
+
+  """
+  Prénom(s) de naissance de l'utilisateur
+  
+  Notez que dans certaines cultures, les personnes peuvent avoir plusieurs prénoms ;
+  tous peuvent être présents, les noms étant séparés par des caractères d'espacement.
+  
+  Par exemple, `Jean` ou `Pierre Jacques`
+  """
+  givenNameAtBirth: String!
+
+  """
+  Prénom(s) d'usage de l'utilisateur
+  
+  Notez que dans certaines cultures, les personnes peuvent avoir plusieurs prénoms ;
+  tous peuvent être présents, les noms étant séparés par des caractères d'espacement.
+  
+  Par exemple, `Jeanne` ou `Pierre Jacques`
+  """
+  givenNameInUse: String
+
+  """Groupe de l'utilisateur"""
+  group(id: GroupId!): GroupOfMember
+
+  """
+  Historique des groupes de l'utilisateur
+  
+  Cet historique retrace tous les rôles que l'utilisateur à pu prendre au
+  sein des groups de l'école.
+  """
+  groupHistory(filter: HistoricalGroupOfMemberFilter): [HistoricalGroupOfMember!]!
+
+  """
+  Groupes de l'utilisateur
+  
+  Les rôles passés ne seront pas renvoyés dans cette requête.
+  Pour cela, se référer à `group_history.`
+  """
+  groups(filter: GroupOfMemberFilter): [GroupOfMember!]!
+
+  """
+  Compte caché
+  
+  Le compte utilisateur sera invisible de toutes les requêtes, sauf admin si le filtre
+  de la requête le demande.
+  """
+  hidden: Boolean!
+
+  """
+  Identifiant de l'utilisateur
+  
+  Plus communément appelé AriseID, il est composé de 8 caractères maximum formés du nom
+  et du prénom, et de 4 chiffre représentant l'année d'arrivée pour arriver à un identifiant unique.
+  
+  Par exemple, `acier2020` ou `dupontj2042`.
+  """
+  id: Identifier!
+
+  """
+  Promotion d'origine de l'utilisateur
+  
+  La promotion présupposée de l'utilisateur au moment où il rentre à l'école.
+  
+  Habituellement `année d'entrée + 3`
+  """
+  initialPromotion: Int!
+
+  """
+  Date de la dernière activité via l'API de l'utilisateur
+  
+  Sa valeur est un string JSON [RFC3339] représentant le temps entre 1970-01-01T0:0:0Z,
+  mesuré en UTC, et la date/heure.
+  """
+  lastUsedAt: DateTime
+
+  """
+  Langue
+  
+  Les paramètres régionaux de l'utilisateur, représentés par une balise de
+  langue BCP47 [RFC5646]. Il s'agit généralement d'un code de langue ISO 639-1
+  Alpha-2 [ISO639-1] en minuscules et d'un code de pays ISO 3166-1 Alpha-2
+  [ISO3166-1] en majuscules, séparés par un tiret.
+  
+  Par exemple, `en-US` ou `fr-CA`.
+  """
+  locale: String!
+
+  """
+  Deuxième(s) prénom(s) de l'utilisateur
+  
+  Notez que dans certaines cultures, les personnes peuvent avoir plusieurs seconds prénoms ;
+  tous peuvent être présents, les noms étant séparés par des caractères d'espacement.
+  Notez également que dans certaines cultures, les seconds prénoms ne sont pas utilisés.
+  
+  Par exemple, `Robert`
+  """
+  middleName: String
+
+  """
+  Nom complet de l'utilisateur
+  
+  Présenté sous une forme affichable, avec toutes les parties du nom, **Y COMPRIS** le surnom.
+  
+  Par exemple, `Jean "Foobar" Dupont` ou `Jean Dupont`
+  """
+  name: String!
+
+  """
+  Surnom de l'utilisateur
+  
+  Le surnom qu'a choisi l'utilisateur.
+  
+  Par exemple, `Foobar` ou `Jean`
+  """
+  nickname: String
+
+  """
+  Groupe courant de l'utilisateur
+  
+  Retourne le propriétaire de l'application si ce dernier est un groupe.
+  
+  Les rôles passés ne seront pas renvoyés dans cette requête.
+  Pour cela, se référer à `current_group_history.`
+  """
+  oAuthGroup: GroupOfMember
+
+  """
+  Hash du mot de passe de l'utilisateur
+  
+  Standard actuel : Argon2id
+  
+  Par exemple, `$argon2id$v=19$m=19456,t=2,p=1$XMBjfqZtpEyVxZbGNjKPCg$krFvSFSL3XUr6736galD8YVmGgXpqNSc02VyLqFesPY'`
+  """
+  passwordHash: String
+
+  """
+  Numéro de téléphone de l'utilisateur
+  
+  E.164 [E.164] est le format adopté pour ce champ.
+  
+  Par exemple, `+14255551212` ou `+5626872400`.
+  """
+  phoneNumber: String
+
+  """
+  Numéro de téléphone de l'utilisateur vérifié
+  
+  Vrai si le numéro de téléphone de l'utilisateur a été vérifié, sinon faux.
+  Lorsque cette valeur d'allégation est vraie, cela signifie qu'ARISE a pris
+  des mesures positives pour s'assurer que ce numéro de téléphone était contrôlé
+  par l'utilisateur final au moment où la vérification a été effectuée. Les moyens
+  par lesquels un numéro de téléphone est vérifié sont spécifiques au contexte
+  et dépendent du cadre de confiance ou des accords contractuels dans lesquels
+  les parties opèrent.
+  """
+  phoneNumberVerified: Boolean!
+
+  """
+  URL de la photo de trombinoscope de l'utilisateur
+  
+  Cette URL fait référence à un fichier image (par exemple, un fichier image PNG,
+  JPEG ou GIF), plutôt qu'à une page Web contenant une image.
+  
+  Par exemple, `https://api.iiens.net/rest/v0/photo/acier2020`
+  """
+  photo: Url
+
+  """Hash de la photo de trombinoscope de l'utilisateur"""
+  photoThumbnailHash: Base64
+
+  """
+  URL de l'image de profil de l'utilisateur
+  
+  Cette URL fait référence à un fichier image (par exemple, un fichier image PNG,
+  JPEG ou GIF), plutôt qu'à une page Web contenant une image.
+  
+  Par exemple, `https://api.iiens.net/picture/acier2020`
+  """
+  picture: Url!
+
+  """Hash de la photo de profil de l'utilisateur"""
+  pictureThumbnailHash: Base64!
+
+  """
+  Pseudo préféré
+  
+  Pseudo abrégé non unique par lequel l'utilisateur souhaite être désigné.
+  
+  Surnom si renseigné par l'utilisateur, sinon nom civil par défaut.
+  
+  Par exemple, `Foobar` ou `dupontj2042`
+  """
+  preferredNickname: String!
+
+  """
+  Nom d'utilisateur préféré
+  
+  Nom abrégé non unique par lequel l'utilisateur souhaite être désigné.
+  
+  Surnom si renseigné par l'utilisateur, sinon identifiant ARISE par défaut.
+  
+  Par exemple, `Foobar` ou `dupontj2042`
+  """
+  preferredUsername: String!
+
+  """
+  URL de la page de profil de l'utilisateur
+  
+  Page de profil sur [www.iiens.net](https://www.iiens.net).
+  
+  Par exemple, `https://www.iiens.net/eleve/acier2020`
+  """
+  profile: Url!
+
+  """
+  Promotion effective de l'utilisateur
+  
+  Promotion de l'utilisateur en tenant compte des redoublements et autres facteurs.
+  """
+  promotion: Int!
+
+  """
+  Accès restreint aux services d'ARISE
+  
+  **VRAI** si la période d'essai est arrivée à terme (42 jours) et que l'utilisateur n'est pas
+  membre d'ARISE **ET** de l'AEIIE.
+  """
+  restrictedAccess: Boolean!
+
+  """
+  Adresse électronique scolaire de l'utilisateur
+  
+  Sa valeur DOIT être conforme à la syntaxe addr-spec de la RFC 5322 [RFC5322].
+  
+  Par exemple, `mael.acier@ensiie.eu` ou `dupont.jean@ensiie.eu`.
+  """
+  schoolEmail: String!
+
+  """
+  Identifiant de connexion scolaire de l'utilisateur
+  
+  Habituellement sous la forme `prenom.nom`
+  """
+  schoolLogin: String!
+
+  """
+  Semestre actuel de l'utilisateur
+  
+  Compteur de semestres en fonction de l'année de promotion effective.
+  Les redoublements sont pris en compte dans l'évaluation de cette valeur.
+  
+  Par exemple, `S1`, `S2`, `S3` *(Le `S` n'est pas ajouté automatiquement)*
+  """
+  semester: Int!
+
+  """
+  Compte suspendu
+  
+  Le compte utilisateur ne sera plus utilisable pour se connecter aux services d'ARISE.
+  """
+  suspended: Boolean!
+
+  """
+  ## Parcours
+  
+  Par exemple, "FISE", "FISA"
+  """
+  training: Training!
+
+  """
+  Nombre de jours restants sur la période d'essai de l'utilisateur
+  
+  **42** lors de la création du compte.
+  """
+  trialPeriodDaysLeft: Int!
+
+  """Compte UNIX"""
+  unixAccount: UnixAccount
+
+  """
+  Date de la dernière mise à jour des informations relatives à l'utilisateur
+  
+  Sa valeur est un string JSON [RFC3339] représentant le temps entre 1970-01-01T0:0:0Z,
+  mesuré en UTC, et la date/heure.
+  """
+  updatedAt: DateTime!
+
+  """UUID du compte de l'utilisateur"""
+  uuid: UUID!
+
+  """
+  URL du site Web ou du blog de l'utilisateur
+  
+  Par exemple, `https://foo.com`
+  """
+  website: Url
+
+  """
+  Année en cours de l'utilisateur
+  
+  Compteur d'années en fonction de l'année de promotion effective.
+  Les redoublements sont pris en compte dans l'évaluation de cette valeur.
+  
+  Par exemple, `1A`, `2A`, `3A` *(Le `A` n'est pas ajouté automatiquement)*
+  """
+  year: Int!
+
+  """
+  Fuseau horaire
+  
+  Chaîne de caractères provenant de la base de données des fuseaux horaires zoneinfo
+  [zoneinfo] représentant le fuseau horaire de l'utilisateur.
+  
+  Par exemple, `Europe/Paris` ou `Amérique/Los_Angeles`.
+  """
+  zoneinfo: String!
 }
 
 type UserConnection {
-	"""
-	A list of edges.
-	"""
-	edges: [UserEdge!]!
+  """A list of edges."""
+  edges: [UserEdge!]!
 
-	"""
-	A list of nodes.
-	"""
-	nodes: [User!]!
+  """A list of nodes."""
+  nodes: [User!]!
 
-	"""
-	Information to aid in pagination.
-	"""
-	pageInfo: PageInfo!
-	remainingCount: Int!
+  """Information to aid in pagination."""
+  pageInfo: PageInfo!
+  remainingCount: Int!
 }
 
-"""
-An edge in a connection.
-"""
+"""An edge in a connection."""
 type UserEdge {
-	"""
-	A cursor for use in pagination
-	"""
-	cursor: String!
+  """A cursor for use in pagination"""
+  cursor: String!
 
-	"""
-	The item at the end of the edge
-	"""
-	node: User!
-	remainingCount: Int!
+  """The item at the end of the edge"""
+  node: User!
+  remainingCount: Int!
 }
 
 input UserFilter {
-	accountUuid: UuidFilter
-	aeiieMember: BooleanFilter
-	backgroundThumbnailHash: NullableFilter
-	birthdate: NullableDateFilter
-	createdAt: DateTimeFilter
-	diplomaYearDuration: Int16Filter
-	email: TextFilter
-	emailVerified: BooleanFilter
-	extendedTrialPeriod: BooleanFilter
-	familyName: TextFilter
-	gapYear: Int16Filter
-	gender: GenderFilter
-	givenName: TextFilter
-	givenNameAtBirth: TextFilter
-	givenNameInUse: NullableTextFilter
-	groups: UserGroupsFilter
-	hidden: BooleanFilter
-	id: TextFilter
-	initialPromotion: Int16Filter
-	lastUsedAt: NullableDateTimeFilter
-	locale: TextFilter
-	middleName: NullableTextFilter
-	nickname: NullableTextFilter
-	or: [UserFilter!]
-	phoneNumber: NullableTextFilter
-	phoneNumberVerified: BooleanFilter
-	photoThumbnailHash: NullableFilter
-	pictureThumbnailHash: NullableFilter
-	promotion: Int16Filter
-	public: BooleanFilter
-	restrictedAccess: BooleanFilter
-	schoolLogin: TextFilter
-	semester: Int16Filter
-	suspended: BooleanFilter
-	training: TextFilter
-	trialPeriodDaysLeft: Int16Filter
-	unixAccount: UnixAccountFilter
-	updatedAt: DateTimeFilter
-	uuid: UuidFilter
-	website: NullableTextFilter
-	year: Int16Filter
-	zoneinfo: TextFilter
+  accountUuid: UuidFilter
+  aeiieMember: BooleanFilter
+  backgroundThumbnailHash: NullableFilter
+  birthdate: NullableDateFilter
+  createdAt: DateTimeFilter
+  diplomaYearDuration: Int16Filter
+  email: TextFilter
+  emailVerified: BooleanFilter
+  extendedTrialPeriod: BooleanFilter
+  familyName: TextFilter
+  gapYear: Int16Filter
+  gender: GenderFilter
+  givenName: TextFilter
+  givenNameAtBirth: TextFilter
+  givenNameInUse: NullableTextFilter
+  groups: UserGroupsFilter
+  hidden: BooleanFilter
+  id: TextFilter
+  initialPromotion: Int16Filter
+  lastUsedAt: NullableDateTimeFilter
+  locale: TextFilter
+  middleName: NullableTextFilter
+  nickname: NullableTextFilter
+  or: [UserFilter!]
+  phoneNumber: NullableTextFilter
+  phoneNumberVerified: BooleanFilter
+  photoThumbnailHash: NullableFilter
+  pictureThumbnailHash: NullableFilter
+  promotion: Int16Filter
+  public: BooleanFilter
+  restrictedAccess: BooleanFilter
+  schoolLogin: TextFilter
+  semester: Int16Filter
+  suspended: BooleanFilter
+  training: TextFilter
+  trialPeriodDaysLeft: Int16Filter
+  unixAccount: UnixAccountFilter
+  updatedAt: DateTimeFilter
+  uuid: UuidFilter
+  website: NullableTextFilter
+  year: Int16Filter
+  zoneinfo: TextFilter
 }
 
 input UserGroupsFilter {
-	active: BooleanFilter
-	hidden: BooleanFilter
-	id: TextFilter
-	name: TextFilter
-	type: GroupTypeFilter
-	uuid: UuidFilter
+  active: BooleanFilter
+  hidden: BooleanFilter
+  id: TextFilter
+  name: TextFilter
+  type: GroupTypeFilter
+  uuid: UuidFilter
 }
 
 input UserId {
-	id: SmolStr
-	personalEmail: String
-	urn: Urn
-	uuid: UUID
+  id: SmolStr
+  personalEmail: String
+  urn: Urn
+  uuid: UUID
 }
 
 input UuidFilter {
-	isIn: [UUID!]!
+  isIn: [UUID!]!
 }
diff --git a/src/routes/quiz/+page.server.ts b/src/routes/quiz/+page.server.ts
index 19af07168aa66fd727eb7c989df84feb3e034775..d7c9432595cc12ec5ae3128cc899d56635a2f015 100644
--- a/src/routes/quiz/+page.server.ts
+++ b/src/routes/quiz/+page.server.ts
@@ -2,10 +2,10 @@ import { superValidate } from 'sveltekit-superforms';
 import { zod } from 'sveltekit-superforms/adapters';
 import { schema } from './schema';
 import { fail, redirect } from '@sveltejs/kit';
-import { GetPromotionStore, UserDetailsStore } from '$houdini';
-import { pageIterator } from '$lib/graphql/query';
+import { UserDetailsStore } from '$houdini';
 import { getRandomItems } from '$lib/utils';
 import { GameStage, Game } from '$lib/game';
+import { getPromotion } from '$lib/data';
 
 type Option = {
 	value: string;
@@ -18,10 +18,7 @@ export async function load(event) {
 	if (game.state.stage === GameStage.GAME_OVER) redirect(303, '/quiz/game-over');
 
 	if (game.state.stage === GameStage.NEXT) {
-		const pagination = pageIterator(event, GetPromotionStore, { promotion: 2023 });
-
-		const promotion = await Array.fromAsync(pagination);
-		const all = new Set(promotion.map((p) => p.id));
+		const all = await getPromotion(2023, event);
 
 		const previous = new Set(game.state.history);
 		const available = all.difference(previous);