diff --git a/src/lib/components/AdminLayout.svelte b/src/lib/components/AdminLayout.svelte
index 71350f6fd8a22129fe7e85beba7c66d37e39542f..795355d3f98042bcdf7cb3ab48835ce06f727e9d 100644
--- a/src/lib/components/AdminLayout.svelte
+++ b/src/lib/components/AdminLayout.svelte
@@ -33,6 +33,8 @@
 			<DropdownItem href="/admin/tutor">Übersicht</DropdownItem>
 			<DropdownItem href="/admin/tutor/training">Schulungen</DropdownItem>
 			<DropdownItem href="/admin/tutor/mail">Mail senden</DropdownItem>
+			<DropdownItem href="/admin/tutor/files">Dateien</DropdownItem>
+			<DropdownItem href="/admin/tutor/studyprogram">Studiengänge</DropdownItem>
 			<DropdownDivider />
 			<DropdownItem href="/admin/tutor/former">Alttutoren</DropdownItem>
 		</Dropdown>
@@ -44,6 +46,7 @@
 			<DropdownItem href="/admin/rallye/station">Stationen</DropdownItem>
 			<DropdownItem href="/admin/rallye/points">Punkte</DropdownItem>
 		</Dropdown>
+		<NavLi href="/admin/mr-x">Mr. X</NavLi>
 		<NavLi href="/admin/rabatte">Rabatte</NavLi>
 		<NavLi href="/admin/flyer">Flyer</NavLi>
 		<NavLi href="/admin/uploads">Uploads</NavLi>
diff --git a/src/lib/components/MailInput.svelte b/src/lib/components/MailInput.svelte
index 974e1097d925c8811bdfd044b2e76e25bda839f0..d77899aad1de21b5bdb01ea77c9c63171a48977d 100644
--- a/src/lib/components/MailInput.svelte
+++ b/src/lib/components/MailInput.svelte
@@ -2,7 +2,7 @@
 	import Degree from "$lib/degrees";
 	import Gender from "$lib/genders";
 	import { locale, locales } from "$lib/i18n/i18n";
-	import { variables, conditions, compileTemplate, type PartialConfig, type TemplateType } from "$lib/mail";
+	import { compileTemplate, type PartialConfig, type TemplateType } from "$lib/mail";
 	import type { StudyProgram } from "$lib/server/database/entities/StudyProgram.entity";
 	import type { Tutor } from "$lib/server/database/entities/Tutor.entity";
 	import type { TutorTraining } from "$lib/server/database/entities/TutorTraining.entity";
@@ -48,6 +48,8 @@
 		birthday: "1970-01-01",
 		coTutorWish: "Maxine Musterfrau",
 		degree: "Bachelor",
+		wouldLeadBachelor: true,
+		wouldLeadMaster: false,
 		gender: "m",
 		mentor: false,
 		notes: "Montag noch nicht in Aachen",
diff --git a/src/lib/csv.ts b/src/lib/csv.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6e02aa76cf44f1654dc08656dd11493ed930f905
--- /dev/null
+++ b/src/lib/csv.ts
@@ -0,0 +1,13 @@
+export function toCSV<T extends Record<string, unknown>>(data: T[], fields: (keyof T)[]): string {
+	return fields.map(f=>escape(f as string)).join(",") + "\n" + data.map(o=>{
+		return fields.map(f=>{
+			const val = o[f];
+			if(val === undefined || val === null) return "";
+			else return escape(String(o[f]));
+		}).join(",") + "\n";
+	}).join("");
+}
+
+function escape(s: string): string {
+	return `"${s.replaceAll('"', '""')}"`;
+}
diff --git a/src/lib/i18n/de.ts b/src/lib/i18n/de.ts
index 27344630797745d975fccef3208951ebb286f85d..42a9abbc099216a9b0b03962156ef6f6bdd6d3b3 100644
--- a/src/lib/i18n/de.ts
+++ b/src/lib/i18n/de.ts
@@ -29,10 +29,15 @@ export default {
 		"/admin/tutor": "Tutor:innen verwalten",
 		"/admin/tutor/[id=number]": "Tutor:in bearbeiten: {tutor.firstname} {tutor.lastname}",
 		"/admin/tutor/mail": "E-Mail senden",
+		"/admin/tutor/studyprogram": "Tutor-Studiengänge verwalten",
 		"/admin/tutor/training": "Tutschulungen verwalten",
 		"/admin/tutor/training/[id=number]": "Tutschulung bearbeiten: {training.date}",
 		"/admin/tutor/training/new": "Neue Tutschulung erstellen",
+		"/admin/uploads": "Hochgeladene Bilder",
 		"/admin/user": "Berechtigungen verwalten",
+		"/intern": "Login",
+		"/intern/rally": "", // TODO
+		"/intern/tutor": "", // TODO
 	},
 	Navbar: {
 		Branding: "Fachschaft I/1",
@@ -41,6 +46,7 @@ export default {
 			"/rabatte": "Rabatte",
 			"/eswe": "Erstsemesterwochenende",
 			"/flyer": "Flyer",
+			"/mr-x": "Mr. X",
 			"/tutor": "Tutor",
 			"/upload": "Momente teilen",
 		},
@@ -126,16 +132,24 @@ export default {
 			Email: "RWTH-Mail",
 			Phone: "Telefonnummer",
 			PhoneHelper: "Für kurzfristige Kommunikation v.a. während der Erstiwoche wollen wir dich eventuell anrufen",
-			Address: "Adresse",
+			FullAddress: "Adresse",
+			Address: "Straße und Hausnummer",
+			Zip: "Postleitzahl",
+			City: "Ort",
 			Gender: "Geschlecht",
 			ShirtSize: "T-Shirt-Größe",
+			ShirtSizeHelper: "Das T-Shirt muss während der Erstirallye als oberste Lage getragen werden, also über deiner Jacke. Im Notfall gib lieber eine Nummer größer an, damit es auch passt.",
 			DietaryRestriction: "Essgewohnheiten und Unverträglichkeiten",
 			DietaryRestrictionHelper: "Bei Veranstaltungen wie bspw. der Tutschulung gibt es Verpflegung. Unverträglichkeiten und Einschränkungen wie Veganismus zu kennen hilft uns, allen etwas anbieten zu können.",
 			StudyProgram: "Studiengang",
 			StudyProgramWaitlist: "Für den Studiengang {studyProgram} werden voraussichtlich keine weiteren Tutoren benötigt. Wenn du dich trotzdem anmeldest, wirst du auf die Warteliste gesetzt und wir melden uns bei dir, falls wir dich doch noch brauchen.",
 			Degree: "Abschluss",
 			DegreeHelper: "Der Abschluss, den du gerade anstrebst",
+			WouldLeadBachelor: "Ich würde ein Bachelor-Tutorium leiten",
+			WouldLeadMaster: "Ich würde ein Master-Tutorium leiten",
 			Training: "Schulung",
+			TrainingOption: "{date|dateWithWeekday} ({language})",
+			TrainingOptionFull: "{date|dateWithWeekday} ({language}) - voll",
 			AlreadyTrained: "Bereits geschult",
 			CoTutorWish: "Wunsch-Co-Tutor (optional)",
 			CoTutorWishHelper: "Ihr müsst beide das gleiche Fach studieren. In der Informatik muss ein:e Mentor:in dabei sein.",
@@ -249,4 +263,17 @@ export default {
 		],
 		Footer: "Sofern nicht anders angegeben, ist die männliche Form in diesem Text nicht geschlechterspezifisch gemeint, sondern wurde aus Gründen der Lesbarkeit gewählt.",
 	},
+	Internal: {
+		Settigs: {
+			Headline: "Einstellungen",
+			ProfileHeadline: "Profil",
+			PasswordHeadline: "Passwort",
+			Save: "Speichern",
+			OldPassword: "Altes Passwort",
+			NewPassword: "Neues Passwort",
+			NewPasswordRepeat: "Neues Passwort wiederholen",
+			PasswordMismatch: "Die Passwörter stimmen nicht überein",
+			PasswordTooShort: "Das Passwort muss mindestens 8 Zeichen lang sein",
+		},
+	},
 }
diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts
index 5781661d73bc1664ccf9b8d2b44dc770d0c2b9c5..266c6a13d191092bd47cc580eb92a0d33ce79f85 100644
--- a/src/lib/i18n/en.ts
+++ b/src/lib/i18n/en.ts
@@ -31,10 +31,15 @@ export default {
 		"/admin/tutor": "Manage Tutors",
 		"/admin/tutor/[id=number]": "Edit Tutor: {tutor.firstname} {tutor.lastname}",
 		"/admin/tutor/mail": "Send Email",
+		"/admin/tutor/studyprogram": "Manage Tutor Study Programs",
 		"/admin/tutor/training": "Manage Tutor Trainings",
 		"/admin/tutor/training/[id=number]": "Edit tutor training: {training.date}",
 		"/admin/tutor/training/new": "Create new tutor training",
+		"/admin/uploads": "Uploaded photos",
 		"/admin/user": "Manage Permissions",
+		"/intern": "Login",
+		"/intern/rally": "", // TODO
+		"/intern/tutor": "", // TODO
 	},
 	Navbar: {
 		Branding: "Student Council I/1",
@@ -43,6 +48,7 @@ export default {
 			"/rabatte": "Discounts",
 			"/eswe": "Freshers' Weekend",
 			"/flyer": "Flyer",
+			"/mr-x": "Mr. X",
 			"/tutor": "Tutor",
 			"/upload": "Share Moments",
 		},
@@ -129,16 +135,24 @@ export default {
 			Email: "RWTH-Mail",
 			Phone: "Phone Number",
 			PhoneHelper: "For short-term communication, especially during the freshers' week, we may want to call you",
-			Address: "Address",
+			FullAddress: "Address",
+			Address: "Street and House Number",
+			Zip: "Zip Code",
+			City: "City",
 			Gender: "Gender",
 			ShirtSize: "T-Shirt Size",
+			ShirtSizeHelper: "The T-shirt must be worn as the top layer during the freshers' rally, i.e. over your jacket. In case of doubt, please give a larger size so that it fits.",
 			DietaryRestriction: "Dietary Restrictions",
 			DietaryRestrictionHelper: "We offer food for events like the tutor trainings. Knowing about your dietary restrictions helps us to plan the food accordingly.",
 			StudyProgram: "Study Program",
 			StudyProgramWaitlist: "We probably won't need any more tutors for {studyProgram|lowercase} this year. If you sign up anyway, we will put you on the waiting list and contact you if we need more tutors.",
 			Degree: "Degree",
 			DegreeHelper: "The degree you are currently pursuing",
+			WouldLeadBachelor: "I would lead a bachelor tutorial",
+			WouldLeadMaster: "I would lead a master tutorial",
 			Training: "Training",
+			TrainingOption: "{date|dateWithWeekday} ({language})",
+			TrainingOptionFull: "{date|dateWithWeekday} ({language}) - full",
 			AlreadyTrained: "Already trained",
 			CoTutorWish: "Co-Tutor Wish (optional)",
 			CoTutorWishHelper: "You both have to study the same subject. In computer science, one of you has to be a mentor.",
@@ -252,4 +266,17 @@ export default {
 		],
 		Footer: "Sofern nicht anders angegeben, ist die männliche Form in diesem Text nicht geschlechterspezifisch gemeint, sondern wurde aus Gründen der Lesbarkeit gewählt.",
 	},
+	Internal: {
+		Settigs: {
+			Headline: "Settings",
+			ProfileHeadline: "Profile",
+			PasswordHeadline: "Password",
+			Save: "Save",
+			OldPassword: "Old password",
+			NewPassword: "New password",
+			NewPasswordRepeat: "Repeat new password",
+			PasswordMismatch: "The passwords do not match",
+			PasswordTooShort: "The password must be at least 8 characters long",
+		},
+	},
 } satisfies Translation<LocalizedString>;
diff --git a/src/lib/i18n/i18n.ts b/src/lib/i18n/i18n.ts
index e95900f923a8f69704bc4948fb620c0d7c7bbd6a..46fc16a489c4ab4ff0e1ef2ef1e8ccc6fc79b5c9 100644
--- a/src/lib/i18n/i18n.ts
+++ b/src/lib/i18n/i18n.ts
@@ -141,6 +141,7 @@ function generateLObject(obj: typeof de, locale: Locale): Translation {
 const formatterBuilders: Record<string, (lang: Locale)=>(arg: any)=>unknown> = {
 	dateLong: (lang: Locale)=>new Intl.DateTimeFormat(lang, {year: "numeric", month: "long", day: "2-digit"}).format,
 	dateRangeLong: (lang: Locale)=>(value: [number|Date, number|Date])=>new Intl.DateTimeFormat(lang, {year: "numeric", month: "long", day: "2-digit"}).formatRange(value[0], value[1]),
+	dateWithWeekday: (lang: Locale)=>new Intl.DateTimeFormat(lang, {weekday: "short", year: "numeric", month: "long", day: "2-digit"}).format,
 	sanitize: ()=>(input: string)=>input.replace(/[^a-zA-Z0-9_ ()äöüÄÖÜß-]/g, "-"),
 	lowercase: ()=>(input: string)=>input.toLowerCase(),
 }
diff --git a/src/lib/perms.ts b/src/lib/perms.ts
index 95f2fe8909640843e6af9bc3404e25c3503bd9e9..6bfda68d4327478707a11b572ada44492f4686be 100644
--- a/src/lib/perms.ts
+++ b/src/lib/perms.ts
@@ -12,6 +12,8 @@ export enum Permission {
 	EDIT_TRAININGS = 1<<10,
 	UPDATE_CONFIG = 1<<11,
 	UPDATE_MAIL_TEMPLATES = 1<<12,
+	UPDATE_RALLY_STATIONS = 1<<13,
+	UPDATE_RALLY_POINTS = 1<<14,
 }
 export const PermissionDescription: {[permission in Permission]: string} = {
 	[Permission.ADMIN]: "Berechtigungen aller Nutzer setzen",
@@ -27,6 +29,8 @@ export const PermissionDescription: {[permission in Permission]: string} = {
 	[Permission.EDIT_TRAININGS]: "Erstellen, Bearbeiten und Löschen von Schulungen",
 	[Permission.UPDATE_CONFIG]: "Grundeinstellungen ändern",
 	[Permission.UPDATE_MAIL_TEMPLATES]: "E-Mail-Vorlagen bearbeiten",
+	[Permission.UPDATE_RALLY_STATIONS]: "Rallye-Stationen erstellen/löschen/bearbeiten",
+	[Permission.UPDATE_RALLY_POINTS]: "Bewertungen für Rallye-Stationen bearbeiten und auswerten",
 };
 
 export class PermissionSet {
diff --git a/src/lib/server/database/entities/StudyProgram.entity.ts b/src/lib/server/database/entities/StudyProgram.entity.ts
index ea0051fe52a356175d36ce6971a2561998dd78bc..e7d558e6882038ee53f3ad16db49e4706cb6ba28 100644
--- a/src/lib/server/database/entities/StudyProgram.entity.ts
+++ b/src/lib/server/database/entities/StudyProgram.entity.ts
@@ -4,8 +4,6 @@ import { sql } from "../db";
 export class StudyProgram {
 	id!: number;
 	name!: Localized;
-	tutorsWanted!: number;
-	tutorialNames!: string[];
 	
 	static parse(program: any){
 		if(!program) return undefined;
@@ -29,8 +27,4 @@ export class StudyProgram {
 		// TODO handle foreign key constraints
 		await sql`DELETE FROM study_programs WHERE id=${id}`;
 	}
-	static async getMissingTutors(): Promise<{[id: number]: number|null}>{
-		const rows = await sql`SELECT study_programs.id, study_programs.tutors_wanted, COUNT(tutors.id) AS tutors FROM study_programs LEFT JOIN tutors ON study_programs.id=tutors.study_program GROUP BY study_programs.id`;
-		return rows.reduce((acc, row)=>({...acc, [row.id]: row.tutorsWanted ? row.tutorsWanted - row.tutors : null}), {});
-	}
 }
diff --git a/src/lib/server/database/entities/Tutor.entity.ts b/src/lib/server/database/entities/Tutor.entity.ts
index 73b8ce3eedff1c12ae1a444686378715a67e6ad8..a65ec6498ae5bc87823671d60bf9f3dcbae55989 100644
--- a/src/lib/server/database/entities/Tutor.entity.ts
+++ b/src/lib/server/database/entities/Tutor.entity.ts
@@ -3,7 +3,7 @@ import type { Gender } from "$lib/genders";
 import type { PartialExcept } from "$lib/utils";
 import { sql } from "../db";
 import { FormerTutor } from "./FormerTutor.entity";
-import { StudyProgram } from "./StudyProgram.entity";
+import { TutorStudyProgram } from "./TutorStudyProgram.entity";
 import { Tutorial } from "./Tutorial.entity";
 import { TutorTraining } from "./TutorTraining.entity";
 
@@ -20,12 +20,14 @@ export class Tutor {
 	shirtSize!: string;
 	degree!: keyof typeof Degree;
 	dietaryRestriction?: string|null;
-	studyProgram!: StudyProgram;
+	studyProgram!: TutorStudyProgram;
 	training?: TutorTraining|null;
 	sentTrainingMail!: boolean;
 	trained!: boolean;
 	coTutorWish!: string;
 	mentor!: boolean;
+	wouldLeadBachelor!: boolean;
+	wouldLeadMaster!: boolean;
 	tutorial?: Tutorial|null;
 	notes!: string;
 	number?: string|null;
@@ -36,7 +38,7 @@ export class Tutor {
 		if(!tutor) return undefined;
 		const t: Tutor = Object.assign(new Tutor(), tutor);
 		if(tutor.training) t.training = TutorTraining.parse(tutor.training);
-		if(tutor.studyProgram) t.studyProgram = StudyProgram.parse(tutor.studyProgram)!;
+		if(tutor.studyProgram) t.studyProgram = TutorStudyProgram.parse(tutor.studyProgram)!;
 		if(tutor.tutorial) t.tutorial = Tutorial.parse(tutor.tutorial);
 		if(tutor.former) t.former = FormerTutor.parse(tutor.former);
 		return t;
@@ -44,17 +46,17 @@ export class Tutor {
 	
 	// TODO include tutorial in getById, getByEmail and getAll
 	static async getById(id: number){
-		const row = (await sql`SELECT row_to_json(tutors.*) AS tutor, row_to_json(tutor_trainings.*) AS training, row_to_json(study_programs.*) AS study_program, row_to_json(former_tutors.*) AS former FROM tutors LEFT JOIN tutor_trainings ON tutors.training=tutor_trainings.id LEFT JOIN study_programs ON tutors.study_program=study_programs.id LEFT JOIN former_tutors ON tutors.email=former_tutors.email WHERE tutors.id=${id}`)[0];
+		const row = (await sql`SELECT row_to_json(tutors.*) AS tutor, row_to_json(tutor_trainings.*) AS training, row_to_json(tutor_study_programs.*) AS study_program, row_to_json(former_tutors.*) AS former FROM tutors LEFT JOIN tutor_trainings ON tutors.training=tutor_trainings.id LEFT JOIN tutor_study_programs ON tutors.study_program=tutor_study_programs.id LEFT JOIN former_tutors ON tutors.email=former_tutors.email WHERE tutors.id=${id}`)[0];
 		if(!row) return undefined;
 		return Tutor.parse(Object.assign(row.tutor, { training: row.training, studyProgram: row.studyProgram }));
 	}
 	static async getByEmail(email: string){
-		const row = (await sql`SELECT row_to_json(tutors.*) AS tutor, row_to_json(tutor_trainings.*) AS training, row_to_json(study_programs.*) AS study_program, row_to_json(former_tutors.*) AS former FROM tutors LEFT JOIN tutor_trainings ON tutors.training=tutor_trainings.id LEFT JOIN study_programs ON tutors.study_program=study_programs.id LEFT JOIN former_tutors ON tutors.email=former_tutors.email WHERE tutors.email=${email}`)[0];
+		const row = (await sql`SELECT row_to_json(tutors.*) AS tutor, row_to_json(tutor_trainings.*) AS training, row_to_json(tutor_study_programs.*) AS study_program, row_to_json(former_tutors.*) AS former FROM tutors LEFT JOIN tutor_trainings ON tutors.training=tutor_trainings.id LEFT JOIN tutor_study_programs ON tutors.study_program=tutor_study_programs.id LEFT JOIN former_tutors ON tutors.email=former_tutors.email WHERE tutors.email=${email}`)[0];
 		if(!row) return undefined;
 		return Tutor.parse(Object.assign(row.tutor, { training: row.training, studyProgram: row.studyProgram }));
 	}
 	static async getAll(){
-		const rows = await sql`SELECT row_to_json(tutors.*) AS tutor, row_to_json(tutor_trainings.*) AS training, row_to_json(study_programs.*) AS study_program, row_to_json(former_tutors.*) AS former FROM tutors LEFT JOIN tutor_trainings ON tutors.training=tutor_trainings.id LEFT JOIN study_programs ON tutors.study_program=study_programs.id LEFT JOIN former_tutors ON tutors.email=former_tutors.email`;
+		const rows = await sql`SELECT row_to_json(tutors.*) AS tutor, row_to_json(tutor_trainings.*) AS training, row_to_json(tutor_study_programs.*) AS study_program, row_to_json(former_tutors.*) AS former FROM tutors LEFT JOIN tutor_trainings ON tutors.training=tutor_trainings.id LEFT JOIN tutor_study_programs ON tutors.study_program=tutor_study_programs.id LEFT JOIN former_tutors ON tutors.email=former_tutors.email`;
 		return rows.map(row => Tutor.parse(Object.assign(row.tutor, { training: row.training, studyProgram: row.studyProgram }))!);
 	}
 	static async create(tutor: Omit<Tutor, "id"|"studyProgram"|"training"|"tutorial"|"former">&{studyProgram: number, training?: number|null}){
diff --git a/src/lib/server/database/entities/TutorStudyProgram.entity.ts b/src/lib/server/database/entities/TutorStudyProgram.entity.ts
new file mode 100644
index 0000000000000000000000000000000000000000..353e5f59597f71fdfd366f3e172532f29dd95ff1
--- /dev/null
+++ b/src/lib/server/database/entities/TutorStudyProgram.entity.ts
@@ -0,0 +1,36 @@
+import type { Localized } from "$lib/utils";
+import { sql } from "../db";
+
+export class TutorStudyProgram {
+	id!: number;
+	name!: Localized;
+	tutorsWanted!: number;
+	tutorialNames!: string[];
+	
+	static parse(program: any){
+		if(!program) return undefined;
+		const p: TutorStudyProgram = Object.assign(new TutorStudyProgram(), program);
+		return p;
+	}
+	
+	static async getById(id: number): Promise<TutorStudyProgram|undefined> {
+		return TutorStudyProgram.parse((await sql`SELECT * FROM tutor_study_programs WHERE id=${id}`)[0]);
+	}
+	static async getAll(){
+		return (await sql`SELECT * FROM tutor_study_programs ORDER BY id ASC`).map(program=>TutorStudyProgram.parse(program)!);
+	}
+	static async update(updatedProgram: TutorStudyProgram){
+		await sql`UPDATE tutor_study_programs SET ${sql(updatedProgram)} WHERE id=${updatedProgram.id}`;
+	}
+	static async create(program: Omit<TutorStudyProgram, "id">){
+		await sql`INSERT INTO tutor_study_programs ${sql(program)}`;
+	}
+	static async delete(id: number){
+		// TODO handle foreign key constraints
+		await sql`DELETE FROM tutor_study_programs WHERE id=${id}`;
+	}
+	static async getMissingTutors(): Promise<{[id: number]: number|null}>{
+		const rows = await sql`SELECT tutor_study_programs.id, tutor_study_programs.tutors_wanted, COUNT(tutors.id) AS tutors FROM tutor_study_programs LEFT JOIN tutors ON tutor_study_programs.id=tutors.study_program GROUP BY tutor_study_programs.id`;
+		return rows.reduce((acc, row)=>({...acc, [row.id]: row.tutorsWanted ? row.tutorsWanted - row.tutors : null}), {});
+	}
+}
diff --git a/src/lib/server/database/migrations/0_init.sql b/src/lib/server/database/migrations/0_init.sql
index e6fbdacd6805ae35ff621d95398598006e0e9ceb..235288bae84ae09f1c72ea2b88b1456625432aea 100644
--- a/src/lib/server/database/migrations/0_init.sql
+++ b/src/lib/server/database/migrations/0_init.sql
@@ -1,4 +1,9 @@
 CREATE TABLE IF NOT EXISTS study_programs (
+	"id" SERIAL PRIMARY KEY,
+	"name" JSONB NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS tutor_study_programs (
 	"id" SERIAL PRIMARY KEY,
 	"name" JSONB NOT NULL,
 	"tutors_wanted" INT NOT NULL DEFAULT 0,
@@ -41,12 +46,14 @@ CREATE TABLE IF NOT EXISTS tutors (
 	"gender" TEXT NOT NULL,
 	"shirt_size" TEXT NOT NULL,
 	"dietary_restriction" TEXT NOT NULL DEFAULT '',
-	"study_program" INT NOT NULL REFERENCES study_programs(id),
+	"study_program" INT NOT NULL REFERENCES tutor_study_programs(id),
 	"training" INT DEFAULT NULL REFERENCES tutor_trainings(id),
 	"sent_training_mail" BOOLEAN NOT NULL DEFAULT FALSE,
 	"trained" BOOLEAN NOT NULL DEFAULT FALSE,
 	"co_tutor_wish" TEXT NOT NULL DEFAULT '',
 	"mentor" BOOLEAN NOT NULL DEFAULT FALSE,
+	"would_lead_bachelor" BOOLEAN NOT NULL DEFAULT FALSE,
+	"would_lead_master" BOOLEAN NOT NULL DEFAULT FALSE,
 	"tutorial" INT DEFAULT NULL REFERENCES tutorials(id),
 	"number" TEXT,
 	"notes" TEXT NOT NULL DEFAULT '',
@@ -61,8 +68,20 @@ CREATE TABLE IF NOT EXISTS former_tutors (
 	"trained_date" TEXT NOT NULL DEFAULT '',
 	"years" INT NOT NULL,
 	"last_year" TEXT NOT NULL,
-	"notes" TEXT,
-	"lists" TEXT[] NOT NULL DEFAULT '{}'
+	"notes" TEXT
+);
+
+CREATE TABLE IF NOT EXISTS tutor_lists (
+	"id" SERIAL PRIMARY KEY,
+	"name" TEXT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS tutor_list_entries (
+	"list" INT NOT NULL REFERENCES tutor_lists(id),
+	"email" TEXT NOT NULL,
+	"notes" TEXT NOT NULL DEFAULT '',
+	"on_list_since" TEXT NOT NULL,
+	PRIMARY KEY (list, email)
 );
 
 CREATE TABLE IF NOT EXISTS rally_station_supervisors (
@@ -151,3 +170,16 @@ CREATE TABLE  IF NOT EXISTS master_freshers (
 	"study_program" TEXT NOT NULL,
 	"aachen_experience" TEXT NOT NULL
 );
+
+CREATE TABLE IF NOT EXISTS mrx (
+	"id" SERIAL PRIMARY KEY,
+	"name" TEXT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS mrx_entries (
+	"id" SERIAL PRIMARY KEY,
+	"mrx" INT NOT NULL REFERENCES mrx(id),
+	"time" TIMESTAMP NOT NULL,
+	"text" TEXT,
+	"image" TEXT
+);
diff --git a/src/lib/server/paths.ts b/src/lib/server/paths.ts
index 3df4f8e65c3485535a5de04258b5de9c08e232ac..cb238fc0f04ddeab7bd72f6cb2deb1154e418154 100644
--- a/src/lib/server/paths.ts
+++ b/src/lib/server/paths.ts
@@ -9,7 +9,8 @@ export const imagesPath = path.join(publicPath, "images");
 export const uploadsPath = path.join(publicPath, "uploads");
 export const flyersPath = path.join(publicPath, "flyer");
 export const schedulesPath = path.join(publicPath, "stundenplaene");
+export const tutorFilesPath = path.join(publicPath, "tutorfiles");
 
-await Promise.all([scheduleFontsPath, imagesPath, uploadsPath, flyersPath, schedulesPath].map(path=>fs.mkdir(path, { recursive: true })));
+await Promise.all([scheduleFontsPath, imagesPath, uploadsPath, flyersPath, schedulesPath, tutorFilesPath].map(path=>fs.mkdir(path, { recursive: true })));
 
 export const configPath = path.join(basePath, "config.json");
diff --git a/src/routes/(non-admin)/tutor/+page.server.ts b/src/routes/(non-admin)/tutor/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..223bbb6144498816a8787560c9f5f3fc0e9b22fb
--- /dev/null
+++ b/src/routes/(non-admin)/tutor/+page.server.ts
@@ -0,0 +1,17 @@
+import { Config } from "$lib/server/config";
+import { TutorTraining } from "$lib/server/database/entities/TutorTraining.entity";
+import type { PageServerLoad } from "./$types";
+
+export const load: PageServerLoad = async () => {
+	const config = Config.get();
+	const tutorTrainings = await TutorTraining.getAll();
+	return {
+		fresherWeekStart: config?.fresherWeekStart,
+		fresherWeekEnd: config?.fresherWeekEnd,
+		registrationOpen: config?.tutorRegistrationOpen ?? false,
+		trainings: tutorTrainings.filter(t=>!t.internal).map(t=>({date: t.date, language: t.language})),
+		semester: config?.currentSemester,
+		trainingsStart: config?.trainingsStart,
+		trainingsEnd: config?.trainingsEnd,
+	};
+};
diff --git a/src/routes/(non-admin)/tutor/+page.svelte b/src/routes/(non-admin)/tutor/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..9abe1dd08d42926caefa6628ed1a6148a08305fd
--- /dev/null
+++ b/src/routes/(non-admin)/tutor/+page.svelte
@@ -0,0 +1,56 @@
+<script>
+	import TutorFaq from "$lib/components/TutorFAQ.svelte";
+	import { Button, P, Heading, List, Li, A } from "flowbite-svelte";
+	import { LL, locale } from "$lib/i18n/i18n";
+	import LocalizedText from "$lib/components/LocalizedText.svelte";
+	import H1 from "$lib/components/H1.svelte";
+	
+	let { data } = $props();
+	
+	let dateFormatter = $derived(new Intl.DateTimeFormat($locale, {day: "2-digit", month: "long", year: "numeric", weekday: "long"}));
+</script>
+
+<H1>{$LL.Tutor.Headline()}</H1>
+
+<P class="mb-3">{$LL.Tutor.Greeting()}</P>
+
+<P class="mb-6" whitespace="preline">{$LL.Tutor.Paragraph({semester: data.semester})}</P>
+
+<Heading tag="h4" class="mb-1">{$LL.Tutor.WhatTitle()}</Heading>
+<P class="mb-6" whitespace="preline">{$LL.Tutor.WhatContent()}</P>
+
+<Heading tag="h4" class="mb-1">{$LL.Tutor.NeedTitle()}</Heading>
+<P class="mb-6" whitespace="preline">{$LL.Tutor.NeedContent()}</P>
+
+<Heading tag="h4" class="mb-1">{$LL.Tutor.WhenTitle()}</Heading>
+<P class="mb-1" whitespace="preline">
+	{$LL.Tutor.WhenContent({fresherWeek: [new Date(data.fresherWeekStart), new Date(data.fresherWeekEnd)], start: data.trainingsStart, end: data.trainingsEnd})}
+</P>
+<List tag="ul" class="mb-6">
+	{#each data.trainings as training}
+	<Li class="text-gray-900 dark:text-white">{dateFormatter.format(new Date(training.date))} ({$LL.Navbar.Languages[training.language]()})</Li>
+	{:else}
+	<Li class="text-gray-900 dark:text-white">{$LL.Tutor.NoDates()}</Li>
+	{/each}
+</List>
+
+<P class="mb-6">
+	<LocalizedText key="Tutor.FurtherQuestions">
+		<A href="mailto:esa@fsmpi.rwth-aachen.de">esa@fsmpi.rwth-aachen.de</A>
+	</LocalizedText>
+</P>
+
+<P class="mb-6 whitespace-pre-line">{$LL.Tutor.ClosingFormula()}</P>
+
+<div class="flex mb-6 justify-center">
+	{#if data.registrationOpen}
+	<Button href="/tutor/register/" size="xl">
+		<svg aria-hidden="true" class="mr-2 -ml-1 w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
+		{$LL.Tutor.RegistrationButton()}
+	</Button>
+	{:else}
+	<Button disabled size="xl">{$LL.Tutor.RegistrationClosed()}</Button>
+	{/if}
+</div>
+
+<TutorFaq />
diff --git a/src/routes/(non-admin)/tutor/register/+page.server.ts b/src/routes/(non-admin)/tutor/register/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a093aec8d23cab66bb19ff33a2daba676f2aa6a1
--- /dev/null
+++ b/src/routes/(non-admin)/tutor/register/+page.server.ts
@@ -0,0 +1,178 @@
+import { error } from "@sveltejs/kit";
+import type { PageServerLoad } from "./$types.js";
+import { Tutor } from "$lib/server/database/entities/Tutor.entity.js";
+import { Config } from "$lib/server/config";
+import { TutorTraining } from "$lib/server/database/entities/TutorTraining.entity.js";
+import Gender from "$lib/genders.js";
+import Degree from "$lib/degrees.js";
+import { sendTrainingMail, sendTutorRegisteredMail } from "$lib/server/mail.js";
+import bcrypt from "bcrypt";
+import { TutorStudyProgram } from "$lib/server/database/entities/TutorStudyProgram.entity.js";
+
+export const load: PageServerLoad = async ()=>{
+	const config = Config.get();
+	const studyPrograms = await TutorStudyProgram.getAll();
+	const missingTutorCounts = await TutorStudyProgram.getMissingTutors();
+	const tutorTrainings = await TutorTraining.getAll();
+	return {
+		tutorTrainings: tutorTrainings.filter(t=>!t.internal).map(t=>({id: t.id, date: t.date, language: t.language, location: t.location, isFull: t.participants.length>=t.maxParticipants})),
+		registrationOpen: config.tutorRegistrationOpen,
+		shirtSizes: config.shirtSizes,
+		fresherWeekStart: config.fresherWeekStart,
+		studyPrograms: studyPrograms.map(s=>({id: s.id, name: s.name, hasWaitlist: missingTutorCounts[s.id] === null ? false : missingTutorCounts[s.id]! <= 0})),
+	};
+};
+
+export const actions = {
+	default: async (request)=>{
+		const config = Config.get();
+		if(!config.tutorRegistrationOpen){
+			throw error(403, "Die Tutoranmeldung ist derzeit geschlossen.");
+		}
+		const data = await request.request.formData();
+		const firstname = data.get("firstname");
+		const lastname = data.get("lastname");
+		const nickname = data.get("nickname") || "";
+		const birthdayString = data.get("birthday");
+		const email = data.get("email");
+		const phone = data.get("phone");
+		const shirtSize = data.get("shirt-size");
+		const address = data.get("address");
+		const zip = data.get("zip");
+		const city = data.get("city");
+		const gender = data.get("gender");
+		const dietaryRestriction = data.get("dietary-restriction");
+		const studyProgramId = Number(data.get("study-program"));
+		const degree = data.get("degree");
+		const wouldLeadBachelor = data.get("would-lead-bachelor") === "on";
+		const wouldLeadMaster = data.get("would-lead-master") === "on";
+		const trainingString = data.get("training");
+		const mentor = !!data.get("mentor");
+		const coTutorWish = data.get("co-tutor") || "";
+		if(!firstname || !lastname || !email || !phone || !shirtSize || !address || !gender || !degree || !birthdayString || !trainingString){
+			throw error(400, "Bitte fülle alle Pflichtfelder aus");
+		}
+		if(!Number.isInteger(studyProgramId) || studyProgramId < 0){
+			throw error(422, "Ungültiger Studiengang");
+		}
+		if(typeof email !== "string" || !email.includes("@") || !(email.endsWith("@rwth-aachen.de") || email.endsWith(".rwth-aachen.de"))){
+			throw error(422, "Ungültige E-Mail Adresse");
+		}
+		if(typeof birthdayString !== "string" || !birthdayString.match(/^\d{4}-\d{2}-\d{2}$/)){
+			throw error(422, "Ungültiger Geburtstag");
+		}
+		const birthday = Date.parse(birthdayString);
+		if(isNaN(birthday)){
+			throw error(422, "Geburtstag ungültig");
+		}
+		if(typeof dietaryRestriction !== "string"){ // is allowed to be empty, but not null
+			throw error(422, "Ungültige Essenseinschränkung");
+		}
+		const date = new Date(birthday);
+		date.setFullYear(date.getFullYear() + 18);
+		if(new Date(config.fresherWeekStart) < date){
+			throw error(422, "Du musst mindestens 18 Jahre alt sein");
+		}
+		if(typeof shirtSize !== "string" || !config.shirtSizes.includes(shirtSize)){
+			throw error(422, "Ungültige T-Shirt Größe");
+		}
+		if(typeof gender !== "string" || !(gender in Gender)){
+			throw error(422, "Ungültiges Geschlecht");
+		}
+		if(typeof degree !== "string" || !(degree in Degree)){
+			throw error(422, "Ungültiger Abschluss");
+		}
+		if(!wouldLeadBachelor && !wouldLeadMaster){
+			throw error(422, "Bitte gib an, ob du Bachelor- oder Master-Tutor:in werden möchtest");
+		}
+		if(typeof firstname !== "string" || typeof lastname !== "string" || typeof nickname !== "string" || typeof phone !== "string" || typeof address !== "string" || typeof zip !== "string" || typeof city !== "string" || typeof coTutorWish !== "string"){
+			throw error(422, "Ungültige Eingabe");
+		}
+		if(typeof trainingString !== "string"){
+			throw error(422, "Ungültige Schulung");
+		}
+		const trainingId = trainingString !== "-" ? Number(trainingString) : null;
+		if(trainingId !== null && (!Number.isInteger(trainingId) || trainingId < 0)){
+			throw error(422, "Ungültige Schulung");
+		}
+		const studyProgram = await TutorStudyProgram.getById(studyProgramId);
+		if(!studyProgram){
+			throw error(422, "Ungültiger Studiengang");
+		}
+		const training = trainingId !== null ? await TutorTraining.getById(trainingId) : null;
+		if(trainingId !== null && !training){
+			throw error(422, "Ungültige Schulung");
+		}
+		const waitlist = ((await TutorStudyProgram.getMissingTutors())[studyProgramId] ?? 1) <= 0;
+		const birthdate = new Date(birthday).toISOString().slice(0, 10);
+		const password = Math.random().toString(36).slice(2);
+		const passwordHash = await bcrypt.hash(password, 10);
+		const tutorId = await Tutor.create({
+			firstname,
+			lastname,
+			nickname,
+			birthday: birthdate,
+			email,
+			phone,
+			shirtSize,
+			address: `${address}, ${zip} ${city}`,
+			gender: gender as keyof typeof Gender,
+			studyProgram: studyProgramId,
+			degree: degree as keyof typeof Degree,
+			wouldLeadBachelor,
+			wouldLeadMaster,
+			training: trainingId,
+			mentor,
+			coTutorWish,
+			notes: "",
+			trained: false,
+			dietaryRestriction,
+			sentTrainingMail: false,
+			password: passwordHash,
+			// TODO add waitlist
+		});
+		const mockTutor: Tutor = {
+			id: tutorId,
+			firstname,
+			lastname,
+			nickname,
+			birthday: birthdate,
+			email,
+			phone,
+			shirtSize,
+			address,
+			gender: gender as keyof typeof Gender,
+			studyProgram,
+			degree: degree as keyof typeof Degree,
+			wouldLeadBachelor,
+			wouldLeadMaster,
+			training,
+			mentor,
+			coTutorWish,
+			notes: "",
+			trained: false,
+			dietaryRestriction,
+			sentTrainingMail: false,
+			password,
+		};
+		/*sendTutorRegisteredMail(mockTutor).then(()=>{ // no await, doesnt need to block response
+			if(training){
+				const { trainingMailReminderDays } = Config.get();
+				const trainingDate = Date.parse(training.date);
+				const now = Date.now();
+				if(trainingDate < now) return;
+				const daysUntilTraining = (trainingDate - now) / 1000 / 60 / 60 / 24;
+				if(daysUntilTraining <= trainingMailReminderDays){
+					return sendTrainingMail(mockTutor);
+				}
+			}
+		}).catch(err=>{
+			console.error(err);
+		});*/
+		return {
+			success: true,
+			tutorId,
+			waitlist,
+		};
+	},
+};
diff --git a/src/routes/(non-admin)/tutor/register/+page.svelte b/src/routes/(non-admin)/tutor/register/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..760517df727a5922e4bbff31de6fb421b4953491
--- /dev/null
+++ b/src/routes/(non-admin)/tutor/register/+page.svelte
@@ -0,0 +1,203 @@
+<script lang="ts">
+	import { enhance } from "$app/forms";
+	import { Select, Label, Button, Helper, Checkbox, A, Alert, Input } from "flowbite-svelte";
+	import { LL, locale } from "$lib/i18n/i18n";
+	import Gender from "$lib/genders";
+	import Degree from "$lib/degrees";
+	import LocalizedText from "$lib/components/LocalizedText.svelte";
+	import { addMessage } from "$lib/messages.js";
+	import H1 from "$lib/components/H1.svelte";
+	
+	let { data } = $props();
+	
+	let birthdayError = $state(false);
+	let ageError = $state(false);
+	let emailError = $state(-1);
+	
+	let success = $state(false);
+	let error: {message: string}|undefined = $state();
+	let waitlist: boolean = $state(false);
+	
+	let studyProgramId: number|undefined = $state(undefined);
+	let studyProgram = $derived(data.studyPrograms.find(sp=>sp.id===studyProgramId));
+	
+	let wouldLeadBachelor = $state(false);
+	
+	function validateEmail(email: any) {
+		if(typeof email !== "string" || !(email.endsWith("@rwth-aachen.de") || email.endsWith(".rwth-aachen.de"))){
+			emailError = 1;
+		}else{
+			if(email.match(/^[a-z]{2}\d{6}@rwth-aachen\.de$/i)) emailError = 0;
+			else emailError = -1;
+		}
+		return emailError;
+	}
+	function validateBirthday(date: any){
+		const birthday = Date.parse(date);
+		birthdayError = isNaN(birthday);
+		ageError = false;
+		if(!birthdayError){
+			let date = new Date(birthday);
+			date.setFullYear(date.getFullYear()+18);
+			ageError = new Date(data.fresherWeekStart) < date;
+		}
+		return !birthdayError && !ageError;
+	}
+	
+	function isTrainingInFuture(trainingDate: string){
+		const date = Date.parse(trainingDate);
+		const now = Date.now();
+		return now + 1000 * 60 * 60 * 24 < date; // can't sign up the day before and later
+	}
+</script>
+
+<H1>{$LL.Tutor.SignUp.Headline()}</H1>
+
+{#if data.registrationOpen}
+{#if success}
+{#if waitlist}
+<!-- TODO i18n and proper message -->
+<Alert border color="yellow" class="mb-6">Du wurdest zur Warteliste hinzugefügt</Alert>
+{:else}
+<Alert border color="green" class="mb-6">{$LL.Tutor.SignUp.SignedUpSuccessfully()}</Alert>
+{/if}
+{/if}
+{#if error?.message}
+<Alert border color="red" class="mb-6">{error.message}</Alert>
+{/if}
+
+<form method="post" use:enhance={({formElement, formData, cancel})=>{
+	if(!validateBirthday(formData.get("birthday")) || validateEmail(formData.get("email")) >= 0) {
+		addMessage({ type: "error", text: $LL.Tutor.SignUp.FormValidationErrors.InvalidForm() });
+		return cancel();
+	}
+	return ({result})=>{
+		if(result.type === "success" && result.data?.success) {
+			waitlist = !!result.data.waitlist;
+			success = true;
+			error = undefined;
+			formElement.reset();
+			window.scrollTo({top: 0, behavior: "smooth"});
+		}else if(result.type === "error"){
+			error = result.error;
+			window.scrollTo({top: 0, behavior: "smooth"});
+		}
+	};
+}}>
+	<div class="lg:flex md:gap-2">
+		<Label class="mb-2 w-full">
+			{$LL.Tutor.SignUp.FirstName()}
+			<Input type="text" name="firstname" required />
+		</Label>
+		<Label class="mb-2 w-full">
+			{$LL.Tutor.SignUp.LastName()}
+			<Input type="text" name="lastname" required />
+		</Label>
+	</div>
+	<Label class="mb-2">
+		{$LL.Tutor.SignUp.NickName()}
+		<Input type="text" name="nickname" />
+	</Label>
+	<Label class="mb-2">
+		{$LL.Tutor.SignUp.BirthDate()}
+		<div>
+			<Input type="date" name="birthday" required on:change={(e)=>validateBirthday((e.target! as HTMLInputElement).value)} />
+			{#if birthdayError}
+			<Helper color="red">{$LL.Tutor.SignUp.InvalidDate()}</Helper>
+			{:else if ageError}
+			<Helper color="red">{$LL.Tutor.SignUp.NotOldEnough()}</Helper>
+			{/if}
+		</div>
+	</Label>
+	<Label class="mb-2">
+		{$LL.Tutor.SignUp.Email()}
+		<Input type="email" name="email" required color={emailError>=0?"red":"base"} on:blur={e=>validateEmail((e.target! as HTMLInputElement).value)} />
+		{#if emailError>=0}
+		<Helper color="red">{$LL.Tutor.SignUp.FormValidationErrors.InvalidEmail[emailError]()}</Helper>
+		{/if}
+	</Label>
+	<Label class="mb-2">
+		{$LL.Tutor.SignUp.Phone()}
+		<Input type="tel" name="phone" required />
+		<Helper>{$LL.Tutor.SignUp.PhoneHelper()}</Helper>
+	</Label>
+	<div class="md:flex md:gap-2">
+		<Label class="mb-2 md:w-full">
+			{$LL.Tutor.SignUp.Address()}
+			<Input name="address" required />
+		</Label>
+		<div class="xs:flex xs:gap-2 w-full">
+			<Label class="mb-2 w-full xs:w-40">
+				{$LL.Tutor.SignUp.Zip()}
+				<Input name="zip" required />
+			</Label>
+			<Label class="mb-2 w-full md:w-full">
+				{$LL.Tutor.SignUp.City()}
+				<Input name="city" required />
+			</Label>
+		</div>
+	</div>
+	<Label class="mb-2">
+		{$LL.Tutor.SignUp.Gender()}
+		<Select name="gender" required placeholder={$LL.Tutor.SignUp.PleaseChoose()} items={Object.entries(Gender).map(([value, name])=>({value, name: name[$locale]}))} />
+	</Label>
+	<Label class="mb-2">
+		{$LL.Tutor.SignUp.ShirtSize()}
+		<Select name="shirt-size" required placeholder={$LL.Tutor.SignUp.PleaseChoose()} items={data.shirtSizes.map(x=>({name:x,value:x}))} />
+		<Helper>{$LL.Tutor.SignUp.ShirtSizeHelper()}</Helper>
+	</Label>
+	<Label class="mb-2">
+		{$LL.Tutor.SignUp.DietaryRestriction()}
+		<Input type="text" name="dietary-restriction" />
+		<Helper>{$LL.Tutor.SignUp.DietaryRestrictionHelper()}</Helper>
+	</Label>
+	<div class="lg:flex lg:gap-2">
+		<Label class="mb-2 w-full">
+			{$LL.Tutor.SignUp.StudyProgram()}
+			<Select name="study-program" required placeholder={$LL.Tutor.SignUp.PleaseChoose()} items={data.studyPrograms.map(x=>({name: x.name[$locale], value: x.id}))} bind:value={studyProgramId} />
+			{#if studyProgram?.hasWaitlist}
+			<Helper color="red">{$LL.Tutor.SignUp.StudyProgramWaitlist({studyProgram: studyProgram!.name[$locale]??""})}</Helper>
+			{/if}
+		</Label>
+		<Label class="mb-2 w-full">
+			{$LL.Tutor.SignUp.Degree()}
+			<Select name="degree" required placeholder={$LL.Tutor.SignUp.PleaseChoose()} items={Object.entries(Degree).map(([value, name])=>({value, name: name[$locale]}))} />
+			<Helper>{$LL.Tutor.SignUp.DegreeHelper()}</Helper>
+		</Label>
+	</div>
+	<div class="mb-2">
+		<Checkbox name="would-lead-bachelor" class="mb-1" bind:checked={wouldLeadBachelor}>{$LL.Tutor.SignUp.WouldLeadBachelor()}</Checkbox>
+		<Checkbox name="would-lead-master" required={!wouldLeadBachelor}>{$LL.Tutor.SignUp.WouldLeadMaster()}</Checkbox>
+	</div>
+	<Label class="mb-2">
+		{$LL.Tutor.SignUp.Training()}
+		<Select name="training" required placeholder={$LL.Tutor.SignUp.PleaseChoose()}>
+			{#each data.tutorTrainings as t}
+			{@const date = new Date(t.date)}
+			{@const language = $LL.Navbar.Languages[t.language]()}
+			{@const translationFunction = t.isFull ? $LL.Tutor.SignUp.TrainingOptionFull : $LL.Tutor.SignUp.TrainingOption}
+			<option value={t.date} disabled={!isTrainingInFuture(t.date) || t.isFull}>{translationFunction({date, language})}</option>
+			{/each}
+			<option value="-">{$LL.Tutor.SignUp.AlreadyTrained()}</option>
+		</Select>
+	</Label>
+	<Label class="mb-2">
+		{$LL.Tutor.SignUp.CoTutorWish()}
+		<Input name="co-tutor" type="text" />
+		<Helper>{$LL.Tutor.SignUp.CoTutorWishHelper()}</Helper>
+	</Label>
+	<Checkbox class="mb-2" name="mentor">{$LL.Tutor.SignUp.IsMentor()}</Checkbox>
+	<Checkbox class="mb-2" required>
+		<LocalizedText key="Tutor.SignUp.ReadPrivacyPolicy">
+			<A class="mx-1" href="/datenschutz" target="_blank">{$LL.Tutor.SignUp.PrivacyPolicy()}</A>
+		</LocalizedText>
+	</Checkbox>
+	<Button type="submit" class="mb-2">{$LL.Tutor.SignUp.Submit()}</Button>
+</form>
+{:else}
+<Alert border color="red">
+	<LocalizedText key="Tutor.SignUp.SignUpClosed">
+		<A href="mailto:esa@fsmpi.rwth-aachen.de">esa@fsmpi.rwth-aachen.de</A>
+	</LocalizedText>
+</Alert>
+{/if}
diff --git a/src/routes/admin/studyprogram/+page.server.ts b/src/routes/admin/studyprogram/+page.server.ts
index f7875114f921d18bcc92f2565dc237d894e2da35..6414ae1452fe100b4d19cddaaca853a7d90b4f92 100644
--- a/src/routes/admin/studyprogram/+page.server.ts
+++ b/src/routes/admin/studyprogram/+page.server.ts
@@ -23,12 +23,7 @@ export const actions = {
 			if(typeof n !== "string" || !n) error(400, `Invalid name for locale ${locale}`);
 			name[locale] = n;
 		}
-		const tutorsWanted = Number(data.get("tutorsWanted") ?? undefined);
-		if(!Number.isInteger(tutorsWanted) || tutorsWanted < 0) error(400, "Invalid tutors wanted");
-		const tutorialNamesString = data.get("tutorialNames");
-		if(!tutorialNamesString || typeof tutorialNamesString !== "string") error(400, "Invalid tutorial names");
-		const tutorialNames = tutorialNamesString.split(";").map(s=>s.trim()).filter(s=>s);
-		await StudyProgram.update({id, name, tutorsWanted, tutorialNames});
+		await StudyProgram.update({id, name});
 	},
 	async createStudyProgram(event){
 		if(!event.locals.user.permissions.has(Permission.UPDATE_STUDY_PROGRAMS)) error(403, "Insufficient permissions");
@@ -39,11 +34,6 @@ export const actions = {
 			if(typeof n !== "string" || !n) error(400, `Invalid name for locale ${locale}`);
 			name[locale] = n;
 		}
-		const tutorsWanted = Number(data.get("tutorsWanted"));
-		if(!Number.isInteger(tutorsWanted) || tutorsWanted < 0) error(400, "Invalid tutors wanted");
-		const tutorialNamesString = data.get("tutorialNames");
-		if(!tutorialNamesString || typeof tutorialNamesString !== "string") error(400, "Invalid tutorial names");
-		const tutorialNames = tutorialNamesString.split(";").map(s=>s.trim()).filter(s=>s);
-		await StudyProgram.create({name, tutorsWanted, tutorialNames});
+		await StudyProgram.create({name});
 	},
 };
diff --git a/src/routes/admin/studyprogram/+page.svelte b/src/routes/admin/studyprogram/+page.svelte
index c5dc601ba10afc520aee0c9c9ce2d46adfb3024c..1a7161bfde8bfcfe156ee3e49e9c832ff7a4673b 100644
--- a/src/routes/admin/studyprogram/+page.svelte
+++ b/src/routes/admin/studyprogram/+page.svelte
@@ -26,7 +26,7 @@
 </Breadcrumb>
 
 {#each data.studyPrograms as studyProgram (studyProgram.id)}
-<form method="post" action="?/updateStudyProgram" id={String(studyProgram.id)} use:enhance={()=>({result, update})=>{
+<form method="post" action="?/updateStudyProgram" id="sp-{studyProgram.id}" use:enhance={()=>({result, update})=>{
 	if(result.type === "success"){
 		addMessage({type: "success", text: "Studiengang gespeichert"});
 		update({ reset: false });
@@ -38,7 +38,7 @@
 	<input type="hidden" name="id" value={studyProgram.id} />
 </form>
 {/each}
-<form method="post" action="?/createStudyProgram" id="new" use:enhance={()=>({result, update})=>{
+<form method="post" action="?/createStudyProgram" id="sp-new" use:enhance={()=>({result, update})=>{
 	if(result.type === "success"){
 		addMessage({type: "success", text: "Studiengang erstellt"});
 		update();
@@ -51,59 +51,39 @@
 <Table>
 	<TableHead>
 		<TableHeadCell>Name</TableHeadCell>
-		<TableHeadCell>gewünschte Tutorenzahl</TableHeadCell>
 		<TableHeadCell></TableHeadCell>
 	</TableHead>
 	<TableBody>
 		{#each data.studyPrograms	as studyProgram (studyProgram.id)}
+		{#key studyProgram.id}
 		<TableBodyRow>
 			<TableBodyCell>
 				{#each locales as lang}
 				<Label>
 					Name ({lang})
-					<Input required name="name[{lang}]" value={studyProgram.name[lang]} form={studyProgram.id} />
+					<Input required name="name[{lang}]" value={studyProgram.name[lang]} form="sp-{studyProgram.id}" />
 				</Label>
 				{/each}
 			</TableBodyCell>
 			<TableBodyCell>
-				<Label>
-					gewünschte Tutorenzahl
-					<Input required type="number" name="tutorsWanted" min={0} value={studyProgram.tutorsWanted} form={studyProgram.id} />
-				</Label>
-				<Label>
-					Tutoriennamen (mit Semikolon getrennt)
-					<Input required name="tutorialNames" value={studyProgram.tutorialNames.join(';')} form={studyProgram.id} />
-				</Label>
-			</TableBodyCell>
-			<TableBodyCell>
-				<Button type="submit" form={studyProgram.id}>Speichern</Button>
+				<Button type="submit" form="sp-{studyProgram.id}">Speichern</Button>
 				<Button color="red" on:click={()=>confirmStudyProgramDelete(studyProgram)}>Löschen</Button>
 			</TableBodyCell>
 		</TableBodyRow>
+		{/key}
 		{/each}
 		{#key "new"}
-		
 		<TableBodyRow>
 			<TableBodyCell>
 				{#each locales as lang}
 				<Label>
 					Name ({lang})
-					<Input required name="name[{lang}]" form="new" />
+					<Input required name="name[{lang}]" form="sp-new" />
 				</Label>
 				{/each}
 			</TableBodyCell>
 			<TableBodyCell>
-				<Label>
-					gewünschte Tutorenzahl
-					<Input required type="number" name="tutorsWanted" min={0} form="new" />
-				</Label>
-				<Label>
-					Tutoriennamen (mit Semikolon getrennt)
-					<Input required name="tutorialNames" form="new" />
-				</Label>
-			</TableBodyCell>
-			<TableBodyCell>
-				<Button type="submit" form="new">Erstellen</Button>
+				<Button type="submit" form="sp-new">Erstellen</Button>
 			</TableBodyCell>
 		</TableBodyRow>
 		{/key}
diff --git a/src/routes/admin/tutor/+page.server.ts b/src/routes/admin/tutor/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..72f82f9c4378b1d5ce2877c8d66eb0f221144912
--- /dev/null
+++ b/src/routes/admin/tutor/+page.server.ts
@@ -0,0 +1,18 @@
+import { Permission } from "$lib/perms";
+import { Config } from "$lib/server/config";
+import { TutorStudyProgram } from "$lib/server/database/entities/TutorStudyProgram.entity";
+import { Tutor } from "$lib/server/database/entities/Tutor.entity";
+import { pick } from "$lib/utils";
+import type { PageServerLoad } from "./$types";
+
+export const load: PageServerLoad = async event => {
+	let tutors = await Tutor.getAll();
+	const shirtSizes = Config.get().shirtSizes.map(s=>[s, tutors.filter(t=>t.shirtSize===s).length] as const);
+	if(!event.locals.user.permissions.has(Permission.VIEW_TUTOR_DETAILS)) tutors = tutors.map(t=>pick(t, ["id", "firstname", "lastname", "nickname", "studyProgram", "degree", "wouldLeadBachelor", "wouldLeadMaster", "training", "notes"]));
+	const studyPrograms = await TutorStudyProgram.getAll();
+	return {
+		tutors: event.locals.user.permissions.has(Permission.VIEW_TUTORS) ? tutors.map(t=>({...t, studyProgram: {...t.studyProgram}, training: t.training ? {...t.training} : null })) : [],
+		studyPrograms: studyPrograms.map(p=>({...p, tutorCount: tutors.filter(t=>t.studyProgram.id===p.id).length})),
+		shirtSizes,
+	};
+};
diff --git a/src/routes/admin/tutor/+page.svelte b/src/routes/admin/tutor/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..14f2f73a72ec571623ce81c8e1de6bc7393d52d4
--- /dev/null
+++ b/src/routes/admin/tutor/+page.svelte
@@ -0,0 +1,185 @@
+<script lang="ts">
+	import { Breadcrumb, BreadcrumbItem, Button, Heading, Label, Modal, P, Radio, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte";
+	import { locale } from "$lib/i18n/i18n";
+	import { Permission } from "$lib/perms.js";
+	import { toCSV } from "$lib/csv";
+	import type { Tutor } from "$lib/server/database/entities/Tutor.entity";
+	import SortableTable from "$lib/components/SortableTable.svelte";
+	import SortableTableHeadCell from "$lib/components/SortableTableHeadCell.svelte";
+	
+	export let data;
+	
+	$: dateFormatter = new Intl.DateTimeFormat($locale, { day: "2-digit", month: "2-digit", year: "numeric" });
+	
+	let showExportModal = false;
+	let exportType = "json";
+	const fields: { prop: (t: Tutor)=>unknown, label: string, value: string }[] = [
+		{ prop: t=>t.firstname, label: "Vorname", value: "firstname" },
+		{ prop: t=>t.lastname, label: "Nachname", value: "lastname" },
+		{ prop: t=>t.nickname, label: "Spitzname", value: "nickname" },
+		{ prop: t=>t.studyProgram.name.de, label: "Fach", value: "studyProgram" },
+		{ prop: t=>t.training?.date, label: "Schulung", value: "training" },
+		{ prop: t=>t.notes, label: "Notiz", value: "notes" },
+		...(data.user.permissions.has(Permission.VIEW_TUTOR_DETAILS) ? [
+			{ prop: t=>t.gender, label: "Geschlecht", value: "gender" },
+			{ prop: t=>t.mentor, label: "Mentor", value: "mentor" },
+			{ prop: t=>t.shirtSize, label: "Shirt", value: "shirtSize" },
+			{ prop: t=>t.coTutorWish, label: "Co-Tutor", value: "coTutorWish" },
+			{ prop: t=>t.trained, label: "Geschult", value: "trained" },
+			{ prop: t=>t.degree, label: "Abschluss", value: "degree" },
+			{ prop: t=>t.email, label: "E-Mail", value: "email" },
+			{ prop: t=>t.phone, label: "Telefon", value: "phone" },
+			{ prop: t=>t.address, label: "Adresse", value: "address" },
+			{ prop: t=>t.birthday, label: "Geburtstag", value: "birthday" },
+			{ prop: t=>t.dietaryRestriction, label: "Essgewohnheiten", value: "dietaryRestriction" },
+		] satisfies { prop: (t: Tutor)=>unknown, label: string, value: string }[] : []),
+	];
+	let selectedFields: string[] = fields.map(f => f.value);
+	function exportTutors(){
+		const tutors = data.tutors.map(t=>{
+			const tutor: Record<string, unknown> = {};
+			for(const field of fields){
+				if(selectedFields.includes(field.value)){
+					tutor[field.value] = field.prop(t);
+				}
+			}
+			return tutor;
+		});
+		const parsed = exportType === "json" ? JSON.stringify(tutors, null, 2) : toCSV(tutors, selectedFields);
+		const blob = new Blob([parsed], { type: exportType === "json" ? "application/json" : "text/csv" });
+		const url = URL.createObjectURL(blob);
+		Object.assign(document.createElement("a"), {
+			href: url,
+			download: `tutors.${exportType}`,
+		}).click();
+		URL.revokeObjectURL(url);
+	}
+	
+	let showMatchModal = false;
+	function matchTutors(){
+		
+	}
+	function matchRemainingTutors(){
+		
+	}
+</script>
+
+<Breadcrumb class="mb-4">
+	<BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem>
+	<BreadcrumbItem href="/admin/tutor">Tutoren</BreadcrumbItem>
+</Breadcrumb>
+
+{#if data.user.permissions.has(Permission.VIEW_TUTORS)}
+<Modal bind:open={showExportModal} autoclose outsideclose title="Tutoren exportieren" classBody="space-y-1">
+	<Heading tag="h2" customSize="text-2xl font-bold">Format</Heading>
+	<Radio bind:group={exportType} value="json">JSON</Radio>
+	<Radio bind:group={exportType} value="csv">CSV</Radio>
+	<Heading tag="h2" customSize="text-2xl font-bold">Felder</Heading>
+	{#each fields as field}
+	<Label class="text-sm rtl:text-right font-medium text-gray-900 dark:text-gray-300 flex items-center">
+		<input type="checkbox" value={field.value} bind:group={selectedFields} class="w-4 h-4 bg-gray-100 border-gray-300 dark:ring-offset-gray-800 focus:ring-2 me-2 dark:bg-gray-600 dark:border-gray-500 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600" />
+		{field.label}
+	</Label>
+	{/each}
+	<svelte:fragment slot="footer">
+		<Button on:click={exportTutors}>Exportieren</Button>
+		<Button color="light">Abbrechen</Button>
+	</svelte:fragment>
+</Modal>
+{#if data.user.permissions.has(Permission.UPDATE_TUTOR)}
+<Modal bind:open={showMatchModal} autoclose outsideclose title="Co-Tutoren automatisch zuweisen" classBody="space-y-1">
+	<Heading tag="h2" customSize="text-2xl font-bold">Wünsche erkennen</Heading>
+	<P class="mb-2">
+		Hier können Co-Tutoren automatisch anhand ihrer Wünsche zugeordnet werden. Tutoren, die sich gegenseitig als Co-Tutor wünschen, werden einander zugeordnet. Wenn kein Wunsch erkannt wird oder der Wunsch nicht automatisch zugewiesen werden kann, wird der Tutor <i>nicht</i> zugeordnet.
+	</P>
+	<P class="mb-2" color="text-red-700 dark:text-red-500">
+		<b>Vorsicht:</b> Alle bisherigen Zuordnungen werden gelöscht und durch die automatischen Zuordnungen ersetzt.
+	</P>
+	<Heading tag="h2" customSize="text-2xl font-bold">Verbleibende Tutoren zuweisen</Heading>
+	<P class="mb-2">
+		Hier werden soweit möglich alle Tutoren, die noch keinem Tutorium zugeordnet sind, zufällig zugeordnet. Bitte prüfe vorher, dass alle Wünsche berücksichtigt sind.
+	</P>
+	<svelte:fragment slot="footer">
+		<Button on:click={matchTutors}>Wünsche erkennen</Button>
+		<Button on:click={matchRemainingTutors}>Verbleibende Tutoren zuweisen</Button>
+		<Button color="light">Abbrechen</Button>
+	</svelte:fragment>
+</Modal>
+{/if}
+
+<div class="flex gap-2 mb-3">
+	{#if data.user.permissions.has(Permission.UPDATE_TUTOR)}
+	<Button href="/admin/tutor/new">Neuer Tutor</Button>
+	<Button on:click={()=>showMatchModal=true}>Tutorien zuweisen</Button>
+	{/if}
+	<Button on:click={()=>showExportModal=true}>Exportieren</Button>
+</div>
+{/if}
+
+<Table class="mb-6">
+	<TableHead>
+		{#each data.studyPrograms as studyProgram}
+		<TableHeadCell>{studyProgram.name[$locale]}</TableHeadCell>
+		{/each}
+		<TableHeadCell>Gesamt</TableHeadCell>
+	</TableHead>
+	<TableBody>
+		<TableBodyRow>
+			{#each data.studyPrograms as studyProgram}
+			<TableBodyCell>{studyProgram.tutorCount}{#if studyProgram.tutorsWanted>0}{" von "}{studyProgram.tutorsWanted}{/if}</TableBodyCell>
+			{/each}
+			<TableBodyCell>{data.studyPrograms.map(p=>p.tutorCount).reduce((a,b)=>a+b, 0)}</TableBodyCell>
+		</TableBodyRow>
+	</TableBody>
+</Table>
+
+<Table class="mb-6">
+	<TableHead>
+		{#each data.shirtSizes as size}
+		<TableHeadCell>{size[0]}</TableHeadCell>
+		{/each}
+	</TableHead>
+	<TableBody>
+		<TableBodyRow>
+			{#each data.shirtSizes as size}
+			<TableBodyCell>{size[1]}</TableBodyCell>
+			{/each}
+		</TableBodyRow>
+	</TableBody>
+</Table>
+
+{#if data.user.permissions.has(Permission.VIEW_TUTORS)}
+<SortableTable class="mb-6" hoverable items={data.tutors}>
+	<TableHead>
+		<SortableTableHeadCell sort={(a: Tutor, b)=>a.firstname.localeCompare(b.firstname)}>Vorname</SortableTableHeadCell>
+		<SortableTableHeadCell sort={(a: Tutor, b)=>a.lastname.localeCompare(b.lastname)}>Nachname</SortableTableHeadCell>
+		<SortableTableHeadCell sort={(a: Tutor, b)=>a.studyProgram.name[$locale].localeCompare(b.studyProgram.name[$locale]) || a.degree.localeCompare(b.degree)}>Fach</SortableTableHeadCell>
+		<SortableTableHeadCell sort={(a: Tutor, b)=>a.coTutorWish.localeCompare(b.coTutorWish)}>Co-Tutor</SortableTableHeadCell>
+		<SortableTableHeadCell sort={(a: Tutor, b)=>{
+			if(a.training && b.training) return a.training.date.localeCompare(b.training.date);
+			else if(a.training) return -1;
+			else if(b.training) return 1;
+			else return 0;
+		}}>Schulung</SortableTableHeadCell>
+		<SortableTableHeadCell sort={(a: Tutor, b)=>a.notes.localeCompare(b.notes)}>Notiz</SortableTableHeadCell>
+		{#if data.user.permissions.has(Permission.UPDATE_TUTOR)}
+		<TableHeadCell></TableHeadCell>
+		{/if}
+	</TableHead>
+	{#snippet row({item: tutor})}
+	<TableBodyRow>
+		<TableBodyCell>{tutor.firstname}{tutor.nickname?` (${tutor.nickname})`:""}</TableBodyCell>
+		<TableBodyCell>{tutor.lastname}</TableBodyCell>
+		<TableBodyCell>{tutor.studyProgram.name[$locale]} {tutor.degree} {#if tutor.wouldLeadBachelor}{#if tutor.wouldLeadMaster}<span title="Würde ein Bachelor- oder Master-Tutorium übernehmen">(B/M)</span>{:else}<span title="Würde ein Bachelor-Tutorium übernehmen">(B)</span>{/if}{:else}<span title="Würde ein Master-Tutorium übernehmen">(M)</span>{/if}</TableBodyCell>
+		<TableBodyCell>{tutor.coTutorWish || ""}</TableBodyCell>
+		<TableBodyCell>{tutor.training?dateFormatter.format(new Date(tutor.training.date)):"Bereits geschult"}</TableBodyCell>
+		<TableBodyCell>{tutor.notes}</TableBodyCell>
+		{#if data.user.permissions.has(Permission.UPDATE_TUTOR)}
+		<TableBodyCell>
+			<Button href={`/admin/tutor/${tutor.id}`} color="light" size="xs">Bearbeiten</Button>
+		</TableBodyCell>
+		{/if}
+	</TableBodyRow>
+	{/snippet}
+</SortableTable>
+{/if}
diff --git a/src/routes/admin/tutor/[id=number]/+page.server.ts b/src/routes/admin/tutor/[id=number]/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d3fe8bb0a057b4a09e981dddec3d4c11cab11c03
--- /dev/null
+++ b/src/routes/admin/tutor/[id=number]/+page.server.ts
@@ -0,0 +1,90 @@
+import Degree from "$lib/degrees";
+import Gender from "$lib/genders.js";
+import { Config } from "$lib/server/config";
+import { TutorStudyProgram } from "$lib/server/database/entities/TutorStudyProgram.entity.js";
+import { Tutor } from "$lib/server/database/entities/Tutor.entity.js";
+import { TutorTraining } from "$lib/server/database/entities/TutorTraining.entity.js";
+import { error } from "@sveltejs/kit";
+
+export const load = async (request) => {
+	const id = Number(request.params.id);
+	if(isNaN(id)) error(400, "Bad Request");
+	const tutor = await Tutor.getById(id);
+	if(!tutor) error(404, "Not Found");
+	const trainings = await TutorTraining.getAll();
+	const studyPrograms = await TutorStudyProgram.getAll();
+	return {
+		tutor: {...tutor, studyProgram: {...tutor.studyProgram}, training: tutor.training ? {...tutor.training} : null},
+		trainings: trainings.map(t=>({...t, participants: t.participants.length})),
+		shirtSizes: Config.get().shirtSizes,
+		studyPrograms: studyPrograms.map(s=>({...s})),
+	};
+};
+
+export const actions = {
+	default: async (request) => {
+		const tutorId = parseInt(request.params.id);
+		const tutor = await Tutor.getById(tutorId);
+		if(!tutor) error(404, "Not Found");
+		const data = await request.request.formData();
+		const { shirtSizes } = Config.get();
+		const firstname = data.get("firstname");
+		const lastname = data.get("lastname");
+		const nickname = data.get("nickname");
+		const birthday = data.get("birthday");
+		const email = data.get("email");
+		const phone = data.get("phone");
+		const shirtSize = data.get("shirtSize");
+		const address = data.get("address");
+		const gender = data.get("gender");
+		const studyProgramString = data.get("studyProgram");
+		const degree = data.get("degree");
+		const trainingId = data.get("training");
+		const trained = data.get("trained") === "on";
+		const mentor = data.get("mentor") === "on";
+		const coTutorWish = data.get("coTutorWish");
+		const notes = data.get("notes");
+		if(!firstname || typeof firstname !== "string") error(400, "Invalid firstname");
+		if(!lastname || typeof lastname !== "string") error(400, "Invalid lastname");
+		if(nickname !== null && typeof nickname !== "string") error(400, "Invalid nickname");
+		if(!birthday || typeof birthday !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(birthday)) error(400, "Invalid birthday");
+		if(isNaN(Date.parse(birthday))) error(400, "Invalid birthday");
+		if(!email || typeof email !== "string" || !email.includes("@") || !(email.endsWith("@rwth-aachen.de") || email.endsWith(".rwth-aachen.de"))) error(400, "Invalid email");
+		if(!phone || typeof phone !== "string") error(400, "Invalid phone");
+		if(!shirtSize || typeof shirtSize !== "string" || !shirtSizes.includes(shirtSize)) error(400, "Invalid shirtSize");
+		if(!address || typeof address !== "string") error(400, "Invalid address");
+		if(typeof gender !== "string" || !(gender in Gender)) error(400, "Invalid gender");
+		if(typeof studyProgramString !== "string" || !/^[1-9]+\d*$/.test(studyProgramString)) error(400, "Invalid studyProgram");
+		const studyProgramId = Number(studyProgramString);
+		const studyProgram = await TutorStudyProgram.getById(studyProgramId);
+		if(!studyProgram) error(400, "Invalid studyProgram");
+		if(typeof degree !== "string" || !(degree in Degree)) error(400, "Invalid degree");
+		if(!trainingId || typeof trainingId !== "string" || !/^[1-9]+\d*$|^-$/.test(trainingId)) error(400, "Invalid training");
+		const training = trainingId === "-" ? null : await TutorTraining.getById(Number(trainingId));
+		if(trainingId !== "-" && !training) error(400, "Invalid training");
+		if(typeof coTutorWish !== "string") error(400, "Invalid coTutorWish");
+		if(typeof notes !== "string") error(400, "Invalid comment");
+		const sentTrainingMail = tutor.sentTrainingMail && tutor.training?.id === training?.id;
+		// eslint-disable-next-line no-unused-vars
+		const updatedTutor = await Tutor.update({
+			id: tutor.id,
+			firstname,
+			lastname,
+			nickname,
+			birthday,
+			email,
+			phone,
+			shirtSize,
+			address,
+			gender: gender as keyof typeof Gender,
+			studyProgram: studyProgram.id,
+			degree: degree as keyof typeof Degree,
+			training: training?.id ?? null,
+			trained,
+			mentor,
+			coTutorWish,
+			notes,
+			sentTrainingMail,
+		});
+	},
+};
diff --git a/src/routes/admin/tutor/[id=number]/+page.svelte b/src/routes/admin/tutor/[id=number]/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..738f68d242c49c84f100ca1b3ad929460448ca60
--- /dev/null
+++ b/src/routes/admin/tutor/[id=number]/+page.svelte
@@ -0,0 +1,88 @@
+<script lang="ts">
+	import { Breadcrumb, BreadcrumbItem, Button, Checkbox, Input, Label, Select, Textarea } from "flowbite-svelte";
+	import { locale } from "$lib/i18n/i18n";
+	import Gender from "$lib/genders";
+	import Degree from "$lib/degrees";
+	import { enhance } from "$app/forms";
+	import { addMessage } from "$lib/messages.js";
+	
+	let { data } = $props();
+	
+	let dateFormatter = $derived(new Intl.DateTimeFormat($locale, { day: "2-digit", month: "2-digit", year: "numeric" }));
+</script>
+
+<Breadcrumb class="mb-4">
+	<BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem>
+	<BreadcrumbItem href="/admin/tutor">Tutoren</BreadcrumbItem>
+	<BreadcrumbItem href="/admin/tutor/{data.tutor.id}">{data.tutor.firstname} {data.tutor.lastname}</BreadcrumbItem>
+</Breadcrumb>
+
+<form method="post" use:enhance={()=>({result, update})=>{
+	if(result.type === "success"){
+		addMessage({type: "success", text: "Tutor gespeichert"});
+		update({ reset: false });
+	}else{
+		addMessage({type: "error", text: "Fehler beim Speichern"});
+		console.error(result);
+	}
+}}>
+	<Label class="mb-2">
+		Vorname
+		<Input name="firstname" value={data.tutor.firstname} />
+	</Label>
+	<Label class="mb-2">
+		Nachname
+		<Input name="lastname" value={data.tutor.lastname} />
+	</Label>
+	<Label class="mb-2">
+		Rufname
+		<Input name="nickname" value={data.tutor.nickname} />
+	</Label>
+	<Label class="mb-2">
+		Geburtstag
+		<Input name="birthday" value={data.tutor.birthday} type="date" />
+	</Label>
+	<Label class="mb-2">
+		Handynummer
+		<Input name="phone" value={data.tutor.phone} />
+	</Label>
+	<Label class="mb-2">
+		E-Mail
+		<Input name="email" value={data.tutor.email} />
+	</Label>
+	<Label class="mb-2">
+		Adresse
+		<Input name="address" value={data.tutor.address} />
+	</Label>
+	<Label class="mb-2">
+		Geschlecht
+		<Select name="gender" value={data.tutor.gender} items={Object.entries(Gender).map(([value, name])=>({value, name: name[$locale]}))} />
+	</Label>
+	<Label class="mb-2">
+		T-Shirt-Größe
+		<Select name="shirtSize" value={data.tutor.shirtSize} items={data.shirtSizes.map(s=>({value:s,name:s}))} />
+	</Label>
+	<Label class="mb-2">
+		Studiengang
+		<Select name="studyProgram" value={data.tutor.studyProgram.id} items={data.studyPrograms.map(p=>({value:p.id,name:p.name[$locale]}))} />
+	</Label>
+	<Label class="mb-2">
+		Abschluss
+		<Select name="degree" value={data.tutor.degree} items={Object.entries(Degree).map(([value, name])=>({value, name: name[$locale]}))} />
+	</Label>
+	<Checkbox class="mb-2" name="trained" checked={data.tutor.trained}>Geschult</Checkbox>
+	<Label class="mb-2">
+		Schulung
+		<Select name="training" value={data.tutor.training?.date ?? "-"} items={[{name:"Bereits geschult",value:"-"}, ...data.trainings.map(t=>({value:t.date,name:`${dateFormatter.format(new Date(t.date))} (${t.language})`}))]} />
+	</Label>
+	<Label class="mb-2">
+		Co-Tutor-Wunsch
+		<Input name="coTutorWish" value={data.tutor.coTutorWish} />
+	</Label>
+	<Label class="mb-2">
+		Notiz
+		<Textarea name="notes" value={data.tutor.notes} />
+	</Label>
+	<Checkbox class="mb-2" name="mentor" checked={data.tutor.mentor}>Informatik Mentor</Checkbox>
+	<Button type="submit" color="primary">Speichern</Button>
+</form>
diff --git a/src/routes/admin/tutor/training/+page.server.ts b/src/routes/admin/tutor/training/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..07d962542d9b945a7aa9990ebf80e0aa0aef1196
--- /dev/null
+++ b/src/routes/admin/tutor/training/+page.server.ts
@@ -0,0 +1,9 @@
+import { TutorTraining } from '$lib/server/database/entities/TutorTraining.entity';
+import type { PageServerLoad } from './$types';
+
+export const load = (async () => {
+	const trainings = await TutorTraining.getAll();
+	return {
+		trainings: trainings.map(t => ({...t, participants: t.participants.length})).sort((a,b)=>a.date.localeCompare(b.date)),
+	};
+}) satisfies PageServerLoad;
diff --git a/src/routes/admin/tutor/training/+page.svelte b/src/routes/admin/tutor/training/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..5e80e64f2b0dc1f1fe4b17e68e900ce1a903fdb8
--- /dev/null
+++ b/src/routes/admin/tutor/training/+page.svelte
@@ -0,0 +1,50 @@
+<script lang="ts">
+	import LL, { locale } from "$lib/i18n/i18n";
+	import { Permission } from "$lib/perms";
+	import { Breadcrumb, BreadcrumbItem, Button, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte";
+	
+	let { data } = $props();
+</script>
+
+<Breadcrumb class="mb-4">
+	<BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem>
+	<BreadcrumbItem href="/admin/tutor">Tutoren</BreadcrumbItem>
+	<BreadcrumbItem href="/admin/tutor/training">Schulungen</BreadcrumbItem>
+</Breadcrumb>
+
+<Table>
+	<TableHead>
+		<TableHeadCell>Datum</TableHeadCell>
+		<TableHeadCell>Ort</TableHeadCell>
+		<TableHeadCell>Sprache</TableHeadCell>
+		<TableHeadCell>Anmeldungen</TableHeadCell>
+		<TableHeadCell>Anmerkung</TableHeadCell>
+		{#if data.user.permissions.has(Permission.EDIT_TRAININGS)}
+		<TableHeadCell></TableHeadCell>
+		{/if}
+	</TableHead>
+	<TableBody>
+		{#each data.trainings as training}
+		<TableBodyRow class={training.internal ? "text-gray-700 dark:text-gray-300" : null}>
+			<TableBodyCell>{new Intl.DateTimeFormat($locale, {year: "numeric", month: "2-digit", day: "2-digit"}).format(Date.parse(training.date))}</TableBodyCell>
+			<TableBodyCell>{training.location}</TableBodyCell>
+			<TableBodyCell>{$LL.Navbar.Languages[training.language]()}</TableBodyCell>
+			<TableBodyCell>{training.participants} / {training.maxParticipants}</TableBodyCell>
+			<TableBodyCell>{training.notes}</TableBodyCell>
+			{#if data.user.permissions.has(Permission.EDIT_TRAININGS)}
+			<TableBodyCell>
+				<Button href="/admin/tutor/training/{training.id}">Bearbeiten</Button>
+				<Button color="red">-</Button>
+			</TableBodyCell>
+			{/if}
+		</TableBodyRow>
+		{/each}
+		{#if data.user.permissions.has(Permission.EDIT_TRAININGS)}
+		<TableBodyRow>
+			<TableBodyCell colspan="5">
+				<Button href="/admin/tutor/training/new" class="w-20 block mx-auto">+</Button>
+			</TableBodyCell>
+		</TableBodyRow>
+		{/if}
+	</TableBody>
+</Table>
diff --git a/src/routes/admin/tutor/training/[id=number]/+page.server.ts b/src/routes/admin/tutor/training/[id=number]/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7b556afe9da51d6428368b5b9c4cb5056469c1a7
--- /dev/null
+++ b/src/routes/admin/tutor/training/[id=number]/+page.server.ts
@@ -0,0 +1,47 @@
+import { error } from '@sveltejs/kit';
+import type { PageServerLoad } from './$types';
+import { TutorTraining } from '$lib/server/database/entities/TutorTraining.entity';
+import { locales, type Locale } from '$lib/i18n/i18n';
+
+export const load = (async event => {
+	const id = Number(event.params.id);
+	if(!Number.isInteger(id) || id < 1) error(404, 'Not Found');
+	const training = await TutorTraining.getById(id);
+	if(!training) error(404, 'Not Found');
+	return {
+		training: {...training, participants: training.participants.length},
+	};
+}) satisfies PageServerLoad;
+
+export const actions = {
+	update: async event => {
+		const id = Number(event.params.id);
+		if(!Number.isInteger(id) || id < 1) error(404, 'Not Found');
+		const training = await TutorTraining.getById(id);
+		if(!training) error(404, 'Not Found');
+		const data = await event.request.formData();
+		const dateStr = data.get("date");
+		const location = data.get("location");
+		const language = data.get("language");
+		const maxParticipantsStr = data.get("maxParticipants");
+		const notes = data.get("notes");
+		const internal = data.get("internal") === "on";
+		if(typeof dateStr !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) error(400, 'Invalid date');
+		const dateNum = Date.parse(dateStr);
+		if(isNaN(dateNum)) error(400, 'Invalid date');
+		if(typeof location !== "string" || location.length < 1) error(400, 'Invalid location');
+		if(typeof language !== "string" || !locales.includes(language as any)) error(400, 'Invalid language');
+		if(typeof maxParticipantsStr !== "string" || !/^\d+$/.test(maxParticipantsStr)) error(400, 'Invalid maxParticipants');
+		const maxParticipants = Number(maxParticipantsStr);
+		if(!Number.isInteger(maxParticipants) || maxParticipants < 1) error(400, 'Invalid maxParticipants');
+		if(typeof notes !== "string") error(400, 'Invalid notes');
+		await TutorTraining.update(id, {
+			date: dateStr,
+			location,
+			language: language as Locale,
+			maxParticipants,
+			notes,
+			internal,
+		});
+	},
+};
diff --git a/src/routes/admin/tutor/training/[id=number]/+page.svelte b/src/routes/admin/tutor/training/[id=number]/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..af4108c91331e353636fd293034f676ad2c46e03
--- /dev/null
+++ b/src/routes/admin/tutor/training/[id=number]/+page.svelte
@@ -0,0 +1,46 @@
+<script lang="ts">
+	import { enhance } from '$app/forms';
+	import { goto } from '$app/navigation';
+    import LL, { locales } from '$lib/i18n/i18n.js';
+	import { addMessage } from '$lib/messages.js';
+	import { Label, Textarea, Checkbox, Button, Input, Select } from 'flowbite-svelte';
+	
+	let { data } = $props();
+</script>
+
+<form action="?/update" method="post" use:enhance={()=>({result})=>{
+	if(result.type === "success"){
+		addMessage({type: "success", text: "Schulung gespeichert"});
+		goto("/admin/tutor/training");
+	}else{
+		addMessage({type: "error", text: "Fehler beim Speichern"});
+		console.error(result);
+	}
+}}>
+	<Label class="mb-2">
+		Datum
+		<Input type="date" name="date" value={data.training.date} required />
+	</Label>
+	<Label class="mb-2">
+		Ort
+		<Input type="text" name="location" value={data.training.location} required />
+	</Label>
+	<Label class="mb-2">
+		Sprache
+		<Select name="language" value={data.training.language} required>
+			{#each locales as locale}
+				<option value={locale}>{$LL.Navbar.Languages[locale]()}</option>
+			{/each}
+		</Select>
+	</Label>
+	<Label class="mb-2">
+		Maximale Teilnehmer
+		<Input type="number" name="maxParticipants" min="1" value={data.training.maxParticipants} required />
+	</Label>
+	<Label class="mb-2">
+		Anmerkung
+		<Textarea type="text" name="notes" value={data.training.notes} />
+	</Label>
+	<Checkbox name="internal" class="mb-2" value="on" checked={data.training.internal}>Intern (Tutoren sehen die Schulung nicht und können sich nicht selbst dafür anmelden)</Checkbox>
+	<Button type="submit" class="mb-2">Speichern</Button>
+</form>
diff --git a/src/routes/admin/tutor/training/new/+page.server.ts b/src/routes/admin/tutor/training/new/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..289d07e21e2fd78f477856db0e83856048a9ff83
--- /dev/null
+++ b/src/routes/admin/tutor/training/new/+page.server.ts
@@ -0,0 +1,27 @@
+import { locales, type Locale } from '$lib/i18n/i18n.js';
+import { Permission } from '$lib/perms';
+import { TutorTraining } from '$lib/server/database/entities/TutorTraining.entity.js';
+import { error } from '@sveltejs/kit';
+
+export const actions = {
+	create: async event => {
+		if(!event.locals.user.permissions.has(Permission.EDIT_TRAININGS)) error(403, "You don't have permission to create trainings");
+		const data = await event.request.formData();
+		const date = data.get('date');
+		const location = data.get('location');
+		const language = data.get('language');
+		const maxParticipantsRaw = data.get('maxParticipants');
+		const notes = data.get('notes');
+		const internal = data.get('internal');
+		if(typeof date !== "string" || !date.match(/^\d{4}-\d{2}-\d{2}$/) || isNaN(Date.parse(date))) error(400, "Invalid date");
+		if(!location || typeof location !== "string") error(400, "Invalid location");
+		if(typeof language !== "string" || !locales.includes(language as any)) error(400, "Invalid language");
+		if(!maxParticipantsRaw || typeof maxParticipantsRaw !== "string") error(400, "Invalid maxParticipants");
+		const maxParticipants = Number(maxParticipantsRaw);
+		if(!Number.isInteger(maxParticipants) || maxParticipants < 0) error(400, "Invalid maxParticipants");
+		if(typeof notes !== "string") error(400, "Invalid notes");
+		if(internal !== null && internal !== "on") error(400, "Invalid internal");
+		const id = await TutorTraining.create({ date, location, language: language as Locale, maxParticipants, notes, internal: internal === "on" });
+		return { id };
+	},
+};
diff --git a/src/routes/admin/tutor/training/new/+page.svelte b/src/routes/admin/tutor/training/new/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..9b5cef1a212935cf238613c84f213f7d481e387f
--- /dev/null
+++ b/src/routes/admin/tutor/training/new/+page.svelte
@@ -0,0 +1,51 @@
+<script lang="ts">
+	import { enhance } from "$app/forms";
+	import { goto } from "$app/navigation";
+    import LL, { locales } from "$lib/i18n/i18n";
+	import { addMessage } from "$lib/messages";
+	import { Breadcrumb, BreadcrumbItem, Button, Checkbox, Input, Label, Select, Textarea } from "flowbite-svelte";
+</script>
+
+<Breadcrumb class="mb-4">
+	<BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem>
+	<BreadcrumbItem href="/admin/tutor">Tutoren</BreadcrumbItem>
+	<BreadcrumbItem href="/admin/tutor/training">Schulungen</BreadcrumbItem>
+	<BreadcrumbItem href="/admin/tutor/training/new">Neue Schulung</BreadcrumbItem>
+</Breadcrumb>
+
+<form action="?/create" method="post" use:enhance={()=>({result})=>{
+	if(result.type === "success"){
+		addMessage({type: "success", text: "Schulung erstellt"});
+		goto("/admin/tutor/training");
+	}else{
+		addMessage({type: "error", text: "Fehler beim Erstellen"});
+		console.error(result);
+	}
+}}>
+	<Label class="mb-2">
+		Datum
+		<Input type="date" name="date" required />
+	</Label>
+	<Label class="mb-2">
+		Ort
+		<Input type="text" name="location" required />
+	</Label>
+	<Label class="mb-2">
+		Sprache
+		<Select name="language" required>
+			{#each locales as locale}
+				<option value={locale}>{$LL.Navbar.Languages[locale]()}</option>
+			{/each}
+		</Select>
+	</Label>
+	<Label class="mb-2">
+		Maximale Teilnehmer
+		<Input type="number" name="maxParticipants" min="1" required />
+	</Label>
+	<Label class="mb-2">
+		Anmerkung
+		<Textarea type="text" name="notes" />
+	</Label>
+	<Checkbox name="internal" class="mb-2" value="on">Intern (Tutoren sehen die Schulung nicht und können sich nicht selbst dafür anmelden)</Checkbox>
+	<Button type="submit" class="mb-2">Erstellen</Button>
+</form>