commit cabf2025cdc756c0345b3cead0906b7b902c3952
Author: Lucas Saburido <240425@MacBook-Pro-13.local>
Date: Wed May 13 16:26:45 2026 +0100
first commit
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..5d9ecf5
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+SUPABASE_URL=https://YOUR_PROJECT_ID.supabase.co
+SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6e151ed
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,51 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+/coverage/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
+
+# Signing / release secrets
+android/key.properties
+android/*.jks
+android/*.keystore
+ios/Runner/GoogleService-Info.plist
diff --git a/.metadata b/.metadata
new file mode 100644
index 0000000..79dc3aa
--- /dev/null
+++ b/.metadata
@@ -0,0 +1,45 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "00b0c91f06209d9e4a41f71b7a512d6eb3b9c694"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+ base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+ - platform: android
+ create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+ base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+ - platform: ios
+ create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+ base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+ - platform: linux
+ create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+ base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+ - platform: macos
+ create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+ base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+ - platform: web
+ create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+ base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+ - platform: windows
+ create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+ base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
new file mode 100644
index 0000000..5782487
--- /dev/null
+++ b/DEPLOYMENT.md
@@ -0,0 +1,87 @@
+# RIOTZ Deployment Guide (Android + iOS)
+
+This guide prepares RIOTZ for production releases with Flutter + Supabase.
+
+## 1) Environment setup
+
+Use `--dart-define` in release builds:
+
+```bash
+--dart-define=SUPABASE_URL=https://YOUR_PROJECT_ID.supabase.co
+--dart-define=SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
+--dart-define=ADMIN_USER_IDS=uuid-1,uuid-2
+```
+
+Optional: use `--dart-define-from-file=env.production.json`.
+
+Example `env.production.json`:
+
+```json
+{
+ "SUPABASE_URL": "https://YOUR_PROJECT_ID.supabase.co",
+ "SUPABASE_ANON_KEY": "YOUR_SUPABASE_ANON_KEY",
+ "ADMIN_USER_IDS": "uuid-1,uuid-2"
+}
+```
+
+## 2) Supabase production checklist
+
+- Apply migration in `supabase/migrations/20260506170000_riotz_production_schema.sql`.
+- Confirm buckets exist: `avatars`, `post-images`, `tracks`.
+- Confirm RLS policies are enabled and tested.
+- Add at least one admin user in `admin_users`.
+
+## 3) Android release
+
+### 3.1 Create upload keystore
+
+```bash
+keytool -genkey -v -keystore ~/riotz-upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
+```
+
+### 3.2 Configure `android/key.properties`
+
+Copy `android/key.properties.example` to `android/key.properties` and fill values.
+
+### 3.3 Build artifacts
+
+```bash
+flutter pub get
+flutter build appbundle --release --dart-define-from-file=env.production.json
+flutter build apk --release --dart-define-from-file=env.production.json
+```
+
+Output:
+- AAB: `build/app/outputs/bundle/release/app-release.aab`
+- APK: `build/app/outputs/flutter-apk/app-release.apk`
+
+## 4) iOS release
+
+### 4.1 Xcode signing
+
+- Open `ios/Runner.xcworkspace`
+- Target `Runner` -> Signing & Capabilities
+- Set Team, Bundle Identifier (`com.riotz.app`), and provisioning profiles
+
+### 4.2 Build and archive
+
+```bash
+flutter pub get
+flutter build ipa --release --dart-define-from-file=env.production.json
+```
+
+Or archive from Xcode and upload via Organizer / Transporter.
+
+## 5) Pre-release quality gate
+
+- `flutter analyze`
+- `flutter test`
+- Smoke test auth/profile/feed/music/discover/admin on physical devices
+- Verify upload, playback, logout/login, banned-user behavior
+- Verify crash-free startup with production Supabase keys
+
+## 6) Security reminders
+
+- Never commit `android/key.properties`, keystores, or private keys
+- Do not expose service-role Supabase keys in Flutter app
+- Rotate anon keys only through Supabase config if leaked
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..63a2749
--- /dev/null
+++ b/README.md
@@ -0,0 +1,38 @@
+# RIOTZ
+
+Underground social/music app starter built with Flutter, Supabase, Riverpod, and `go_router`.
+
+## Run
+
+```bash
+flutter pub get
+flutter run \
+ --dart-define=SUPABASE_URL=https://YOUR_PROJECT_ID.supabase.co \
+ --dart-define=SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
+```
+
+## Architecture
+
+```text
+lib/
+ app/
+ app.dart # MaterialApp wiring and global shell
+ core/
+ config/supabase_config.dart # Supabase bootstrap
+ router/ # Route constants + go_router config
+ supabase/supabase_providers.dart # Base Supabase Riverpod providers
+ theme/ # RIOTZ dark palette and theme
+ features/
+ auth/presentation/providers/ # Auth state providers
+ splash/presentation/pages/ # Entry page
+ home/presentation/pages/ # Main starter page
+ shared/widgets/riotz_shell.dart # Reusable branded page container
+```
+
+## Notes
+
+- `SupabaseConfig` reads credentials from `--dart-define` values.
+- Use `.env.example` as a template for local values.
+- Use `env.production.json.example` as a template for release `--dart-define-from-file`.
+- This scaffold is clean-architecture ready and feature-module friendly.
+- For production Android/iOS release setup, see `DEPLOYMENT.md`.
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..0d29021
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at https://dart.dev/lints.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..be3943c
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
new file mode 100644
index 0000000..af3fe9d
--- /dev/null
+++ b/android/app/build.gradle.kts
@@ -0,0 +1,68 @@
+import java.util.Properties
+
+plugins {
+ id("com.android.application")
+ id("kotlin-android")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+ namespace = "com.riotz.app"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ }
+
+ defaultConfig {
+ applicationId = "com.riotz.app"
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+ }
+
+ val keystoreProperties = Properties()
+ val keystorePropertiesFile = rootProject.file("key.properties")
+ if (keystorePropertiesFile.exists()) {
+ keystoreProperties.load(keystorePropertiesFile.inputStream())
+ }
+
+ signingConfigs {
+ create("release") {
+ if (keystorePropertiesFile.exists()) {
+ keyAlias = keystoreProperties["keyAlias"] as String?
+ keyPassword = keystoreProperties["keyPassword"] as String?
+ storeFile = file(keystoreProperties["storeFile"] as String)
+ storePassword = keystoreProperties["storePassword"] as String?
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ signingConfig = if (keystorePropertiesFile.exists()) {
+ signingConfigs.getByName("release")
+ } else {
+ signingConfigs.getByName("debug")
+ }
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro",
+ )
+ }
+ }
+}
+
+flutter {
+ source = "../.."
+}
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
new file mode 100644
index 0000000..bc5b4aa
--- /dev/null
+++ b/android/app/proguard-rules.pro
@@ -0,0 +1,10 @@
+# Keep Flutter and plugin registrant classes
+-keep class io.flutter.** { *; }
+-keep class io.flutter.plugins.** { *; }
+
+# Keep Supabase model reflection paths (safe default)
+-keep class com.supabase.** { *; }
+-dontwarn com.supabase.**
+
+# Keep Kotlin metadata
+-keep class kotlin.Metadata { *; }
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8685e2a
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/java/com/riotz/app/MainActivity.java b/android/app/src/main/java/com/riotz/app/MainActivity.java
new file mode 100644
index 0000000..7734b61
--- /dev/null
+++ b/android/app/src/main/java/com/riotz/app/MainActivity.java
@@ -0,0 +1,6 @@
+package com.riotz.app;
+
+import io.flutter.embedding.android.FlutterActivity;
+
+public class MainActivity extends FlutterActivity {
+}
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
new file mode 100644
index 0000000..dbee657
--- /dev/null
+++ b/android/build.gradle.kts
@@ -0,0 +1,24 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+val newBuildDir: Directory =
+ rootProject.layout.buildDirectory
+ .dir("../../build")
+ .get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+ project.evaluationDependsOn(":app")
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..fbee1d8
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,2 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e4ef43f
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
diff --git a/android/key.properties.example b/android/key.properties.example
new file mode 100644
index 0000000..b8a053e
--- /dev/null
+++ b/android/key.properties.example
@@ -0,0 +1,4 @@
+storeFile=/absolute/path/to/riotz-upload-keystore.jks
+storePassword=CHANGE_ME
+keyAlias=upload
+keyPassword=CHANGE_ME
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
new file mode 100644
index 0000000..ca7fe06
--- /dev/null
+++ b/android/settings.gradle.kts
@@ -0,0 +1,26 @@
+pluginManagement {
+ val flutterSdkPath =
+ run {
+ val properties = java.util.Properties()
+ file("local.properties").inputStream().use { properties.load(it) }
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+ flutterSdkPath
+ }
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "8.11.1" apply false
+ id("org.jetbrains.kotlin.android") version "2.2.20" apply false
+}
+
+include(":app")
diff --git a/backend/riotz-api/.env.example b/backend/riotz-api/.env.example
new file mode 100644
index 0000000..a543055
--- /dev/null
+++ b/backend/riotz-api/.env.example
@@ -0,0 +1,9 @@
+DB_URL=jdbc:postgresql://localhost:5432/riotz
+DB_USERNAME=postgres
+DB_PASSWORD=postgres
+JWT_ACCESS_SECRET=change-me-access
+JWT_REFRESH_SECRET=change-me-refresh
+JWT_ACCESS_EXPIRATION_MS=900000
+JWT_REFRESH_EXPIRATION_MS=604800000
+MEDIA_PROVIDER=s3
+MEDIA_BUCKET_NAME=riotz-media
diff --git a/backend/riotz-api/pom.xml b/backend/riotz-api/pom.xml
new file mode 100644
index 0000000..3a7ed7a
--- /dev/null
+++ b/backend/riotz-api/pom.xml
@@ -0,0 +1,69 @@
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.5
+
+
+
+ com.riotz
+ riotz-api
+ 0.0.1-SNAPSHOT
+ riotz-api
+ RIOTZ backend API
+
+
+ 21
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/RiotzApiApplication.java b/backend/riotz-api/src/main/java/com/riotz/api/RiotzApiApplication.java
new file mode 100644
index 0000000..f09ab40
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/RiotzApiApplication.java
@@ -0,0 +1,12 @@
+package com.riotz.api;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class RiotzApiApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(RiotzApiApplication.class, args);
+ }
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/admin/controller/AdminController.java b/backend/riotz-api/src/main/java/com/riotz/api/admin/controller/AdminController.java
new file mode 100644
index 0000000..90e0162
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/admin/controller/AdminController.java
@@ -0,0 +1,31 @@
+package com.riotz.api.admin.controller;
+
+import com.riotz.api.admin.service.AdminService;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/admin")
+public class AdminController {
+
+ private final AdminService adminService;
+
+ @PatchMapping("/users/{userId}/ban")
+ public ResponseEntity banUser(@PathVariable UUID userId) {
+ adminService.banUser(userId);
+ return ResponseEntity.noContent().build();
+ }
+
+ @DeleteMapping("/posts/{postId}")
+ public ResponseEntity deletePost(@PathVariable UUID postId) {
+ adminService.deletePost(postId);
+ return ResponseEntity.noContent().build();
+ }
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/admin/service/AdminService.java b/backend/riotz-api/src/main/java/com/riotz/api/admin/service/AdminService.java
new file mode 100644
index 0000000..24f7b74
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/admin/service/AdminService.java
@@ -0,0 +1,8 @@
+package com.riotz.api.admin.service;
+
+import java.util.UUID;
+
+public interface AdminService {
+ void banUser(UUID userId);
+ void deletePost(UUID postId);
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/admin/service/AdminServiceImpl.java b/backend/riotz-api/src/main/java/com/riotz/api/admin/service/AdminServiceImpl.java
new file mode 100644
index 0000000..a6f5ec5
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/admin/service/AdminServiceImpl.java
@@ -0,0 +1,33 @@
+package com.riotz.api.admin.service;
+
+import com.riotz.api.common.exception.ResourceNotFoundException;
+import com.riotz.api.post.repository.PostRepository;
+import com.riotz.api.user.repository.UserRepository;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class AdminServiceImpl implements AdminService {
+
+ private final UserRepository userRepository;
+ private final PostRepository postRepository;
+
+ @Override
+ @Transactional
+ public void banUser(UUID userId) {
+ final var user = userRepository.findById(userId)
+ .orElseThrow(() -> new ResourceNotFoundException("User not found"));
+ user.setBanned(true);
+ }
+
+ @Override
+ public void deletePost(UUID postId) {
+ if (!postRepository.existsById(postId)) {
+ throw new ResourceNotFoundException("Post not found");
+ }
+ postRepository.deleteById(postId);
+ }
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/auth/controller/AuthController.java b/backend/riotz-api/src/main/java/com/riotz/api/auth/controller/AuthController.java
new file mode 100644
index 0000000..38f3555
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/auth/controller/AuthController.java
@@ -0,0 +1,21 @@
+package com.riotz.api.auth.controller;
+
+import com.riotz.api.auth.service.AuthService;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/auth")
+public class AuthController {
+
+ private final AuthService authService;
+
+ @GetMapping("/health")
+ public Map health() {
+ return Map.of("status", authService.health());
+ }
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/auth/service/AuthService.java b/backend/riotz-api/src/main/java/com/riotz/api/auth/service/AuthService.java
new file mode 100644
index 0000000..aeead6f
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/auth/service/AuthService.java
@@ -0,0 +1,5 @@
+package com.riotz.api.auth.service;
+
+public interface AuthService {
+ String health();
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/auth/service/AuthServiceImpl.java b/backend/riotz-api/src/main/java/com/riotz/api/auth/service/AuthServiceImpl.java
new file mode 100644
index 0000000..90d40cf
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/auth/service/AuthServiceImpl.java
@@ -0,0 +1,12 @@
+package com.riotz.api.auth.service;
+
+import org.springframework.stereotype.Service;
+
+@Service
+public class AuthServiceImpl implements AuthService {
+
+ @Override
+ public String health() {
+ return "auth-module-ready";
+ }
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/chat/controller/ChatController.java b/backend/riotz-api/src/main/java/com/riotz/api/chat/controller/ChatController.java
new file mode 100644
index 0000000..d933575
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/chat/controller/ChatController.java
@@ -0,0 +1,22 @@
+package com.riotz.api.chat.controller;
+
+import com.riotz.api.chat.model.Chat;
+import com.riotz.api.chat.service.ChatService;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/chats")
+public class ChatController {
+
+ private final ChatService chatService;
+
+ @GetMapping
+ public List listChats() {
+ return chatService.listChats();
+ }
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/chat/model/Chat.java b/backend/riotz-api/src/main/java/com/riotz/api/chat/model/Chat.java
new file mode 100644
index 0000000..029fc79
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/chat/model/Chat.java
@@ -0,0 +1,26 @@
+package com.riotz.api.chat.model;
+
+import com.riotz.api.common.model.BaseEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import java.util.UUID;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Entity
+@Table(name = "chats")
+public class Chat extends BaseEntity {
+
+ @Id
+ private UUID id;
+
+ @Column(nullable = false, length = 120)
+ private String name;
+
+ @Column(nullable = false)
+ private boolean groupChat = true;
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/chat/repository/ChatRepository.java b/backend/riotz-api/src/main/java/com/riotz/api/chat/repository/ChatRepository.java
new file mode 100644
index 0000000..e2f8009
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/chat/repository/ChatRepository.java
@@ -0,0 +1,8 @@
+package com.riotz.api.chat.repository;
+
+import com.riotz.api.chat.model.Chat;
+import java.util.UUID;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ChatRepository extends JpaRepository {
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/chat/service/ChatService.java b/backend/riotz-api/src/main/java/com/riotz/api/chat/service/ChatService.java
new file mode 100644
index 0000000..b8f94d8
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/chat/service/ChatService.java
@@ -0,0 +1,8 @@
+package com.riotz.api.chat.service;
+
+import com.riotz.api.chat.model.Chat;
+import java.util.List;
+
+public interface ChatService {
+ List listChats();
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/chat/service/ChatServiceImpl.java b/backend/riotz-api/src/main/java/com/riotz/api/chat/service/ChatServiceImpl.java
new file mode 100644
index 0000000..6ff78de
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/chat/service/ChatServiceImpl.java
@@ -0,0 +1,19 @@
+package com.riotz.api.chat.service;
+
+import com.riotz.api.chat.model.Chat;
+import com.riotz.api.chat.repository.ChatRepository;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class ChatServiceImpl implements ChatService {
+
+ private final ChatRepository chatRepository;
+
+ @Override
+ public List listChats() {
+ return chatRepository.findAll();
+ }
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/comment/controller/CommentController.java b/backend/riotz-api/src/main/java/com/riotz/api/comment/controller/CommentController.java
new file mode 100644
index 0000000..1f81582
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/comment/controller/CommentController.java
@@ -0,0 +1,24 @@
+package com.riotz.api.comment.controller;
+
+import com.riotz.api.comment.model.Comment;
+import com.riotz.api.comment.service.CommentService;
+import java.util.List;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/comments")
+public class CommentController {
+
+ private final CommentService commentService;
+
+ @GetMapping("/post/{postId}")
+ public List listPostComments(@PathVariable UUID postId) {
+ return commentService.listByPost(postId);
+ }
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/comment/model/Comment.java b/backend/riotz-api/src/main/java/com/riotz/api/comment/model/Comment.java
new file mode 100644
index 0000000..a0fa8fc
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/comment/model/Comment.java
@@ -0,0 +1,29 @@
+package com.riotz.api.comment.model;
+
+import com.riotz.api.common.model.BaseEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import java.util.UUID;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Entity
+@Table(name = "comments")
+public class Comment extends BaseEntity {
+
+ @Id
+ private UUID id;
+
+ @Column(nullable = false)
+ private UUID postId;
+
+ @Column(nullable = false)
+ private UUID userId;
+
+ @Column(nullable = false, length = 1200)
+ private String content;
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/comment/repository/CommentRepository.java b/backend/riotz-api/src/main/java/com/riotz/api/comment/repository/CommentRepository.java
new file mode 100644
index 0000000..c6fb1d0
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/comment/repository/CommentRepository.java
@@ -0,0 +1,10 @@
+package com.riotz.api.comment.repository;
+
+import com.riotz.api.comment.model.Comment;
+import java.util.List;
+import java.util.UUID;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface CommentRepository extends JpaRepository {
+ List findByPostIdOrderByCreatedAtAsc(UUID postId);
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/comment/service/CommentService.java b/backend/riotz-api/src/main/java/com/riotz/api/comment/service/CommentService.java
new file mode 100644
index 0000000..cc78cc6
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/comment/service/CommentService.java
@@ -0,0 +1,9 @@
+package com.riotz.api.comment.service;
+
+import com.riotz.api.comment.model.Comment;
+import java.util.List;
+import java.util.UUID;
+
+public interface CommentService {
+ List listByPost(UUID postId);
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/comment/service/CommentServiceImpl.java b/backend/riotz-api/src/main/java/com/riotz/api/comment/service/CommentServiceImpl.java
new file mode 100644
index 0000000..5e9e835
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/comment/service/CommentServiceImpl.java
@@ -0,0 +1,20 @@
+package com.riotz.api.comment.service;
+
+import com.riotz.api.comment.model.Comment;
+import com.riotz.api.comment.repository.CommentRepository;
+import java.util.List;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class CommentServiceImpl implements CommentService {
+
+ private final CommentRepository commentRepository;
+
+ @Override
+ public List listByPost(UUID postId) {
+ return commentRepository.findByPostIdOrderByCreatedAtAsc(postId);
+ }
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/common/config/JpaConfig.java b/backend/riotz-api/src/main/java/com/riotz/api/common/config/JpaConfig.java
new file mode 100644
index 0000000..ca7f3a3
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/common/config/JpaConfig.java
@@ -0,0 +1,9 @@
+package com.riotz.api.common.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+@Configuration
+@EnableJpaAuditing
+public class JpaConfig {
+}
diff --git a/backend/riotz-api/src/main/java/com/riotz/api/common/exception/GlobalExceptionHandler.java b/backend/riotz-api/src/main/java/com/riotz/api/common/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..a0f8ff7
--- /dev/null
+++ b/backend/riotz-api/src/main/java/com/riotz/api/common/exception/GlobalExceptionHandler.java
@@ -0,0 +1,31 @@
+package com.riotz.api.common.exception;
+
+import java.time.OffsetDateTime;
+import java.util.Map;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(ResourceNotFoundException.class)
+ public ResponseEntity