diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
new file mode 100644
index 0000000..62c1d86
--- /dev/null
+++ b/.github/workflows/deploy.yaml
@@ -0,0 +1,26 @@
+name: Deploy to Server
+
+on:
+ push:
+ branches: [ dev ]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Deploy via SSH
+ uses: appleboy/ssh-action@v1.0.3
+ with:
+ host: ${{ secrets.SERVER_IP }}
+ username: ${{ secrets.SERVER_USER }}
+ password: ${{ secrets.SERVER_PASS }}
+ port: ${{ secrets.SERVER_PORT }}
+ script: |
+ cd ~/GoWithMe
+ docker compose down
+ git pull
+ docker compose up -d --build
\ No newline at end of file
diff --git a/.github/workflows/flutter.yaml b/.github/workflows/flutter.yaml
new file mode 100644
index 0000000..80c0a0b
--- /dev/null
+++ b/.github/workflows/flutter.yaml
@@ -0,0 +1,68 @@
+name: Build & Deploy Web
+
+permissions:
+ contents: write
+
+on:
+ push:
+ branches: [ dev, main, devops ]
+ pull_request:
+ branches: [ dev, main, devops ]
+
+jobs:
+ build-and-deploy-web:
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup Flutter
+ uses: subosito/flutter-action@v2
+ with:
+ channel: stable
+
+ - name: Install dependencies
+ run: |
+ cd frontend
+ flutter pub get
+
+ - name: Check formatting
+ run: |
+ cd frontend
+ dart format --set-exit-if-changed .
+
+ - name: Run tests with coverage
+ run: |
+ cd frontend
+ flutter test --coverage
+
+ - name: Install lcov
+ run: sudo apt-get update && sudo apt-get install -y lcov
+
+ - name: Generate HTML report
+ run: |
+ cd frontend
+ genhtml coverage/lcov.info --output-directory coverage/html
+
+ - name: Upload coverage artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: flutter-coverage-report
+ path: coverage/html
+
+ - name: Static analysis
+ run: flutter analyze --no-fatal-warnings --no-fatal-infos
+
+ - name: Build web
+ run: |
+ cd frontend
+ flutter build web --release --base-href='/GoWithMe/'
+
+ - name: Deploy to GitHub Pages
+ uses: bluefireteam/flutter-gh-pages@v9
+ with:
+ baseHref: '/GoWithMe/'
+ workingDir: frontend
+ customArgs: --target="lib/main.dart"
diff --git a/.github/workflows/go-backend.yaml b/.github/workflows/go-backend.yaml
new file mode 100644
index 0000000..2b5dd43
--- /dev/null
+++ b/.github/workflows/go-backend.yaml
@@ -0,0 +1,34 @@
+name: Go Backend CI
+
+on:
+ push:
+ branches: [ main, dev ]
+ pull_request:
+ branches: [ main, dev ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./backend
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: '1.21'
+
+ - name: Install dependencies
+ run: go mod download
+
+ - name: Lint
+ run: go fmt ./... && go vet ./...
+
+ - name: Run tests
+ run: go test -v ./...
+
+ - name: Build
+ run: go build -v ./cmd/main.go
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dff8271
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+.DS_Store
+.idea
+api/.DS_STORE
+api/protobuf/.DS_STORE
+backend/.DS_STORE
+backend/.idea
+backend/auth/.DS_STORE
+backend/auth/.idea
+backend/gateway/.DS_STORE
+backend/gateway/.idea
+backend/interests/.DS_STORE
+backend/interests/.idea
+backend/profile/.DS_STORE
+backend/profile/.idea
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..3d84e8d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,12 @@
+export PATH := $(HOME)/go/bin:$(PATH)
+
+.PHONY: all
+all: backend_dependencies backend_docs
+
+.PHONY: backend_dependencies
+backend_dependencies:
+ cd backend && go install github.com/swaggo/swag/cmd/swag@latest
+ cd backend && go mod tidy
+
+backend_docs:
+ cd backend && swag init --generalInfo cmd/main.go
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c759673
--- /dev/null
+++ b/README.md
@@ -0,0 +1,307 @@
+# GoWithMe – Find Your Activity Buddy
+
+**GoWithMe** is a full-stack cross-platform app that helps people find partners for sports and healthy lifestyle activities in their local area. The project was built during the **Flutter Summer Course** at Innopolis University
+
+---
+
+## Team
+
+| Name | Role(s) | Responsibilities |
+|-------------------|-------------------------------|---------------------------------------------------|
+| Lavrova Marina | Frontend, Task Planning | Flutter, UI, integration with backend, task board |
+| Alexander Blinov | Backend, DB | Architecture, database, backend, API, design slides |
+| Leonid Merkulov | DevOps, Tests, Docs, Slides | CI/CD, Docker, tests, documentation, presentation |
+
+---
+
+## Implementation checklist
+
+
+### Technical requirements (20 points)
+#### Backend development (8 points)
+- [3] Go-based backend (3 points)
+- [2] RESTful API with Swagger documentation (2 point) [- api](https://github.com/MAL-da-udal/GoWithMe/blob/dev/backend/internal/api/handlers.go) and [docs](https://github.com/MAL-da-udal/GoWithMe/tree/dev/backend/docs)
+- [1] PostgreSQL database with proper schema design (1 point) [- db](https://github.com/MAL-da-udal/GoWithMe/tree/dev/backend/internal/db)
+- [1] JWT-based authentication and authorization (1 point) [- jwt](https://github.com/MAL-da-udal/GoWithMe/blob/dev/backend/internal/api/handlers.go)
+- [1] Comprehensive unit and integration tests (1 point) [- tests][https://github.com/MAL-da-udal/GoWithMe/blob/dev/backend/internal/api/api_test.go]
+
+#### Frontend development (8 points)
+- [3] Flutter-based cross-platform application (mobile + web) (3 points)
+all app
+- [1] Responsive UI design with custom widgets (1 point) [- custom widgets](https://github.com/MAL-da-udal/GoWithMe/commit/bd9105f3eb86d3b883eb2aca849dcc2a5669ba45)
+- [1] State management implementation (1 point) [- for search](https://github.com/MAL-da-udal/GoWithMe/commit/d6aabbab14efb84f276356a08fad985a69c3d67b) and [for theme](https://github.com/MAL-da-udal/GoWithMe/commit/bd9105f3eb86d3b883eb2aca849dcc2a5669ba45)
+- [1] Offline data persistence (1 point) [- sharedPrefs](https://github.com/MAL-da-udal/GoWithMe/commit/1b49dfaa21b93b809e655cc596a6d0c9d690a846) and [getStorage](https://github.com/MAL-da-udal/GoWithMe/commit/c017b6bc00eae3f0e5c6b9612a5ad7d8465defa9)
+- [1] Unit and widget tests (1 point) [- here](https://github.com/MAL-da-udal/GoWithMe/commits/dev/frontend/test)
+- [1] Support light and dark mode (1 point) [- here](https://github.com/MAL-da-udal/GoWithMe/commit/bd9105f3eb86d3b883eb2aca849dcc2a5669ba45)
+
+#### DevOps & deployment (4 points)
+- [1] Docker compose for all services (1 point) [- here](https://github.com/MAL-da-udal/GoWithMe/blob/dev/compose.yaml)
+- [1] CI/CD pipeline implementation (1 point) [- here](https://github.com/MAL-da-udal/GoWithMe/tree/dev/.github/workflows)
+- [0] Environment configuration management using config files (1 point)
+- [1] GitHub pages for the project (1 point) [- gh-pages](https://mal-da-udal.github.io/GoWithMe/)
+
+### Non-Technical Requirements (10 points)
+#### Project management (4 points)
+- [1] GitHub organization with well-maintained repository (1 point)
+- [1] Regular commits and meaningful pull requests from all team members (1 point)
+- [1] Project board (GitHub Projects) with task tracking (1 point) [- board](https://github.com/orgs/MAL-da-udal/projects/5)
+- [1] Team member roles and responsibilities documentation (1 point)
+
+#### Documentation (4 points)
+- [1] Project overview and setup instructions (1 point)
+- [1] Screenshots and GIFs of key features (1 point)
+- [1] API documentation (1 point)
+- [1] Architecture diagrams and explanations (1 point)
+
+#### Code quality (2 points)
+- [1] Consistent code style and formatting during CI/CD pipeline (1 point)
+- [1] Code review participation and resolution (1 point)
+
+### Bonus Features (up to 10 points)
+- [2] Localization for Russian (RU) and English (ENG) languages (2 points) [- here](https://github.com/MAL-da-udal/GoWithMe/commit/f6368d41d444b8317f04883e8c9038b5c97de1c9)
+- [3] Good UI/UX design (up to 3 points) [- PR with design](https://github.com/MAL-da-udal/GoWithMe/pull/25)
+- [0] Integration with external APIs (fitness trackers, health devices) (up to 5 points)
+- [2] Comprehensive error handling and user feedback (up to 2 points) [- here](https://github.com/MAL-da-udal/GoWithMe/commit/c9de86cc1e6900de5b2f29abd3961aef8c840026)
+- [3] Advanced animations and transitions (up to 3 points) [- custom transitions via go_router](https://github.com/MAL-da-udal/GoWithMe/pull/25/commits/bd9105f3eb86d3b883eb2aca849dcc2a5669ba45)
+- [2] Widget implementation for native mobile elements (up to 2 points) [- settings page](https://github.com/MAL-da-udal/GoWithMe/blob/dev/frontend/lib/ui/pages/settings_page.dart)
+
+Total points implemented: 29/30 (excluding bonus points)
+
+---
+
+## Project Overview
+
+GoWithMe is designed to help users find activity partners for sports and healthy lifestyle events. The app supports user registration, profile management, activity search, and real-time results.
+
+---
+
+## Setup Instructions
+
+### Prerequisites
+- **Docker** & **Docker Compose** (recommended for all platforms)
+- (For development only) Flutter (3.22+) and Go (1.22+)
+
+---
+
+### Quick Start (recommended)
+
+The easiest way to run the project (backend, frontend, and database) is using Docker Compose:
+
+```sh
+# Clone the repository
+git clone https://github.com/MAL-da-udal/GoWithMe.git
+cd GoWithMe
+
+# Build and start all services
+docker compose up --build
+```
+
+- **Backend:** http://localhost:8080
+- **Frontend (Web):** http://localhost:80
+- **Swagger API docs:** http://localhost:8080/swagger/index.html
+
+---
+
+### Development Mode (for development)
+
+If you want to run and update services separately:
+
+#### Frontend
+
+```sh
+cd frontend
+flutter pub get
+flutter run
+```
+Then choose the device on which you want to run
+
+> Important note: now the frontend connected to the API of the deployed server , so if you want to use your local mashine as a server: - change the baseUrl ( uncomment the line 7 and comment the line 6 in the file `frontend/lib/data/api/api_client.dart`)
+
+
+#### Backend
+
+```sh
+cd backend
+go mod tidy
+go run cmd/main.go
+```
+
+#### Database
+
+- You can use the database from Docker Compose or your own local PostgreSQL instance.
+
+---
+
+### Running Tests
+
+#### Frontend
+
+```sh
+cd frontend
+flutter test --coverage
+```
+
+#### Backend
+
+```sh
+cd backend
+go test ./...
+```
+
+
+---
+
+## API Documentation
+
+All documentation can be found in swagger. You can access it on `http://mhdserver.ru:8080/swagger/index.html`
+
+
+---
+
+## Architecture
+
+### Backend
+
+
+#### Stack Overview
+
+* **HTTP Server framework:** gin
+* **Database ORM:** gorm
+* **Database:** PostgreSQL
+
+### Key features
+- JWT Authentication and authorization
+- gORM for convenient communication with Database
+- Gin framework to organize RESTful API
+- Swagger for documentation
+
+### Frontend Architecture
+
+#### Stack Overview
+
+* **State Management:** Flutter_riverpod
+* **HTTP Client:** Dio handle errors and refresh auth token automatically
+* **Storage:** GetStorage for auth/refresh tokens and shared_prefs for other data
+* **Routing**: GoRouter with custom tranistions
+* **Cross-platform**: supports all platform with specific settings customization
+* **Localization**: easyLocalization
+
+### Key features
+
+- Authorization - login/register, handling tokens
+ - Automatically update access token after it expired using refresh token and interceptors in Dio
+ - Handle errors
+ - Store tokens in getStorage
+- Profile - create and update profile, offline storing
+ - Store in cache with shared_prefs
+ - Upadte via dio
+- Search - apply filters, store state in riverpod
+ - Load interests list from api
+ - Load users matched the filters
+ - Allow to open user profile with info and redirect to telegram
+- Settings - theme and language switch + logout
+ - Theme switching via Theme and riverpod
+ - Translates all texts to chosen language
+ - Allows to logout
+
+
+
+---
+
+### Architecture Diagram
+### Frontend schema
+```
+[User]
+ ↓
+[UI Layer: Screens & Widgets]
+ ├── auth_page.dart
+ ├── home_page.dart
+ ├── profile_tab.dart
+ ├── search_tab.dart
+ ├── settings_page.dart
+ └── user_profile_page.dart
+ ↓
+[State Management (Riverpod Providers)]
+ ├── searchProvider
+ └── themeProvider
+ ↓
+[Repositories]
+ ├── AuthRepository
+ ├── ProfileRepository
+ └── SearchRepository
+ ↓
+[API Client Layer]
+ └── ApiClient (Dio + interceptors + token refresh logic)
+ ├── Base URL: http://mhdserver.ru:8080
+ ├── Request Interceptor (adds Authorization header)
+ ├── Error Interceptor (handles 401 + refreshes token)
+ └── Token Storage: GetStorage
+ ↓
+[HTTP REST API (Go Backend + PostgreSQL)]
+```
+### Layers, which is also included
+```
+[Models]
+ ├── user.dart
+ └── (interests, etc.)
+
+[Utilities & Helpers]
+ ├── validations.dart
+ ├── show_api_error.dart
+ ├── open_url.dart
+ └── text_to_string.dart
+
+[Local Storage Services]
+ └── shared_preferences_service.dart
+
+[Theming & Localization]
+ ├── app_theme.dart
+ ├── app_colors.dart
+ ├── app_text_styles.dart
+ └── assets/translations/ (en.json, ru.json)
+ ```
+
+Full app structure
+```
+[User]
+ |
+ v
+[Flutter App]
+ |
+ v
+[ApiClient] --> [AuthRepository, ProfileRepository, SearchRepository]
+ |
+ v
+[REST API on Go] (Swagger)
+ |
+ v
+[Business Logic Layer (Go)]
+ |
+ v
+[PostgreSQL Database]
+```
+
+## Screenshots & GIFs
+
+Full demo you could see [here](https://drive.google.com/drive/folders/16_uYnuXlzk4iJTm0Z0DVk7sfJwKf0gUK?usp=sharing)
+
+### Login screen
+
+
+### Profile screen
+
+
+### Search
+
+### Settings
+
+
+---
+
+## Links
+
+- **GitHub Organization:** [MAL-da-udal](https://github.com/MAL-da-udal)
+- **Repository:** [GoWithMe](https://github.com/MAL-da-udal/GoWithMe)
+- **GitHub Pages (Web Demo):** https://mal-da-udal.github.io/GoWithMe/
+- **Presentation video:** https://drive.google.com/drive/folders/16_uYnuXlzk4iJTm0Z0DVk7sfJwKf0gUK?usp=sharing
+- **Project board:** https://github.com/orgs/MAL-da-udal/projects/5
diff --git a/backend/.gitignore b/backend/.gitignore
new file mode 100644
index 0000000..d8ad59f
--- /dev/null
+++ b/backend/.gitignore
@@ -0,0 +1,4 @@
+.DS_STORE
+.idea
+internal/.DS_STORE
+internal/.idea
\ No newline at end of file
diff --git a/backend/Dockerfile b/backend/Dockerfile
index b6dbda7..e943aff 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,2 +1,10 @@
-# TODO
+FROM golang:tip-bullseye
+WORKDIR /app
+
+COPY . .
+RUN go mod download
+
+RUN go build -o main ./cmd
+RUN chmod +x main
+CMD ["./main"]
\ No newline at end of file
diff --git a/backend/cmd/main.go b/backend/cmd/main.go
new file mode 100644
index 0000000..9ebe50c
--- /dev/null
+++ b/backend/cmd/main.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+ _ "backend/docs"
+ "backend/internal/config"
+ "backend/internal/db"
+ "backend/internal/routes"
+)
+
+// @title GoWithMe API Documentation
+// @version 1.0
+// @license.name Apache 2.0
+// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
+// @BasePath /
+// @securityDefinitions.apikey BearerAuth
+// @in header
+// @name Authorization
+
+func main() {
+ config.Load()
+ db.Connect()
+
+ router := routes.Setup()
+ router.Run(":8081")
+}
diff --git a/backend/docs/docs.go b/backend/docs/docs.go
new file mode 100644
index 0000000..e52cd4d
--- /dev/null
+++ b/backend/docs/docs.go
@@ -0,0 +1,685 @@
+// Package docs Code generated by swaggo/swag. DO NOT EDIT
+package docs
+
+import "github.com/swaggo/swag"
+
+const docTemplate = `{
+ "schemes": {{ marshal .Schemes }},
+ "swagger": "2.0",
+ "info": {
+ "description": "{{escape .Description}}",
+ "title": "{{.Title}}",
+ "contact": {},
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "{{.Version}}"
+ },
+ "host": "{{.Host}}",
+ "basePath": "{{.BasePath}}",
+ "paths": {
+ "/auth/": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Validates token, and if it valid returns it's claims",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authorization"
+ ],
+ "summary": "Get token claims",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/login": {
+ "post": {
+ "description": "Checks credentials and gives JWT if alles ist gut",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authorization"
+ ],
+ "summary": "Login user",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Username",
+ "name": "username",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Password",
+ "name": "password",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/refresh": {
+ "post": {
+ "description": "Refreshes access token using refresh token",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authorization"
+ ],
+ "summary": "Refresh access token",
+ "parameters": [
+ {
+ "description": "Refresh Token",
+ "name": "refresh_token",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.RefreshRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/register": {
+ "post": {
+ "description": "Registers user with provided login and password",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authorization"
+ ],
+ "summary": "Register user",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Username",
+ "name": "username",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Password",
+ "name": "password",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/avatar": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Retrieves the avatar image for the specified user or the authenticated user if no user_id is provided.",
+ "produces": [
+ "image/jpeg",
+ "image/png"
+ ],
+ "tags": [
+ "Avatar"
+ ],
+ "summary": "Get user avatar",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "User ID",
+ "name": "user_id",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Image",
+ "schema": {
+ "type": "file"
+ }
+ },
+ "400": {
+ "description": "Invalid input",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "401": {
+ "description": "Invalid or unauthorized token",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "404": {
+ "description": "Avatar not found",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Server error",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ }
+ }
+ },
+ "put": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Updates the avatar image for the authenticated user. Supports JPEG and PNG files up to 2MB.",
+ "consumes": [
+ "multipart/form-data"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Avatar"
+ ],
+ "summary": "Update user avatar",
+ "parameters": [
+ {
+ "type": "file",
+ "description": "Avatar image file (JPEG or PNG)",
+ "name": "avatar",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Avatar uploaded successfully",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid input (e.g., file too large, wrong format)",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "401": {
+ "description": "Invalid or unauthorized token",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Server error",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/interests/": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interests"
+ ],
+ "summary": "Get user's interests",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "User ID",
+ "name": "user_id",
+ "in": "query"
+ }
+ ],
+ "responses": {}
+ },
+ "put": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interests"
+ ],
+ "summary": "Update user's interests",
+ "parameters": [
+ {
+ "description": "Interests to set",
+ "name": "interests",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.UpdateInterestsRequest"
+ }
+ }
+ ],
+ "responses": {}
+ }
+ },
+ "/interests/all": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Retrieves paginated users who have specified interests, along with their profiles and all interests",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interests"
+ ],
+ "summary": "Get users by interests",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "Page number (starting from 1)",
+ "name": "page_num",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "default": 10,
+ "description": "Number of users per page",
+ "name": "page_size",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Semicolon-separated list of interest IDs (e.g., bicycle;swimming)",
+ "name": "interests",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Paginated users with profiles and interests",
+ "schema": {
+ "$ref": "#/definitions/models.PaginatedUsersResponse"
+ }
+ },
+ "400": {
+ "description": "Invalid input (e.g., missing token, invalid page number, or empty interests)",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "401": {
+ "description": "Invalid or unauthorized token",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/interests/cats": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interests"
+ ],
+ "summary": "Get existing interests",
+ "responses": {}
+ }
+ },
+ "/profile/": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Returns profile info of specified user id. If not specified, returns profile info of user encoded in jwt",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Profile"
+ ],
+ "summary": "Get profile",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "User ID",
+ "name": "user_id",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/models.Profile"
+ }
+ }
+ }
+ },
+ "post": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Creates profile",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Profile"
+ ],
+ "summary": "Create profile",
+ "parameters": [
+ {
+ "description": "Profile Info",
+ "name": "profile_info",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.Profile"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "patch": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Partially updates profile information",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Profile"
+ ],
+ "summary": "Update profile",
+ "parameters": [
+ {
+ "description": "Profile Info to update",
+ "name": "profile_info",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.UpdateProfileRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/models.Profile"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "models.ErrorResponse": {
+ "type": "object",
+ "properties": {
+ "details": {
+ "type": "string"
+ }
+ }
+ },
+ "models.PaginatedUsersResponse": {
+ "type": "object",
+ "properties": {
+ "page_num": {
+ "type": "integer"
+ },
+ "users": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/definitions/models.UserProfileWithInterests"
+ }
+ }
+ }
+ },
+ "models.Profile": {
+ "type": "object",
+ "properties": {
+ "age": {
+ "type": "integer"
+ },
+ "description": {
+ "type": "string"
+ },
+ "gender": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "surname": {
+ "type": "string"
+ },
+ "telegram": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "integer"
+ }
+ }
+ },
+ "models.RefreshRequest": {
+ "type": "object",
+ "required": [
+ "refresh_token"
+ ],
+ "properties": {
+ "refresh_token": {
+ "type": "string"
+ }
+ }
+ },
+ "models.UpdateInterestsRequest": {
+ "type": "object",
+ "properties": {
+ "interests": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "models.UpdateProfileRequest": {
+ "type": "object",
+ "properties": {
+ "age": {
+ "type": "integer"
+ },
+ "description": {
+ "type": "string"
+ },
+ "gender": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "surname": {
+ "type": "string"
+ },
+ "telegram": {
+ "type": "string"
+ }
+ }
+ },
+ "models.UserProfileWithInterests": {
+ "type": "object",
+ "properties": {
+ "interests": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "profile": {
+ "$ref": "#/definitions/models.Profile"
+ }
+ }
+ }
+ },
+ "securityDefinitions": {
+ "BearerAuth": {
+ "type": "apiKey",
+ "name": "Authorization",
+ "in": "header"
+ }
+ }
+}`
+
+// SwaggerInfo holds exported Swagger Info so clients can modify it
+var SwaggerInfo = &swag.Spec{
+ Version: "1.0",
+ Host: "",
+ BasePath: "/",
+ Schemes: []string{},
+ Title: "GoWithMe API Documentation",
+ Description: "",
+ InfoInstanceName: "swagger",
+ SwaggerTemplate: docTemplate,
+ LeftDelim: "{{",
+ RightDelim: "}}",
+}
+
+func init() {
+ swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
+}
diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json
new file mode 100644
index 0000000..4258bd9
--- /dev/null
+++ b/backend/docs/swagger.json
@@ -0,0 +1,659 @@
+{
+ "swagger": "2.0",
+ "info": {
+ "title": "GoWithMe API Documentation",
+ "contact": {},
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "1.0"
+ },
+ "basePath": "/",
+ "paths": {
+ "/auth/": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Validates token, and if it valid returns it's claims",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authorization"
+ ],
+ "summary": "Get token claims",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/login": {
+ "post": {
+ "description": "Checks credentials and gives JWT if alles ist gut",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authorization"
+ ],
+ "summary": "Login user",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Username",
+ "name": "username",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Password",
+ "name": "password",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/refresh": {
+ "post": {
+ "description": "Refreshes access token using refresh token",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authorization"
+ ],
+ "summary": "Refresh access token",
+ "parameters": [
+ {
+ "description": "Refresh Token",
+ "name": "refresh_token",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.RefreshRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/register": {
+ "post": {
+ "description": "Registers user with provided login and password",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authorization"
+ ],
+ "summary": "Register user",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Username",
+ "name": "username",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Password",
+ "name": "password",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/avatar": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Retrieves the avatar image for the specified user or the authenticated user if no user_id is provided.",
+ "produces": [
+ "image/jpeg",
+ "image/png"
+ ],
+ "tags": [
+ "Avatar"
+ ],
+ "summary": "Get user avatar",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "User ID",
+ "name": "user_id",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Image",
+ "schema": {
+ "type": "file"
+ }
+ },
+ "400": {
+ "description": "Invalid input",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "401": {
+ "description": "Invalid or unauthorized token",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "404": {
+ "description": "Avatar not found",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Server error",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ }
+ }
+ },
+ "put": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Updates the avatar image for the authenticated user. Supports JPEG and PNG files up to 2MB.",
+ "consumes": [
+ "multipart/form-data"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Avatar"
+ ],
+ "summary": "Update user avatar",
+ "parameters": [
+ {
+ "type": "file",
+ "description": "Avatar image file (JPEG or PNG)",
+ "name": "avatar",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Avatar uploaded successfully",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid input (e.g., file too large, wrong format)",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "401": {
+ "description": "Invalid or unauthorized token",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Server error",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/interests/": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interests"
+ ],
+ "summary": "Get user's interests",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "User ID",
+ "name": "user_id",
+ "in": "query"
+ }
+ ],
+ "responses": {}
+ },
+ "put": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interests"
+ ],
+ "summary": "Update user's interests",
+ "parameters": [
+ {
+ "description": "Interests to set",
+ "name": "interests",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.UpdateInterestsRequest"
+ }
+ }
+ ],
+ "responses": {}
+ }
+ },
+ "/interests/all": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Retrieves paginated users who have specified interests, along with their profiles and all interests",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interests"
+ ],
+ "summary": "Get users by interests",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "Page number (starting from 1)",
+ "name": "page_num",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "default": 10,
+ "description": "Number of users per page",
+ "name": "page_size",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Semicolon-separated list of interest IDs (e.g., bicycle;swimming)",
+ "name": "interests",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Paginated users with profiles and interests",
+ "schema": {
+ "$ref": "#/definitions/models.PaginatedUsersResponse"
+ }
+ },
+ "400": {
+ "description": "Invalid input (e.g., missing token, invalid page number, or empty interests)",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "401": {
+ "description": "Invalid or unauthorized token",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "schema": {
+ "$ref": "#/definitions/models.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/interests/cats": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interests"
+ ],
+ "summary": "Get existing interests",
+ "responses": {}
+ }
+ },
+ "/profile/": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Returns profile info of specified user id. If not specified, returns profile info of user encoded in jwt",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Profile"
+ ],
+ "summary": "Get profile",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "User ID",
+ "name": "user_id",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/models.Profile"
+ }
+ }
+ }
+ },
+ "post": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Creates profile",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Profile"
+ ],
+ "summary": "Create profile",
+ "parameters": [
+ {
+ "description": "Profile Info",
+ "name": "profile_info",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.Profile"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "patch": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Partially updates profile information",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Profile"
+ ],
+ "summary": "Update profile",
+ "parameters": [
+ {
+ "description": "Profile Info to update",
+ "name": "profile_info",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.UpdateProfileRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/models.Profile"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "models.ErrorResponse": {
+ "type": "object",
+ "properties": {
+ "details": {
+ "type": "string"
+ }
+ }
+ },
+ "models.PaginatedUsersResponse": {
+ "type": "object",
+ "properties": {
+ "page_num": {
+ "type": "integer"
+ },
+ "users": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/definitions/models.UserProfileWithInterests"
+ }
+ }
+ }
+ },
+ "models.Profile": {
+ "type": "object",
+ "properties": {
+ "age": {
+ "type": "integer"
+ },
+ "description": {
+ "type": "string"
+ },
+ "gender": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "surname": {
+ "type": "string"
+ },
+ "telegram": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "integer"
+ }
+ }
+ },
+ "models.RefreshRequest": {
+ "type": "object",
+ "required": [
+ "refresh_token"
+ ],
+ "properties": {
+ "refresh_token": {
+ "type": "string"
+ }
+ }
+ },
+ "models.UpdateInterestsRequest": {
+ "type": "object",
+ "properties": {
+ "interests": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "models.UpdateProfileRequest": {
+ "type": "object",
+ "properties": {
+ "age": {
+ "type": "integer"
+ },
+ "description": {
+ "type": "string"
+ },
+ "gender": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "surname": {
+ "type": "string"
+ },
+ "telegram": {
+ "type": "string"
+ }
+ }
+ },
+ "models.UserProfileWithInterests": {
+ "type": "object",
+ "properties": {
+ "interests": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "profile": {
+ "$ref": "#/definitions/models.Profile"
+ }
+ }
+ }
+ },
+ "securityDefinitions": {
+ "BearerAuth": {
+ "type": "apiKey",
+ "name": "Authorization",
+ "in": "header"
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml
new file mode 100644
index 0000000..d985311
--- /dev/null
+++ b/backend/docs/swagger.yaml
@@ -0,0 +1,426 @@
+basePath: /
+definitions:
+ models.ErrorResponse:
+ properties:
+ details:
+ type: string
+ type: object
+ models.PaginatedUsersResponse:
+ properties:
+ page_num:
+ type: integer
+ users:
+ additionalProperties:
+ $ref: '#/definitions/models.UserProfileWithInterests'
+ type: object
+ type: object
+ models.Profile:
+ properties:
+ age:
+ type: integer
+ description:
+ type: string
+ gender:
+ type: string
+ id:
+ type: integer
+ name:
+ type: string
+ surname:
+ type: string
+ telegram:
+ type: string
+ user_id:
+ type: integer
+ type: object
+ models.RefreshRequest:
+ properties:
+ refresh_token:
+ type: string
+ required:
+ - refresh_token
+ type: object
+ models.UpdateInterestsRequest:
+ properties:
+ interests:
+ items:
+ type: string
+ type: array
+ type: object
+ models.UpdateProfileRequest:
+ properties:
+ age:
+ type: integer
+ description:
+ type: string
+ gender:
+ type: string
+ name:
+ type: string
+ surname:
+ type: string
+ telegram:
+ type: string
+ type: object
+ models.UserProfileWithInterests:
+ properties:
+ interests:
+ items:
+ type: string
+ type: array
+ profile:
+ $ref: '#/definitions/models.Profile'
+ type: object
+info:
+ contact: {}
+ license:
+ name: Apache 2.0
+ url: http://www.apache.org/licenses/LICENSE-2.0.html
+ title: GoWithMe API Documentation
+ version: "1.0"
+paths:
+ /auth/:
+ get:
+ consumes:
+ - application/json
+ description: Validates token, and if it valid returns it's claims
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ additionalProperties:
+ type: string
+ type: object
+ security:
+ - BearerAuth: []
+ summary: Get token claims
+ tags:
+ - Authorization
+ /auth/login:
+ post:
+ consumes:
+ - application/json
+ description: Checks credentials and gives JWT if alles ist gut
+ parameters:
+ - description: Username
+ in: query
+ name: username
+ required: true
+ type: string
+ - description: Password
+ in: query
+ name: password
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ additionalProperties:
+ type: string
+ type: object
+ summary: Login user
+ tags:
+ - Authorization
+ /auth/refresh:
+ post:
+ consumes:
+ - application/json
+ description: Refreshes access token using refresh token
+ parameters:
+ - description: Refresh Token
+ in: body
+ name: refresh_token
+ required: true
+ schema:
+ $ref: '#/definitions/models.RefreshRequest'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ additionalProperties:
+ type: string
+ type: object
+ summary: Refresh access token
+ tags:
+ - Authorization
+ /auth/register:
+ post:
+ consumes:
+ - application/json
+ description: Registers user with provided login and password
+ parameters:
+ - description: Username
+ in: query
+ name: username
+ required: true
+ type: string
+ - description: Password
+ in: query
+ name: password
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ additionalProperties:
+ type: string
+ type: object
+ summary: Register user
+ tags:
+ - Authorization
+ /avatar:
+ get:
+ description: Retrieves the avatar image for the specified user or the authenticated
+ user if no user_id is provided.
+ parameters:
+ - description: User ID
+ in: query
+ name: user_id
+ type: integer
+ produces:
+ - image/jpeg
+ - image/png
+ responses:
+ "200":
+ description: Image
+ schema:
+ type: file
+ "400":
+ description: Invalid input
+ schema:
+ $ref: '#/definitions/models.ErrorResponse'
+ "401":
+ description: Invalid or unauthorized token
+ schema:
+ $ref: '#/definitions/models.ErrorResponse'
+ "404":
+ description: Avatar not found
+ schema:
+ $ref: '#/definitions/models.ErrorResponse'
+ "500":
+ description: Server error
+ schema:
+ $ref: '#/definitions/models.ErrorResponse'
+ security:
+ - BearerAuth: []
+ summary: Get user avatar
+ tags:
+ - Avatar
+ put:
+ consumes:
+ - multipart/form-data
+ description: Updates the avatar image for the authenticated user. Supports JPEG
+ and PNG files up to 2MB.
+ parameters:
+ - description: Avatar image file (JPEG or PNG)
+ in: formData
+ name: avatar
+ required: true
+ type: file
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Avatar uploaded successfully
+ schema:
+ additionalProperties:
+ type: string
+ type: object
+ "400":
+ description: Invalid input (e.g., file too large, wrong format)
+ schema:
+ $ref: '#/definitions/models.ErrorResponse'
+ "401":
+ description: Invalid or unauthorized token
+ schema:
+ $ref: '#/definitions/models.ErrorResponse'
+ "500":
+ description: Server error
+ schema:
+ $ref: '#/definitions/models.ErrorResponse'
+ security:
+ - BearerAuth: []
+ summary: Update user avatar
+ tags:
+ - Avatar
+ /interests/:
+ get:
+ consumes:
+ - application/json
+ parameters:
+ - description: User ID
+ in: query
+ name: user_id
+ type: integer
+ produces:
+ - application/json
+ responses: {}
+ security:
+ - BearerAuth: []
+ summary: Get user's interests
+ tags:
+ - Interests
+ put:
+ consumes:
+ - application/json
+ parameters:
+ - description: Interests to set
+ in: body
+ name: interests
+ required: true
+ schema:
+ $ref: '#/definitions/models.UpdateInterestsRequest'
+ produces:
+ - application/json
+ responses: {}
+ security:
+ - BearerAuth: []
+ summary: Update user's interests
+ tags:
+ - Interests
+ /interests/all:
+ get:
+ consumes:
+ - application/json
+ description: Retrieves paginated users who have specified interests, along with
+ their profiles and all interests
+ parameters:
+ - description: Page number (starting from 1)
+ in: query
+ name: page_num
+ required: true
+ type: integer
+ - default: 10
+ description: Number of users per page
+ in: query
+ name: page_size
+ type: integer
+ - description: Semicolon-separated list of interest IDs (e.g., bicycle;swimming)
+ in: query
+ name: interests
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Paginated users with profiles and interests
+ schema:
+ $ref: '#/definitions/models.PaginatedUsersResponse'
+ "400":
+ description: Invalid input (e.g., missing token, invalid page number, or
+ empty interests)
+ schema:
+ $ref: '#/definitions/models.ErrorResponse'
+ "401":
+ description: Invalid or unauthorized token
+ schema:
+ $ref: '#/definitions/models.ErrorResponse'
+ "500":
+ description: Internal server error
+ schema:
+ $ref: '#/definitions/models.ErrorResponse'
+ security:
+ - BearerAuth: []
+ summary: Get users by interests
+ tags:
+ - Interests
+ /interests/cats:
+ get:
+ consumes:
+ - application/json
+ produces:
+ - application/json
+ responses: {}
+ security:
+ - BearerAuth: []
+ summary: Get existing interests
+ tags:
+ - Interests
+ /profile/:
+ get:
+ consumes:
+ - application/json
+ description: Returns profile info of specified user id. If not specified, returns
+ profile info of user encoded in jwt
+ parameters:
+ - description: User ID
+ in: query
+ name: user_id
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/models.Profile'
+ security:
+ - BearerAuth: []
+ summary: Get profile
+ tags:
+ - Profile
+ patch:
+ consumes:
+ - application/json
+ description: Partially updates profile information
+ parameters:
+ - description: Profile Info to update
+ in: body
+ name: profile_info
+ required: true
+ schema:
+ $ref: '#/definitions/models.UpdateProfileRequest'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/models.Profile'
+ security:
+ - BearerAuth: []
+ summary: Update profile
+ tags:
+ - Profile
+ post:
+ consumes:
+ - application/json
+ description: Creates profile
+ parameters:
+ - description: Profile Info
+ in: body
+ name: profile_info
+ required: true
+ schema:
+ $ref: '#/definitions/models.Profile'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ additionalProperties:
+ type: string
+ type: object
+ security:
+ - BearerAuth: []
+ summary: Create profile
+ tags:
+ - Profile
+securityDefinitions:
+ BearerAuth:
+ in: header
+ name: Authorization
+ type: apiKey
+swagger: "2.0"
diff --git a/backend/go.mod b/backend/go.mod
new file mode 100644
index 0000000..6edea20
--- /dev/null
+++ b/backend/go.mod
@@ -0,0 +1,65 @@
+module backend
+
+go 1.24.1
+
+require (
+ github.com/gin-gonic/gin v1.10.1
+ github.com/golang-jwt/jwt/v5 v5.2.2
+ github.com/stretchr/testify v1.10.0
+ github.com/swaggo/files v1.0.1
+ github.com/swaggo/gin-swagger v1.6.0
+ github.com/swaggo/swag v1.16.4
+ golang.org/x/crypto v0.40.0
+ gorm.io/driver/postgres v1.6.0
+ gorm.io/driver/sqlite v1.6.0
+ gorm.io/gorm v1.30.0
+)
+
+require (
+ github.com/KyleBanks/depth v1.2.1 // indirect
+ github.com/bytedance/sonic v1.13.3 // indirect
+ github.com/bytedance/sonic/loader v0.2.4 // indirect
+ github.com/cloudwego/base64x v0.1.5 // indirect
+ github.com/cloudwego/iasm v0.2.0 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.9 // indirect
+ github.com/gin-contrib/cors v1.7.6 // indirect
+ github.com/gin-contrib/sse v1.1.0 // indirect
+ github.com/go-openapi/jsonpointer v0.21.1 // indirect
+ github.com/go-openapi/jsonreference v0.21.0 // indirect
+ github.com/go-openapi/spec v0.21.0 // indirect
+ github.com/go-openapi/swag v0.23.1 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.26.0 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+ github.com/jackc/pgx/v5 v5.6.0 // indirect
+ github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.10 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mailru/easyjson v0.9.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-sqlite3 v1.14.22 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/rogpeppe/go-internal v1.14.1 // indirect
+ github.com/stretchr/objx v0.5.2 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.3.0 // indirect
+ golang.org/x/arch v0.18.0 // indirect
+ golang.org/x/net v0.42.0 // indirect
+ golang.org/x/sync v0.16.0 // indirect
+ golang.org/x/sys v0.34.0 // indirect
+ golang.org/x/text v0.27.0 // indirect
+ golang.org/x/tools v0.35.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/backend/go.sum b/backend/go.sum
new file mode 100644
index 0000000..68f8b66
--- /dev/null
+++ b/backend/go.sum
@@ -0,0 +1,198 @@
+github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
+github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
+github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
+github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
+github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
+github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
+github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
+github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
+github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
+github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
+github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
+github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
+github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
+github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
+github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
+github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
+github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
+github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
+github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
+github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
+github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
+github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
+github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
+github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
+github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
+github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
+github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
+golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
+golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
+golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
+golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
+golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
+golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
+golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
+golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
+gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
+gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
+gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
+gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
+gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/backend/internal/api/api_test.go b/backend/internal/api/api_test.go
new file mode 100644
index 0000000..bd55ba9
--- /dev/null
+++ b/backend/internal/api/api_test.go
@@ -0,0 +1,154 @@
+package api
+
+import (
+ "bytes"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+)
+
+func init() {
+ gin.SetMode(gin.TestMode)
+}
+
+func getTestContext(method, path string, body []byte, token string) (*gin.Context, *httptest.ResponseRecorder) {
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(method, path, bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ if token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
+ ctx, _ := gin.CreateTestContext(rec)
+ ctx.Request = req
+
+ if token != "" {
+ ctx.Set("jwt", token)
+ } else {
+ ctx.Set("jwt", "")
+ }
+ return ctx, rec
+}
+
+func TestCreateProfile_NoJWT(t *testing.T) {
+ ctx, rec := getTestContext("POST", "/profile", nil, "")
+ CreateProfile(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", rec.Code)
+ }
+}
+
+func TestCreateProfile_InvalidJSON(t *testing.T) {
+ ctx, rec := getTestContext("POST", "/profile", []byte(`{invalid json}`), "validtoken")
+ CreateProfile(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", rec.Code)
+ }
+}
+
+func TestGetProfile_NoJWT(t *testing.T) {
+ ctx, rec := getTestContext("GET", "/profile", nil, "")
+ GetProfile(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", rec.Code)
+ }
+}
+
+func TestGetProfile_InvalidUserID(t *testing.T) {
+ ctx, rec := getTestContext("GET", "/profile?user_id=abc", nil, "validtoken")
+ GetProfile(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400 for invalid user_id, got %d", rec.Code)
+ }
+}
+
+func TestUpdateProfile_NoJWT(t *testing.T) {
+ ctx, rec := getTestContext("PATCH", "/profile", nil, "")
+ UpdateProfile(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", rec.Code)
+ }
+}
+
+func TestUpdateProfile_InvalidJSON(t *testing.T) {
+ ctx, rec := getTestContext("PATCH", "/profile", []byte(`{name:bad}`), "validtoken")
+ UpdateProfile(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", rec.Code)
+ }
+}
+
+func TestUpdateInterests_NoJWT(t *testing.T) {
+ ctx, rec := getTestContext("PUT", "/interests", nil, "")
+ UpdateInterests(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", rec.Code)
+ }
+}
+
+func TestUpdateInterests_InvalidJSON(t *testing.T) {
+ ctx, rec := getTestContext("PUT", "/interests", []byte(`not a json`), "validtoken")
+ UpdateInterests(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", rec.Code)
+ }
+}
+
+func TestGetInterests_NoJWT(t *testing.T) {
+ ctx, rec := getTestContext("GET", "/interests", nil, "")
+ GetInterests(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", rec.Code)
+ }
+}
+
+func TestGetInterests_InvalidUserID(t *testing.T) {
+ ctx, rec := getTestContext("GET", "/interests?user_id=xyz", nil, "validtoken")
+ GetInterests(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", rec.Code)
+ }
+}
+
+func TestGetAllInterests_NoJWT(t *testing.T) {
+ ctx, rec := getTestContext("GET", "/interests/cats", nil, "")
+ GetAllInterests(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", rec.Code)
+ }
+}
+
+func TestGetUsersByInterests_NoJWT(t *testing.T) {
+ ctx, rec := getTestContext("GET", "/interests/all?page_num=1&interests=book", nil, "")
+ GetUsersByInterests(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", rec.Code)
+ }
+}
+
+func TestGetUsersByInterests_InvalidPageNum(t *testing.T) {
+ ctx, rec := getTestContext("GET", "/interests/all?page_num=0&interests=book", nil, "validtoken")
+ GetUsersByInterests(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400 for invalid page_num, got %d", rec.Code)
+ }
+}
+
+func TestUpdateAvatar_NoJWT(t *testing.T) {
+ ctx, rec := getTestContext("PUT", "/avatar", nil, "")
+
+ rec.Code = 0
+ UpdateAvatar(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", rec.Code)
+ }
+}
+
+func TestGetAvatar_NoJWT(t *testing.T) {
+ ctx, rec := getTestContext("GET", "/avatar", nil, "")
+ GetAvatar(ctx)
+ if rec.Code != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", rec.Code)
+ }
+}
diff --git a/backend/internal/api/handlers.go b/backend/internal/api/handlers.go
new file mode 100644
index 0000000..c69da33
--- /dev/null
+++ b/backend/internal/api/handlers.go
@@ -0,0 +1,614 @@
+package api
+
+import (
+ "backend/internal/models"
+ "backend/internal/services"
+ "errors"
+ "github.com/gin-gonic/gin"
+ "strconv"
+ "strings"
+)
+
+// @Summary Register user
+// @Description Registers user with provided login and password
+// @Tags Authorization
+// @Accept json
+// @Produce json
+// @Param username query string true "Username"
+// @Param password query string true "Password"
+// @Success 200 {object} map[string]string
+// @Router /auth/register [post]
+func Register(ctx *gin.Context) {
+ username := ctx.Query("username")
+ password := ctx.Query("password")
+
+ if len(username) == 0 || len(password) == 0 {
+ ctx.JSON(400, models.ErrorResponse{Details: services.ErrInvalidInput.Error()})
+ return
+ }
+
+ accessToken, refreshToken, err := services.RegisterUser(username, password)
+ if err != nil {
+ if errors.Is(err, services.ErrUserExists) {
+ ctx.JSON(400, models.ErrorResponse{Details: err.Error()})
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: "Internal server error"})
+ }
+ return
+ }
+
+ ctx.JSON(200, models.RegisterResponse{AccessToken: accessToken, RefreshToken: refreshToken})
+}
+
+// @Summary Login user
+// @Description Checks credentials and gives JWT if alles ist gut
+// @Tags Authorization
+// @Accept json
+// @Produce json
+// @Param username query string true "Username"
+// @Param password query string true "Password"
+// @Success 200 {object} map[string]string
+// @Router /auth/login [post]
+func Login(ctx *gin.Context) {
+ username := ctx.Query("username")
+ password := ctx.Query("password")
+
+ if len(username) == 0 || len(password) == 0 {
+ ctx.JSON(400, models.ErrorResponse{Details: services.ErrInvalidInput.Error()})
+ return
+ }
+
+ accessToken, refreshToken, err := services.LoginUser(username, password)
+ if err != nil {
+ if errors.Is(err, services.ErrUnknownUserOrPass) {
+ ctx.JSON(400, models.ErrorResponse{Details: err.Error()})
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: "Internal server error"})
+ }
+ return
+ }
+
+ ctx.JSON(200, models.LoginResponse{AccessToken: accessToken, RefreshToken: refreshToken})
+}
+
+// @Summary Refresh access token
+// @Description Refreshes access token using refresh token
+// @Tags Authorization
+// @Accept json
+// @Produce json
+// @Param refresh_token body models.RefreshRequest true "Refresh Token"
+// @Success 200 {object} map[string]string
+// @Router /auth/refresh [post]
+func RefreshToken(ctx *gin.Context) {
+
+ var refreshRequest models.RefreshRequest
+
+ if err := ctx.ShouldBindJSON(&refreshRequest); err != nil {
+ ctx.JSON(400, models.ErrorResponse{Details: "Refresh token is required"})
+ return
+ }
+
+ newAccessToken, err := services.RefreshAccessToken(refreshRequest.RefreshToken)
+ if err != nil {
+ if errors.Is(err, services.ErrInvalidRefreshToken) {
+ ctx.JSON(401, models.ErrorResponse{Details: err.Error()})
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: "Internal server error"})
+ }
+ return
+ }
+
+ ctx.JSON(200, map[string]string{"access_token": newAccessToken})
+}
+
+// @Summary Get token claims
+// @Description Validates token, and if it valid returns it's claims
+// @Tags Authorization
+// @Accept json
+// @Produce json
+// @Security BearerAuth
+// @Success 200 {object} map[string]string
+// @Router /auth/ [get]
+func GetTokenClaims(ctx *gin.Context) {
+ tokenParam, _ := ctx.Get("jwt")
+ token := tokenParam.(string)
+
+ if len(token) == 0 {
+ ctx.JSON(400, models.ErrorResponse{Details: services.ErrInvalidInput.Error()})
+ return
+ }
+
+ claims, err := services.GetTokenClaims(token)
+ if err != nil {
+ if errors.Is(err, services.ErrInvalidToken) {
+ ctx.JSON(401, models.ErrorResponse{Details: err.Error()})
+ return
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+ }
+
+ ctx.JSON(200, claims)
+}
+
+// @Summary Create profile
+// @Description Creates profile
+// @Tags Profile
+// @Accept json
+// @Produce json
+// @Security BearerAuth
+// @Param profile_info body models.Profile true "Profile Info"
+// @Success 200 {object} map[string]string
+// @Router /profile/ [post]
+func CreateProfile(ctx *gin.Context) {
+ var req models.Profile
+ tokenParam, _ := ctx.Get("jwt")
+ token := tokenParam.(string)
+
+ if len(token) == 0 {
+ ctx.JSON(400, models.ErrorResponse{Details: services.ErrInvalidInput.Error()})
+ return
+ }
+
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ ctx.JSON(400, models.ErrorResponse{Details: services.ErrInvalidInput.Error()})
+ return
+ }
+
+ claims, err := services.GetTokenClaims(token)
+ if err != nil {
+ if errors.Is(err, services.ErrInvalidToken) {
+ ctx.JSON(401, models.ErrorResponse{Details: err.Error()})
+ return
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+ }
+
+ toCreate := models.Profile{
+ UserId: claims.UserID,
+ Name: req.Name,
+ Surname: req.Surname,
+ Age: req.Age,
+ Gender: req.Gender,
+ Telegram: req.Telegram,
+ Description: req.Description,
+ }
+
+ err = services.CreateProfile(&toCreate)
+
+ if err != nil {
+ if errors.Is(err, services.ErrProfileAlreadyExists) {
+ ctx.JSON(400, models.ErrorResponse{Details: err.Error()})
+ return
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+ }
+
+ ctx.JSON(201, map[string]interface{}{
+ "details": "Created successfully",
+ })
+}
+
+// @Summary Get profile
+// @Description Returns profile info of specified user id. If not specified, returns profile info of user encoded in jwt
+// @Tags Profile
+// @Accept json
+// @Produce json
+// @Security BearerAuth
+// @Param user_id query int false "User ID"
+// @Success 200 {object} models.Profile
+// @Router /profile/ [get]
+func GetProfile(ctx *gin.Context) {
+ tokenParam, _ := ctx.Get("jwt")
+ token := tokenParam.(string)
+
+ if len(token) == 0 {
+ ctx.JSON(400, models.ErrorResponse{Details: services.ErrInvalidInput.Error()})
+ return
+ }
+
+ var neededUserId int
+
+ if len(ctx.Query("user_id")) == 0 {
+ neededUserId = 0
+ } else {
+ var err error
+ neededUserId, err = strconv.Atoi(ctx.Query("user_id"))
+
+ if err != nil {
+ ctx.JSON(400, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+ }
+
+ claims, err := services.GetTokenClaims(token)
+ if err != nil {
+ if errors.Is(err, services.ErrInvalidToken) {
+ ctx.JSON(401, models.ErrorResponse{Details: err.Error()})
+ return
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+ }
+
+ info, err := services.GetProfile(&claims, neededUserId)
+
+ if err != nil {
+ if errors.Is(err, services.ErrProfileDoesNotExists) {
+ ctx.JSON(400, models.ErrorResponse{Details: err.Error()})
+ return
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+ }
+
+ ctx.JSON(200, info)
+}
+
+// @Summary Update profile
+// @Description Partially updates profile information
+// @Tags Profile
+// @Accept json
+// @Produce json
+// @Security BearerAuth
+// @Param profile_info body models.UpdateProfileRequest true "Profile Info to update"
+// @Success 200 {object} models.Profile
+// @Router /profile/ [patch]
+func UpdateProfile(ctx *gin.Context) {
+ tokenParam, _ := ctx.Get("jwt")
+ token := tokenParam.(string)
+
+ if len(token) == 0 {
+ ctx.JSON(400, models.ErrorResponse{Details: services.ErrInvalidInput.Error()})
+ return
+ }
+
+ var req models.UpdateProfileRequest
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ ctx.JSON(400, models.ErrorResponse{Details: services.ErrInvalidInput.Error()})
+ return
+ }
+
+ claims, err := services.GetTokenClaims(token)
+ if err != nil {
+ if errors.Is(err, services.ErrInvalidToken) {
+ ctx.JSON(401, models.ErrorResponse{Details: err.Error()})
+ return
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+ }
+
+ updatedProfile, err := services.UpdateProfile(claims.UserID, &req)
+ if err != nil {
+ if errors.Is(err, services.ErrProfileDoesNotExists) {
+ ctx.JSON(404, models.ErrorResponse{Details: err.Error()})
+ return
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+ }
+
+ ctx.JSON(200, updatedProfile)
+}
+
+// @Summary Update user's interests
+// @Tags Interests
+// @Accept json
+// @Produce json
+// @Security BearerAuth
+// @Param interests body models.UpdateInterestsRequest true "Interests to set"
+// @Router /interests/ [put]
+func UpdateInterests(ctx *gin.Context) {
+ tokenParam, _ := ctx.Get("jwt")
+ token := tokenParam.(string)
+
+ if len(token) == 0 {
+ ctx.JSON(400, models.ErrorResponse{Details: services.ErrInvalidInput.Error()})
+ return
+ }
+
+ var req models.UpdateInterestsRequest
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ ctx.JSON(400, models.ErrorResponse{Details: services.ErrInvalidInput.Error()})
+ return
+ }
+
+ claims, err := services.GetTokenClaims(token)
+ if err != nil {
+ if errors.Is(err, services.ErrInvalidToken) {
+ ctx.JSON(401, models.ErrorResponse{Details: err.Error()})
+ return
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+ }
+
+ err = services.SetInterests(&claims, req.Interests)
+ if err != nil {
+ if errors.Is(err, services.ErrUnknownInterest) {
+ ctx.JSON(400, models.ErrorResponse{Details: err.Error()})
+ return
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+ }
+
+ ctx.JSON(200, map[string]string{"details": "Interests updated successfully"})
+}
+
+// @Summary Get user's interests
+// @Tags Interests
+// @Accept json
+// @Produce json
+// @Security BearerAuth
+// @Param user_id query int false "User ID"
+// @Router /interests/ [get]
+func GetInterests(ctx *gin.Context) {
+ tokenParam, _ := ctx.Get("jwt")
+ token := tokenParam.(string)
+
+ if len(token) == 0 {
+ ctx.JSON(400, models.ErrorResponse{Details: services.ErrInvalidInput.Error()})
+ return
+ }
+
+ var neededUserId int
+
+ if len(ctx.Query("user_id")) == 0 {
+ neededUserId = 0
+ } else {
+ var err error
+ neededUserId, err = strconv.Atoi(ctx.Query("user_id"))
+
+ if err != nil {
+ ctx.JSON(400, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+ }
+
+ claims, err := services.GetTokenClaims(token)
+ if err != nil {
+ if errors.Is(err, services.ErrInvalidToken) {
+ ctx.JSON(401, models.ErrorResponse{Details: err.Error()})
+ return
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+ }
+
+ interests, err := services.GetInterests(&claims, neededUserId)
+
+ if err != nil {
+ ctx.JSON(500, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+
+ ctx.JSON(200, map[string]interface{}{
+ "details": interests,
+ })
+}
+
+// @Summary Get existing interests
+// @Tags Interests
+// @Accept json
+// @Produce json
+// @Security BearerAuth
+// @Router /interests/cats [get]
+func GetAllInterests(ctx *gin.Context) {
+ tokenParam, _ := ctx.Get("jwt")
+ token := tokenParam.(string)
+
+ if len(token) == 0 {
+ ctx.JSON(400, models.ErrorResponse{Details: services.ErrInvalidInput.Error()})
+ return
+ }
+
+ _, err := services.GetTokenClaims(token)
+ if err != nil {
+ if errors.Is(err, services.ErrInvalidToken) {
+ ctx.JSON(401, models.ErrorResponse{Details: err.Error()})
+ return
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+ }
+
+ interests := services.GetAllInterests()
+
+ ctx.JSON(200, map[string]interface{}{
+ "details": interests,
+ })
+}
+
+// @Summary Get users by interests
+// @Description Retrieves paginated users who have specified interests, along with their profiles and all interests
+// @Tags Interests
+// @Accept json
+// @Produce json
+// @Security BearerAuth
+// @Param page_num query int true "Page number (starting from 1)"
+// @Param page_size query int false "Number of users per page" default(10)
+// @Param interests query string true "Semicolon-separated list of interest IDs (e.g., bicycle;swimming)"
+// @Success 200 {object} models.PaginatedUsersResponse "Paginated users with profiles and interests"
+// @Failure 400 {object} models.ErrorResponse "Invalid input (e.g., missing token, invalid page number, or empty interests)"
+// @Failure 401 {object} models.ErrorResponse "Invalid or unauthorized token"
+// @Failure 500 {object} models.ErrorResponse "Internal server error"
+// @Router /interests/all [get]
+func GetUsersByInterests(ctx *gin.Context) {
+ // Extract and validate token
+ tokenParam, _ := ctx.Get("jwt")
+ token := tokenParam.(string)
+
+ if len(token) == 0 {
+ ctx.JSON(400, models.ErrorResponse{Details: "Token is required"})
+ return
+ }
+
+ // Extract and validate page number
+ pageNumStr := ctx.Query("page_num")
+ pageNum, err := strconv.Atoi(pageNumStr)
+ if err != nil || pageNum < 1 {
+ ctx.JSON(400, models.ErrorResponse{Details: "Invalid page number"})
+ return
+ }
+
+ // Extract and validate page size (default to 10)
+ pageSizeStr := ctx.DefaultQuery("page_size", "10")
+ pageSize, err := strconv.Atoi(pageSizeStr)
+ if err != nil || pageSize < 1 {
+ ctx.JSON(400, models.ErrorResponse{Details: "Invalid page size"})
+ return
+ }
+
+ // Extract and split interests
+ interestsStr := ctx.Query("interests")
+ if len(interestsStr) == 0 {
+ ctx.JSON(400, models.ErrorResponse{Details: "Interests are required"})
+ return
+ }
+ interests := strings.Split(interestsStr, ";")
+
+ // Validate token
+ _, err = services.GetTokenClaims(token)
+ if err != nil {
+ if errors.Is(err, services.ErrInvalidToken) {
+ ctx.JSON(401, models.ErrorResponse{Details: "Invalid token"})
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: "Internal server error"})
+ }
+ return
+ }
+
+ // Call service to get users
+ response, err := services.GetUsersByInterests(pageNum, interests, pageSize)
+ if err != nil {
+ ctx.JSON(500, models.ErrorResponse{Details: err.Error()})
+ return
+ }
+
+ ctx.JSON(200, response)
+}
+
+// @Summary Update user avatar
+// @Description Updates the avatar image for the authenticated user. Supports JPEG and PNG files up to 2MB.
+// @Tags Avatar
+// @Accept multipart/form-data
+// @Produce json
+// @Security BearerAuth
+// @Param avatar formData file true "Avatar image file (JPEG or PNG)"
+// @Success 200 {object} map[string]string "Avatar uploaded successfully"
+// @Failure 400 {object} models.ErrorResponse "Invalid input (e.g., file too large, wrong format)"
+// @Failure 401 {object} models.ErrorResponse "Invalid or unauthorized token"
+// @Failure 500 {object} models.ErrorResponse "Server error"
+// @Router /avatar [put]
+func UpdateAvatar(ctx *gin.Context) {
+
+ tokenParam, _ := ctx.Get("jwt")
+ token := tokenParam.(string)
+
+ if len(token) == 0 {
+ ctx.JSON(400, models.ErrorResponse{Details: "Token is required"})
+ return
+ }
+
+ claims, err := services.GetTokenClaims(token)
+ if err != nil {
+ if errors.Is(err, services.ErrInvalidToken) {
+ ctx.JSON(401, models.ErrorResponse{Details: "Invalid token"})
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: "Internal server error"})
+ }
+ return
+ }
+
+ file, header, err := ctx.Request.FormFile("avatar")
+ if err != nil {
+ ctx.JSON(400, models.ErrorResponse{Details: "Failed to get file"})
+ return
+ }
+ defer file.Close()
+
+ _, err = services.UploadAvatar(uint(claims.UserID), file, header)
+ if err != nil {
+ if errors.Is(err, services.ErrFileTooLarge) || errors.Is(err, services.ErrInvalidFileType) || errors.Is(err, services.ErrFileReadFailed) {
+ ctx.JSON(400, models.ErrorResponse{Details: err.Error()})
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: "Internal server error"})
+ }
+ return
+ }
+
+ ctx.JSON(200, map[string]string{"details": "Avatar uploaded successfully"})
+}
+
+// @Summary Get user avatar
+// @Description Retrieves the avatar image for the specified user or the authenticated user if no user_id is provided.
+// @Tags Avatar
+// @Produce image/jpeg,image/png
+// @Security BearerAuth
+// @Param user_id query int false "User ID"
+// @Success 200 {file} file "Image"
+// @Failure 400 {object} models.ErrorResponse "Invalid input"
+// @Failure 401 {object} models.ErrorResponse "Invalid or unauthorized token"
+// @Failure 404 {object} models.ErrorResponse "Avatar not found"
+// @Failure 500 {object} models.ErrorResponse "Server error"
+// @Router /avatar [get]
+func GetAvatar(ctx *gin.Context) {
+
+ tokenParam, _ := ctx.Get("jwt")
+ token := tokenParam.(string)
+
+ if len(token) == 0 {
+ ctx.JSON(400, models.ErrorResponse{Details: "Token is required"})
+ return
+ }
+
+ claims, err := services.GetTokenClaims(token)
+ if err != nil {
+ if errors.Is(err, services.ErrInvalidToken) {
+ ctx.JSON(401, models.ErrorResponse{Details: "Invalid token"})
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: "Internal server error"})
+ }
+ return
+ }
+
+ var targetUserId int
+ userIdStr := ctx.Query("user_id")
+ if userIdStr == "" {
+ targetUserId = claims.UserID
+ } else {
+ parsedId, err := strconv.Atoi(userIdStr)
+ if err != nil || parsedId < 1 {
+ ctx.JSON(400, models.ErrorResponse{Details: "Invalid user_id"})
+ return
+ }
+ targetUserId = parsedId
+ }
+
+ path, err := services.GetAvatarPath(targetUserId)
+ if err != nil {
+ if errors.Is(err, services.ErrAvatarNotFound) {
+ ctx.JSON(404, models.ErrorResponse{Details: "Avatar not found"})
+ } else {
+ ctx.JSON(500, models.ErrorResponse{Details: "Internal server error"})
+ }
+ return
+ }
+
+ ctx.File(path)
+}
diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go
new file mode 100644
index 0000000..cb63907
--- /dev/null
+++ b/backend/internal/config/config.go
@@ -0,0 +1,93 @@
+package config
+
+import "os"
+
+type Config struct {
+ Routes struct {
+ CORSAddresses []string
+ }
+
+ Database struct {
+ Host string
+ User string
+ Password string
+ Name string
+ }
+
+ JWT struct {
+ SecretKey string
+ }
+
+ Interest struct {
+ Interests map[string]struct{}
+ }
+
+ Avatars struct {
+ UploadDir string
+ MaxFileSize int64
+ }
+}
+
+var AppConfig Config
+
+func CreateInterests() {
+ interests := []string{
+ "swimming",
+ "cycling",
+ "football",
+ "basketball",
+ "tennis",
+ "running",
+ "yoga",
+ "golf",
+ "skiing",
+ "surfing",
+ "hiking",
+ "climbing",
+ "kayaking",
+ "fishing",
+ "horse riding",
+ "nature walks",
+ "beach walks",
+ "dog walking",
+ "photography walks",
+ "bird watching",
+ "fitness",
+ "weightlifting",
+ "crossfit",
+ "pilates",
+ "dancing",
+ "skateboarding",
+ "snowboarding",
+ "paragliding",
+ "diving",
+ "martial arts",
+ "healthy food",
+ }
+
+ AppConfig.Interest.Interests = make(map[string]struct{}, 2)
+
+ for _, interest := range interests {
+ AppConfig.Interest.Interests[interest] = struct{}{}
+ }
+}
+
+func Load() {
+ AppConfig.Database.Host = os.Getenv("POSTGRES_HOST")
+ AppConfig.Database.User = os.Getenv("POSTGRES_USER")
+ AppConfig.Database.Password = os.Getenv("POSTGRES_PASSWORD")
+ AppConfig.Database.Name = os.Getenv("POSTGRES_DB")
+
+ AppConfig.JWT.SecretKey = os.Getenv("SECRET_KEY")
+
+ CreateInterests()
+
+ AppConfig.Avatars.UploadDir = "./uploads/avatars"
+ AppConfig.Avatars.MaxFileSize = 2 * 1024 * 1024
+
+ if err := os.MkdirAll(AppConfig.Avatars.UploadDir, os.ModePerm); err != nil {
+ panic("Failed to create avatar upload directory")
+ }
+
+ AppConfig.Routes.CORSAddresses = []string{"http://mhdserver.ru", "http://localhost:8080", "https://mal-da-udal.github.io/GoWithMe/"}
+}
diff --git a/backend/internal/db/db.go b/backend/internal/db/db.go
new file mode 100644
index 0000000..f5b95ec
--- /dev/null
+++ b/backend/internal/db/db.go
@@ -0,0 +1,28 @@
+package db
+
+import (
+ "backend/internal/config"
+ "backend/internal/models"
+ "gorm.io/driver/postgres"
+ "gorm.io/gorm"
+ "log"
+)
+
+var DB *gorm.DB
+
+func MakeMigrations() {
+ DB.AutoMigrate(&models.User{}, &models.Profile{}, &models.Interest{}, &models.RefreshToken{}, &models.Avatar{})
+}
+
+func Connect() {
+ dsn := "host=" + config.AppConfig.Database.Host + " user=" + config.AppConfig.Database.User + " password=" + config.AppConfig.Database.Password + " dbname=" + config.AppConfig.Database.Name + " sslmode=disable TimeZone=Europe/Moscow"
+
+ var err error
+ DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
+
+ if err != nil {
+ log.Fatalf("Error while connecting to database: %s\n", err.Error())
+ }
+
+ MakeMigrations()
+}
diff --git a/backend/internal/models/auth.go b/backend/internal/models/auth.go
new file mode 100644
index 0000000..4d2e9b0
--- /dev/null
+++ b/backend/internal/models/auth.go
@@ -0,0 +1,43 @@
+package models
+
+import "time"
+
+type RegisterRequest struct {
+ Username string `json:"username" binding:"required"`
+ Password string `json:"password" binding:"required"`
+}
+
+type RefreshRequest struct {
+ RefreshToken string `json:"refresh_token" binding:"required"`
+}
+
+type LoginRequest struct {
+ Username string `json:"username" binding:"required"`
+ Password string `json:"password" binding:"required"`
+}
+
+type RegisterResponse struct {
+ AccessToken string `json:"access_token"`
+ RefreshToken string `json:"refresh_token"`
+}
+
+type TokenClaimsResponse struct {
+ UserID int `json:"user_id"`
+ Username string `json:"username"`
+}
+
+type LoginResponse struct {
+ AccessToken string `json:"access_token"`
+ RefreshToken string `json:"refresh_token"`
+}
+
+type ErrorResponse struct {
+ Details string `json:"details"`
+}
+
+type RefreshToken struct {
+ ID int `gorm:"primarykey"`
+ UserID int `gorm:"not null"`
+ Token string `gorm:"not null"`
+ ExpiresAt time.Time `gorm:"not null"`
+}
diff --git a/backend/internal/models/avatar.go b/backend/internal/models/avatar.go
new file mode 100644
index 0000000..4c3c56a
--- /dev/null
+++ b/backend/internal/models/avatar.go
@@ -0,0 +1,7 @@
+package models
+
+type Avatar struct {
+ ID uint `gorm:"primaryKey" json:"id"`
+ UserID uint `gorm:"not null" json:"user_id"`
+ Path string `gorm:"not null" json:"path"`
+}
diff --git a/backend/internal/models/interest.go b/backend/internal/models/interest.go
new file mode 100644
index 0000000..d9d698d
--- /dev/null
+++ b/backend/internal/models/interest.go
@@ -0,0 +1,29 @@
+package models
+
+type Interest struct {
+ Id int `json:"id" gorm:"primarykey"`
+ InterestId string `json:"interest_id"`
+ UserId int `json:"user_id"`
+}
+
+type UpdateInterestsRequest struct {
+ Interests []string `json:"interests"`
+}
+
+type AllInterestsResponse struct {
+ Interests []string `json:"interests"`
+}
+
+type UserProfileWithInterests struct {
+ Profile Profile `json:"profile"`
+ Interests []string `json:"interests"`
+}
+
+type PaginatedUsersResponse struct {
+ PageNum int `json:"page_num"`
+ Users map[int]UserProfileWithInterests `json:"users"`
+}
+
+type InterestsRequest struct {
+ Interests []string `json:"interests"`
+}
diff --git a/backend/internal/models/profile.go b/backend/internal/models/profile.go
new file mode 100644
index 0000000..5ed8aa4
--- /dev/null
+++ b/backend/internal/models/profile.go
@@ -0,0 +1,21 @@
+package models
+
+type Profile struct {
+ Id int `json:"id" gorm:"primarykey"`
+ UserId int `json:"user_id"`
+ Name string `json:"name"`
+ Surname string `json:"surname"`
+ Age int `json:"age"`
+ Gender string `json:"gender"`
+ Telegram string `json:"telegram"`
+ Description string `json:"description"`
+}
+
+type UpdateProfileRequest struct {
+ Name *string `json:"name"`
+ Surname *string `json:"surname"`
+ Age *int `json:"age"`
+ Gender *string `json:"gender"`
+ Telegram *string `json:"telegram"`
+ Description *string `json:"description"`
+}
diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go
new file mode 100644
index 0000000..2ec0fda
--- /dev/null
+++ b/backend/internal/models/user.go
@@ -0,0 +1,7 @@
+package models
+
+type User struct {
+ Id int `json:"id" gorm:"primarykey"`
+ Username string `json:"username"`
+ Password string `json:"password"`
+}
diff --git a/backend/internal/routes/routes.go b/backend/internal/routes/routes.go
new file mode 100644
index 0000000..e514e54
--- /dev/null
+++ b/backend/internal/routes/routes.go
@@ -0,0 +1,85 @@
+package routes
+
+import (
+ api "backend/internal/api"
+ "backend/internal/config"
+ "github.com/gin-contrib/cors"
+ "github.com/gin-gonic/gin"
+ swaggerFiles "github.com/swaggo/files"
+ ginSwagger "github.com/swaggo/gin-swagger"
+ "net/http"
+ "strings"
+)
+
+func jwtMiddleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+
+ authHeader := c.GetHeader("Authorization")
+ if authHeader == "" {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
+ return
+ }
+
+ parts := strings.SplitN(authHeader, " ", 2)
+ if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"})
+ return
+ }
+
+ token := parts[1]
+
+ c.Set("jwt", token)
+
+ c.Next()
+ }
+}
+
+func Setup() *gin.Engine {
+ router := gin.Default()
+
+ corsConfig := cors.DefaultConfig()
+ corsConfig.AllowOrigins = config.AppConfig.Routes.CORSAddresses
+ corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}
+ corsConfig.AllowHeaders = []string{"Origin", "Content-Type", "Authorization"}
+ corsConfig.AllowCredentials = true
+ router.Use(cors.New(corsConfig))
+
+ router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
+
+ {
+ auth := router.Group("/auth")
+ auth.POST("/register", api.Register)
+ auth.POST("/login", api.Login)
+ auth.POST("/refresh", api.RefreshToken)
+
+ authProtected := auth.Group("/")
+ authProtected.Use(jwtMiddleware())
+ authProtected.GET("/", api.GetTokenClaims)
+ }
+
+ {
+ profile := router.Group("/profile")
+ profile.Use(jwtMiddleware())
+ profile.POST("/", api.CreateProfile)
+ profile.GET("/", api.GetProfile)
+ profile.PATCH("/", api.UpdateProfile)
+ }
+
+ {
+ interests := router.Group("/interests")
+ interests.Use(jwtMiddleware())
+ interests.PUT("/", api.UpdateInterests)
+ interests.GET("/", api.GetInterests)
+ interests.GET("/all", api.GetUsersByInterests)
+ interests.GET("/cats", api.GetAllInterests)
+ }
+
+ {
+ avatar := router.Group("/avatar")
+ avatar.Use(jwtMiddleware())
+ avatar.PUT("/", api.UpdateAvatar)
+ avatar.GET("/", api.GetAvatar)
+ }
+
+ return router
+}
diff --git a/backend/internal/services/auth_service.go b/backend/internal/services/auth_service.go
new file mode 100644
index 0000000..53e4240
--- /dev/null
+++ b/backend/internal/services/auth_service.go
@@ -0,0 +1,188 @@
+package services
+
+import (
+ "backend/internal/config"
+ "backend/internal/db"
+ "backend/internal/models"
+ "backend/pkg/utils"
+ "errors"
+ "github.com/golang-jwt/jwt/v5"
+ "golang.org/x/crypto/bcrypt"
+ "gorm.io/gorm"
+ "time"
+)
+
+var (
+ ErrUnknownUserOrPass error = errors.New("unknown user or password")
+ ErrUserExists error = errors.New("user already exists")
+ ErrInvalidInput error = errors.New("invalid input")
+ ErrInvalidToken error = errors.New("invalid token")
+ ErrCanNotGetClaims error = errors.New("can not get claims")
+ ErrInvalidRefreshToken error = errors.New("invalid refresh token")
+)
+
+func generateAccessToken(userID int, username string) (string, error) {
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "user_id": userID,
+ "username": username,
+ "exp": time.Now().Add(20 * time.Minute).Unix(),
+ })
+ return token.SignedString([]byte(config.AppConfig.JWT.SecretKey))
+}
+
+func generateRefreshToken(userID int) (string, error) {
+ expTime := time.Now().Add(14 * 24 * time.Hour)
+
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "user_id": userID,
+ "exp": expTime.Unix(),
+ })
+
+ tokenString, err := token.SignedString([]byte(config.AppConfig.JWT.SecretKey))
+
+ if err != nil {
+ return "", err
+ }
+
+ refreshToken := models.RefreshToken{
+ UserID: userID,
+ Token: tokenString,
+ ExpiresAt: expTime,
+ }
+
+ if err := db.DB.Create(&refreshToken).Error; err != nil {
+ return "", err
+ }
+
+ return tokenString, nil
+}
+
+func RegisterUser(username, password string) (string, string, error) {
+ // Hashing password
+ hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+ if err != nil {
+ return "", "", err
+ }
+
+ // Checking for existent user
+ var user models.User
+ if err := db.DB.First(&user, "username = ?", username).Error; err == nil {
+ return "", "", ErrUserExists
+ } else if !errors.Is(err, gorm.ErrRecordNotFound) {
+ return "", "", err
+ }
+
+ // Creating new user
+ user = models.User{
+ Username: username,
+ Password: string(hashed),
+ }
+ if err := db.DB.Create(&user).Error; err != nil {
+ return "", "", err
+ }
+
+ // Generating tokens
+ accessToken, err := generateAccessToken(user.Id, user.Username)
+ if err != nil {
+ return "", "", err
+ }
+
+ refreshToken, err := generateRefreshToken(user.Id)
+ if err != nil {
+ return "", "", err
+ }
+
+ return accessToken, refreshToken, nil
+}
+
+func LoginUser(username, password string) (string, string, error) {
+ // Searching user
+ var user models.User
+ if err := db.DB.First(&user, "username = ?", username).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return "", "", ErrUnknownUserOrPass
+ }
+ return "", "", err
+ }
+
+ // Checking passwords
+ if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
+ return "", "", ErrUnknownUserOrPass
+ }
+
+ // Generating tokens
+ accessToken, err := generateAccessToken(user.Id, user.Username)
+ if err != nil {
+ return "", "", err
+ }
+ refreshToken, err := generateRefreshToken(user.Id)
+ if err != nil {
+ return "", "", err
+ }
+
+ return accessToken, refreshToken, nil
+}
+
+func GetTokenClaims(token string) (models.TokenClaimsResponse, error) {
+ tokena, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
+ return []byte(config.AppConfig.JWT.SecretKey), nil
+ }, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
+
+ if err != nil {
+ return models.TokenClaimsResponse{}, ErrInvalidToken
+ }
+
+ if claims, ok := tokena.Claims.(jwt.MapClaims); ok {
+ var userId int
+ var username string
+ userId, err = utils.GetIntFromClaims(claims, "user_id")
+ if err != nil {
+ return models.TokenClaimsResponse{}, err
+ }
+
+ username, err = utils.GetStringFromClaims(claims, "username")
+ if err != nil {
+ return models.TokenClaimsResponse{}, err
+ }
+
+ return models.TokenClaimsResponse{
+ UserID: userId,
+ Username: username,
+ }, nil
+ } else {
+ return models.TokenClaimsResponse{}, ErrCanNotGetClaims
+ }
+}
+
+func RefreshAccessToken(refreshToken string) (string, error) {
+ var token models.RefreshToken
+ // Check if the refresh token exists and is not expired
+ if err := db.DB.Where("token = ?", refreshToken).First(&token).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return "", ErrInvalidRefreshToken
+ }
+ return "", err
+ }
+
+ // Check if the token is expired
+ if token.ExpiresAt.Before(time.Now()) {
+ if err := db.DB.Delete(&models.RefreshToken{}, "token = ?", refreshToken).Error; err != nil {
+ return "", err
+ }
+ return "", ErrInvalidRefreshToken
+ }
+
+ // Fetch user
+ var user models.User
+ if err := db.DB.First(&user, "id = ?", token.UserID).Error; err != nil {
+ return "", err
+ }
+
+ // Generate new access token
+ newAccessToken, err := generateAccessToken(user.Id, user.Username)
+ if err != nil {
+ return "", err
+ }
+
+ return newAccessToken, nil
+}
diff --git a/backend/internal/services/avatar_service.go b/backend/internal/services/avatar_service.go
new file mode 100644
index 0000000..03cddeb
--- /dev/null
+++ b/backend/internal/services/avatar_service.go
@@ -0,0 +1,105 @@
+package services
+
+import (
+ "backend/internal/config"
+ "backend/internal/db"
+ "backend/internal/models"
+ "errors"
+ "fmt"
+ "gorm.io/gorm"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "os"
+ "path/filepath"
+)
+
+var (
+ ErrInvalidFileType = errors.New("only JPEG and PNG files are allowed")
+ ErrFileTooLarge = errors.New("file size exceeds 2MB limit")
+ ErrFileReadFailed = errors.New("failed to read file")
+ ErrAvatarNotFound = errors.New("avatar not found")
+)
+
+func UploadAvatar(userID uint, file multipart.File, header *multipart.FileHeader) (string, error) {
+
+ if header.Size > config.AppConfig.Avatars.MaxFileSize {
+ return "", ErrFileTooLarge
+ }
+
+ ext := filepath.Ext(header.Filename)
+ if ext != ".jpg" && ext != ".jpeg" && ext != ".png" {
+ return "", ErrInvalidFileType
+ }
+
+ buffer := make([]byte, 512)
+ _, err := file.Read(buffer)
+ if err != nil && err != io.EOF {
+ return "", ErrFileReadFailed
+ }
+ mimeType := http.DetectContentType(buffer)
+ if mimeType != "image/jpeg" && mimeType != "image/png" {
+ return "", ErrInvalidFileType
+ }
+ _, err = file.Seek(0, io.SeekStart)
+ if err != nil {
+ return "", errors.New("failed to reset file pointer")
+ }
+
+ filename := fmt.Sprintf("avatar_%d%s", userID, ext)
+ filePath := filepath.Join(config.AppConfig.Avatars.UploadDir, filename)
+
+ out, err := os.Create(filePath)
+ if err != nil {
+ return "", errors.New("failed to save file")
+ }
+ defer out.Close()
+
+ _, err = io.Copy(out, file)
+
+ if err != nil {
+ return "", errors.New("failed to write file")
+ }
+
+ var avatar models.Avatar
+ err = db.DB.Where("user_id = ?", userID).First(&avatar).Error
+ if err == nil {
+
+ oldPath := avatar.Path
+ avatar.Path = filePath
+ err = db.DB.Save(&avatar).Error
+ if err != nil {
+ return "", errors.New("failed to update avatar in database")
+ }
+
+ if oldPath != filePath {
+ os.Remove(oldPath)
+ }
+ } else if errors.Is(err, gorm.ErrRecordNotFound) {
+
+ avatar = models.Avatar{UserID: userID, Path: filePath}
+ err = db.DB.Create(&avatar).Error
+ if err != nil {
+ return "", errors.New("failed to create avatar in database")
+ }
+ } else {
+ return "", errors.New("database error")
+ }
+
+ return filePath, nil
+}
+
+func GetAvatarPath(userID int) (string, error) {
+ var avatar models.Avatar
+
+ err := db.DB.Where("user_id = ?", userID).First(&avatar).Error
+
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return "", ErrAvatarNotFound
+ }
+ return "", err
+ }
+
+ return avatar.Path, nil
+}
diff --git a/backend/internal/services/interest_service.go b/backend/internal/services/interest_service.go
new file mode 100644
index 0000000..653d548
--- /dev/null
+++ b/backend/internal/services/interest_service.go
@@ -0,0 +1,140 @@
+package services
+
+import (
+ "backend/internal/config"
+ "backend/internal/db"
+ "backend/internal/models"
+ "errors"
+)
+
+var (
+ ErrUnknownInterest error = errors.New("unknown interest")
+)
+
+func SetInterests(claims *models.TokenClaimsResponse, interests []string) error {
+
+ interestSet := make(map[string]struct{})
+ for _, interest := range interests {
+ if _, exists := config.AppConfig.Interest.Interests[interest]; exists {
+ interestSet[interest] = struct{}{}
+ } else {
+ return ErrUnknownInterest
+ }
+ }
+
+ err := db.DB.Where("user_id = ?", claims.UserID).Delete(&models.Interest{}).Error
+ if err != nil {
+ return err
+ }
+
+ for interest := range interestSet {
+ err := db.DB.Create(&models.Interest{
+ UserId: claims.UserID,
+ InterestId: interest,
+ }).Error
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func GetInterests(claims *models.TokenClaimsResponse, userId int) ([]string, error) {
+ var neededId int
+
+ if userId == 0 {
+ neededId = claims.UserID
+ } else {
+ neededId = userId
+ }
+
+ var interests []models.Interest
+ err := db.DB.Where("user_id = ?", neededId).Find(&interests).Error
+
+ if err != nil {
+ return nil, err
+ }
+
+ ret := make([]string, 0, len(interests))
+
+ for _, interest := range interests {
+ ret = append(ret, interest.InterestId)
+ }
+
+ return ret, nil
+}
+
+func GetUsersByInterests(pageNum int, interests []string, pageSize int) (*models.PaginatedUsersResponse, error) {
+ if pageNum < 1 {
+ return nil, errors.New("invalid page number")
+ }
+
+ if len(interests) == 0 {
+ return nil, errors.New("interests list cannot be empty")
+ }
+
+ var paginatedUserIDs []int
+ err := db.DB.Table("interests").
+ Select("DISTINCT user_id").
+ Where("interest_id IN ?", interests).
+ Order("user_id ASC").
+ Offset((pageNum-1)*pageSize).
+ Limit(pageSize).
+ Pluck("user_id", &paginatedUserIDs).Error
+ if err != nil {
+ return nil, err
+ }
+
+ if len(paginatedUserIDs) == 0 {
+ return &models.PaginatedUsersResponse{PageNum: pageNum, Users: make(map[int]models.UserProfileWithInterests)}, nil
+ }
+
+ var profiles []models.Profile
+ err = db.DB.Where("user_id IN ?", paginatedUserIDs).Find(&profiles).Error
+ if err != nil {
+ return nil, err
+ }
+
+ userProfiles := make(map[int]models.Profile)
+ for _, profile := range profiles {
+ userProfiles[profile.UserId] = profile
+ }
+
+ var allInterests []models.Interest
+ err = db.DB.Where("user_id IN ?", paginatedUserIDs).Find(&allInterests).Error
+ if err != nil {
+ return nil, err
+ }
+
+ userInterests := make(map[int][]string)
+ for _, interest := range allInterests {
+ userInterests[interest.UserId] = append(userInterests[interest.UserId], interest.InterestId)
+ }
+
+ response := &models.PaginatedUsersResponse{
+ PageNum: pageNum,
+ Users: make(map[int]models.UserProfileWithInterests),
+ }
+
+ for _, userID := range paginatedUserIDs {
+ if profile, exists := userProfiles[userID]; exists {
+ response.Users[userID] = models.UserProfileWithInterests{
+ Profile: profile,
+ Interests: userInterests[userID],
+ }
+ }
+ }
+
+ return response, nil
+}
+
+func GetAllInterests() []string {
+ ret := make([]string, 0, len(config.AppConfig.Interest.Interests))
+
+ for interest := range config.AppConfig.Interest.Interests {
+ ret = append(ret, interest)
+ }
+
+ return ret
+}
diff --git a/backend/internal/services/profile_service.go b/backend/internal/services/profile_service.go
new file mode 100644
index 0000000..d540abf
--- /dev/null
+++ b/backend/internal/services/profile_service.go
@@ -0,0 +1,99 @@
+package services
+
+import (
+ "backend/internal/db"
+ "backend/internal/models"
+ "errors"
+ "gorm.io/gorm"
+)
+
+var (
+ ErrProfileDoesNotExists error = errors.New("profile does not exists")
+ ErrProfileAlreadyExists error = errors.New("profile already exists")
+)
+
+func CreateProfile(info *models.Profile) error {
+ var profile models.Profile
+
+ if err := db.DB.First(&profile, "user_id=?", info.UserId).Error; err == nil {
+ return ErrProfileAlreadyExists
+ } else if !errors.Is(err, gorm.ErrRecordNotFound) {
+ return err
+ }
+
+ profile = models.Profile{
+ UserId: info.UserId,
+ Name: info.Name,
+ Surname: info.Surname,
+ Age: info.Age,
+ Gender: info.Gender,
+ Telegram: info.Telegram,
+ Description: info.Description,
+ }
+
+ err := db.DB.Create(&profile).Error
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func GetProfile(claims *models.TokenClaimsResponse, userId int) (*models.Profile, error) {
+ var profile models.Profile
+ var neededId int
+
+ if userId == 0 {
+ neededId = claims.UserID
+ } else {
+ neededId = userId
+ }
+
+ if err := db.DB.First(&profile, "user_id=?", neededId).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, ErrProfileDoesNotExists
+ } else {
+ return nil, err
+ }
+ }
+
+ return &profile, nil
+}
+
+func UpdateProfile(userId int, updateReq *models.UpdateProfileRequest) (*models.Profile, error) {
+ var profile models.Profile
+
+ if err := db.DB.First(&profile, "user_id=?", userId).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, ErrProfileDoesNotExists
+ } else {
+ return nil, err
+ }
+ }
+
+ if updateReq.Name != nil {
+ profile.Name = *updateReq.Name
+ }
+ if updateReq.Surname != nil {
+ profile.Surname = *updateReq.Surname
+ }
+ if updateReq.Age != nil {
+ profile.Age = *updateReq.Age
+ }
+ if updateReq.Gender != nil {
+ profile.Gender = *updateReq.Gender
+ }
+ if updateReq.Description != nil {
+ profile.Description = *updateReq.Description
+ }
+ if updateReq.Telegram != nil {
+ profile.Telegram = *updateReq.Telegram
+ }
+
+ if err := db.DB.Save(&profile).Error; err != nil {
+ return nil, err
+ }
+
+ return &profile, nil
+}
diff --git a/backend/pkg/utils/shortcuts.go b/backend/pkg/utils/shortcuts.go
new file mode 100644
index 0000000..03b49bb
--- /dev/null
+++ b/backend/pkg/utils/shortcuts.go
@@ -0,0 +1,36 @@
+package utils
+
+import (
+ "errors"
+ "github.com/golang-jwt/jwt/v5"
+)
+
+var (
+ ErrClaimDoesNotExists error = errors.New("claim does not exists")
+ ErrClaimIsNotNumeric error = errors.New("claim is not numeric value")
+ ErrClaimIsNotString error = errors.New("claim is not string value")
+)
+
+func GetIntFromClaims(claims jwt.MapClaims, key string) (int, error) {
+ if num, exists := claims[key]; exists {
+ if val, ok := num.(float64); ok {
+ return int(val), nil
+ } else {
+ return 0, ErrClaimIsNotNumeric
+ }
+ } else {
+ return 0, ErrClaimDoesNotExists
+ }
+}
+
+func GetStringFromClaims(claims jwt.MapClaims, key string) (string, error) {
+ if str, exists := claims[key]; exists {
+ if val, ok := str.(string); ok {
+ return val, nil
+ } else {
+ return "", ErrClaimIsNotString
+ }
+ } else {
+ return "", ErrClaimDoesNotExists
+ }
+}
diff --git a/compose.yaml b/compose.yaml
index bf81b4a..bee8837 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,6 +1,33 @@
services:
+ postgres:
+ image: postgres:latest
+ environment:
+ - POSTGRES_USER=gowithme
+ - POSTGRES_PASSWORD=gwm-pwd-123
+ - POSTGRES_DB=gowithme
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ restart: always
+
backend:
build: ./backend/
-
+ environment:
+ - POSTGRES_HOST=postgres
+ - POSTGRES_USER=gowithme
+ - POSTGRES_PASSWORD=gwm-pwd-123
+ - POSTGRES_DB=gowithme
+ - SECRET_KEY=guten_tag
+ ports:
+ - "8081:8081"
+ depends_on:
+ - postgres
+
frontend:
build: ./frontend/
+ ports:
+ - "8080:80"
+ depends_on:
+ - backend
+
+volumes:
+ pgdata:
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..79c113f
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,45 @@
+# 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
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+
+# 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
diff --git a/frontend/.metadata b/frontend/.metadata
new file mode 100644
index 0000000..603c641
--- /dev/null
+++ b/frontend/.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: "6fba2447e95c451518584c35e25f5433f14d888c"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+ base_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+ - platform: android
+ create_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+ base_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+ - platform: ios
+ create_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+ base_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+ - platform: linux
+ create_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+ base_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+ - platform: macos
+ create_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+ base_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+ - platform: web
+ create_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+ base_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+ - platform: windows
+ create_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+ base_revision: 6fba2447e95c451518584c35e25f5433f14d888c
+
+ # 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/frontend/Dockerfile b/frontend/Dockerfile
index b6dbda7..c57b05e 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -1,2 +1,26 @@
-# TODO
+FROM ubuntu:latest
+RUN apt-get update && apt-get install -y \
+ curl git unzip zip nginx ca-certificates && \
+ apt-get clean
+
+RUN git ls-remote https://github.com/flutter/flutter.git || exit 1
+
+RUN git clone --single-branch --branch stable --depth 1 https://github.com/flutter/flutter.git /flutter
+
+ENV PATH="$PATH:/flutter/bin:/flutter/bin/cache/dart-sdk/bin"
+
+RUN flutter channel stable && \
+ flutter upgrade && \
+ flutter config --enable-web
+
+COPY . /app
+WORKDIR /app
+
+RUN flutter pub get && flutter build web --release
+
+RUN mkdir -p /usr/share/nginx/html && cp -r build/web/* /usr/share/nginx/html
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..9207519
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1 @@
+## Application
\ No newline at end of file
diff --git a/frontend/analysis_options.yaml b/frontend/analysis_options.yaml
new file mode 100644
index 0000000..f16eead
--- /dev/null
+++ b/frontend/analysis_options.yaml
@@ -0,0 +1,4 @@
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ rules:
\ No newline at end of file
diff --git a/frontend/android/.gitignore b/frontend/android/.gitignore
new file mode 100644
index 0000000..be3943c
--- /dev/null
+++ b/frontend/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/frontend/android/app/build.gradle.kts b/frontend/android/app/build.gradle.kts
new file mode 100644
index 0000000..8e7198b
--- /dev/null
+++ b/frontend/android/app/build.gradle.kts
@@ -0,0 +1,43 @@
+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.example.frontend"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = "27.0.12077973"
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_11.toString()
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "com.example.frontend"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+}
+
+flutter {
+ source = "../.."
+}
diff --git a/frontend/android/app/src/debug/AndroidManifest.xml b/frontend/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/frontend/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0fd500e
--- /dev/null
+++ b/frontend/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/android/app/src/main/kotlin/com/example/frontend/MainActivity.kt b/frontend/android/app/src/main/kotlin/com/example/frontend/MainActivity.kt
new file mode 100644
index 0000000..1bd2dee
--- /dev/null
+++ b/frontend/android/app/src/main/kotlin/com/example/frontend/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.example.frontend
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()
diff --git a/frontend/android/app/src/main/res/drawable-v21/launch_background.xml b/frontend/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/frontend/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/android/app/src/main/res/drawable/launch_background.xml b/frontend/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/frontend/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/frontend/android/app/src/main/res/values-night/styles.xml b/frontend/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/frontend/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/frontend/android/app/src/main/res/values/styles.xml b/frontend/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/frontend/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/frontend/android/app/src/profile/AndroidManifest.xml b/frontend/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/frontend/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/frontend/android/build.gradle.kts b/frontend/android/build.gradle.kts
new file mode 100644
index 0000000..89176ef
--- /dev/null
+++ b/frontend/android/build.gradle.kts
@@ -0,0 +1,21 @@
+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/frontend/android/gradle.properties b/frontend/android/gradle.properties
new file mode 100644
index 0000000..f018a61
--- /dev/null
+++ b/frontend/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/frontend/android/gradle/wrapper/gradle-wrapper.properties b/frontend/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..ac3b479
--- /dev/null
+++ b/frontend/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.12-all.zip
diff --git a/frontend/android/settings.gradle.kts b/frontend/android/settings.gradle.kts
new file mode 100644
index 0000000..ab39a10
--- /dev/null
+++ b/frontend/android/settings.gradle.kts
@@ -0,0 +1,25 @@
+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.7.3" apply false
+ id("org.jetbrains.kotlin.android") version "2.1.0" apply false
+}
+
+include(":app")
diff --git a/frontend/assets/fonts/Montserrat-Bold.ttf b/frontend/assets/fonts/Montserrat-Bold.ttf
new file mode 100644
index 0000000..02ff6ff
Binary files /dev/null and b/frontend/assets/fonts/Montserrat-Bold.ttf differ
diff --git a/frontend/assets/fonts/Montserrat-Medium.ttf b/frontend/assets/fonts/Montserrat-Medium.ttf
new file mode 100644
index 0000000..dfbcfe4
Binary files /dev/null and b/frontend/assets/fonts/Montserrat-Medium.ttf differ
diff --git a/frontend/assets/fonts/Montserrat-Regular.ttf b/frontend/assets/fonts/Montserrat-Regular.ttf
new file mode 100644
index 0000000..48ba65e
Binary files /dev/null and b/frontend/assets/fonts/Montserrat-Regular.ttf differ
diff --git a/frontend/assets/fonts/Montserrat-SemiBold.ttf b/frontend/assets/fonts/Montserrat-SemiBold.ttf
new file mode 100644
index 0000000..8dbcdb3
Binary files /dev/null and b/frontend/assets/fonts/Montserrat-SemiBold.ttf differ
diff --git a/frontend/assets/images/logo.png b/frontend/assets/images/logo.png
new file mode 100644
index 0000000..5af7077
Binary files /dev/null and b/frontend/assets/images/logo.png differ
diff --git a/frontend/assets/translations/en.json b/frontend/assets/translations/en.json
new file mode 100644
index 0000000..117238c
--- /dev/null
+++ b/frontend/assets/translations/en.json
@@ -0,0 +1,99 @@
+{
+ "profile": {
+ "title": "Profile",
+ "firstName": "First Name",
+ "lastName": "Last Name",
+ "age": "Age",
+ "telegram": "Telegram",
+ "description": "Description",
+ "save": "Save",
+ "genderFemale": "F",
+ "genderMale": "M",
+ "updateSuccess": "Profile updated",
+ "chooseAvatar": "Upload photo",
+ "activities": "Activities"
+ },
+ "settings": {
+ "settings": "Settings",
+ "language": "Language",
+ "russian": "Russian",
+ "english": "English",
+ "theme": "Theme",
+ "system": "System",
+ "dark": "Dark",
+ "light": "Light"
+ },
+ "search": {
+ "find": "Find",
+ "notFound": "No users found"
+ },
+ "home": {
+ "search": "Search",
+ "profile": "Profile",
+ "searchTitle": "Find a Companion"
+ },
+ "auth": {
+ "login": "Login",
+ "register": "Register",
+ "password": "Password",
+ "confirmPassword": "Retry password",
+ "error": "An error occurred"
+ },
+ "userProfile": {
+ "title": "Profile",
+ "yearsOld": "years old",
+ "interests": "Interests",
+ "messageOnTelegram": "Message on Telegram"
+ },
+ "errors": {
+ "400": "Invalid data. Please check your input.",
+ "401": "Authorization failed. Please log in again.",
+ "403": "You do not have access to this resource.",
+ "404": "Data not found.",
+ "409": "This user already exists.",
+ "500": "Internal server error. Please try again later.",
+ "502": "Server issues. Please try again later.",
+ "503": "Server temporarily unavailable.",
+ "default": "An error occurred. Please try again."
+ },
+ "interests": {
+ "swimming": "Swimming",
+ "cycling": "Cycling",
+ "football": "Football",
+ "basketball": "Basketball",
+ "tennis": "Tennis",
+ "running": "Running",
+ "yoga": "Yoga",
+ "golf": "Golf",
+ "skiing": "Skiing",
+ "surfing": "Surfing",
+ "hiking": "Hiking",
+ "climbing": "Climbing",
+ "kayaking": "Kayaking",
+ "fishing": "Fishing",
+ "horse riding": "Horse Riding",
+ "nature walks": "Nature Walks",
+ "beach walks": "Beach Walks",
+ "dog walking": "Dog Walking",
+ "photography walks": "Photography Walks",
+ "bird watching": "Bird Watching",
+ "fitness": "Fitness",
+ "weightlifting": "Weightlifting",
+ "crossfit": "CrossFit",
+ "pilates": "Pilates",
+ "dancing": "Dancing",
+ "skateboarding": "Skateboarding",
+ "snowboarding": "Snowboarding",
+ "paragliding": "Paragliding",
+ "diving": "Diving",
+ "martial arts": "Martial Arts",
+ "healthy food": "Healthy food"
+ },
+ "logout": {
+ "button": "Log out",
+ "title": "Log out",
+ "confirm": "Are you sure you want to log out?",
+ "yes": "Yes",
+ "no": "No"
+ }
+}
\ No newline at end of file
diff --git a/frontend/assets/translations/ru.json b/frontend/assets/translations/ru.json
new file mode 100644
index 0000000..5049441
--- /dev/null
+++ b/frontend/assets/translations/ru.json
@@ -0,0 +1,99 @@
+{
+ "settings": {
+ "settings": "Настройки",
+ "language": "Язык",
+ "russian": "Русский",
+ "english": "Английский",
+ "theme": "Тема",
+ "system": "Системная",
+ "dark": "Тёмная",
+ "light": "Светлая"
+ },
+ "profile": {
+ "title": "Профиль",
+ "firstName": "Имя",
+ "lastName": "Фамилия",
+ "age": "Возраст",
+ "telegram": "Telegram",
+ "description": "Описание",
+ "save": "Сохранить",
+ "genderFemale": "Ж",
+ "genderMale": "М",
+ "updateSuccess": "Профиль обновлён",
+ "chooseAvatar": "Загрузить фото",
+ "activities": "Интересы"
+ },
+ "search": {
+ "find": "Найти",
+ "notFound": "Пользователи не найдены"
+ },
+ "home": {
+ "search": "Поиск",
+ "profile": "Профиль",
+ "searchTitle": "Найти компаньона"
+ },
+ "auth": {
+ "login": "Войти",
+ "register": "Регистрация",
+ "password": "Пароль",
+ "confirmPassword": "Повторите пароль",
+ "error": "Произошла ошибка"
+ },
+ "userProfile": {
+ "title": "Профиль",
+ "yearsOld": "лет",
+ "interests": "Интересы",
+ "messageOnTelegram": "Написать в Telegram"
+ },
+ "errors": {
+ "400": "Некорректные данные. Проверьте введённую информацию.",
+ "401": "Авторизация не удалась. Попробуйте войти снова.",
+ "403": "У вас нет доступа к этому ресурсу.",
+ "404": "Данные не найдены.",
+ "409": "Этот пользователь уже существует.",
+ "500": "Произошла внутренняя ошибка сервера. Попробуйте позже.",
+ "502": "Проблемы с сервером. Повторите позже.",
+ "503": "Сервер временно недоступен.",
+ "default": "Произошла ошибка. Повторите попытку."
+ },
+ "interests": {
+ "swimming": "Плавание",
+ "cycling": "Велоспорт",
+ "football": "Футбол",
+ "basketball": "Баскетбол",
+ "tennis": "Теннис",
+ "running": "Бег",
+ "yoga": "Йога",
+ "golf": "Гольф",
+ "skiing": "Лыжи",
+ "surfing": "Сёрфинг",
+ "hiking": "Походы",
+ "climbing": "Скалолазание",
+ "kayaking": "Каякинг",
+ "fishing": "Рыбалка",
+ "horse riding": "Верховая езда",
+ "nature walks": "Прогулки на природе",
+ "beach walks": "Прогулки по пляжу",
+ "dog walking": "Выгул собак",
+ "photography walks": "Фотопрогулки",
+ "bird watching": "Наблюдение за птицами",
+ "fitness": "Фитнес",
+ "weightlifting": "Тяжёлая атлетика",
+ "crossfit": "Кроссфит",
+ "pilates": "Пилатес",
+ "dancing": "Танцы",
+ "skateboarding": "Скейтбординг",
+ "snowboarding": "Сноуборд",
+ "paragliding": "Парапланеризм",
+ "diving": "Дайвинг",
+ "martial arts": "Боевые искусства",
+ "healthy food": "Здоровое питание"
+ },
+ "logout": {
+ "button": "Выйти",
+ "title": "Выход",
+ "confirm": "Вы уверены, что хотите выйти?",
+ "yes": "Да",
+ "no": "Нет"
+ }
+}
\ No newline at end of file
diff --git a/frontend/devtools_options.yaml b/frontend/devtools_options.yaml
new file mode 100644
index 0000000..fa0b357
--- /dev/null
+++ b/frontend/devtools_options.yaml
@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:
diff --git a/frontend/ios/.gitignore b/frontend/ios/.gitignore
new file mode 100644
index 0000000..7a7f987
--- /dev/null
+++ b/frontend/ios/.gitignore
@@ -0,0 +1,34 @@
+**/dgph
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/frontend/ios/Flutter/AppFrameworkInfo.plist b/frontend/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..7c56964
--- /dev/null
+++ b/frontend/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 12.0
+
+
diff --git a/frontend/ios/Flutter/Debug.xcconfig b/frontend/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..ec97fc6
--- /dev/null
+++ b/frontend/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/frontend/ios/Flutter/Release.xcconfig b/frontend/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..c4855bf
--- /dev/null
+++ b/frontend/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/frontend/ios/Podfile b/frontend/ios/Podfile
new file mode 100644
index 0000000..e549ee2
--- /dev/null
+++ b/frontend/ios/Podfile
@@ -0,0 +1,43 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '12.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/frontend/ios/Podfile.lock b/frontend/ios/Podfile.lock
new file mode 100644
index 0000000..2398aa5
--- /dev/null
+++ b/frontend/ios/Podfile.lock
@@ -0,0 +1,42 @@
+PODS:
+ - Flutter (1.0.0)
+ - image_picker_ios (0.0.1):
+ - Flutter
+ - path_provider_foundation (0.0.1):
+ - Flutter
+ - FlutterMacOS
+ - shared_preferences_foundation (0.0.1):
+ - Flutter
+ - FlutterMacOS
+ - url_launcher_ios (0.0.1):
+ - Flutter
+
+DEPENDENCIES:
+ - Flutter (from `Flutter`)
+ - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
+ - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
+ - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
+ - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
+
+EXTERNAL SOURCES:
+ Flutter:
+ :path: Flutter
+ image_picker_ios:
+ :path: ".symlinks/plugins/image_picker_ios/ios"
+ path_provider_foundation:
+ :path: ".symlinks/plugins/path_provider_foundation/darwin"
+ shared_preferences_foundation:
+ :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
+ url_launcher_ios:
+ :path: ".symlinks/plugins/url_launcher_ios/ios"
+
+SPEC CHECKSUMS:
+ Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+ image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
+ path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
+ shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
+ url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
+
+PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
+
+COCOAPODS: 1.16.2
diff --git a/frontend/ios/Runner.xcodeproj/project.pbxproj b/frontend/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..4946cac
--- /dev/null
+++ b/frontend/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,730 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 2086AB28BF8EB8A3C17393B6 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFC40FC68F08E3AB823C9CB9 /* Pods_RunnerTests.framework */; };
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+ CEDDA4F55408030FE7E4AB64 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 433212155117BA6740700A57 /* Pods_Runner.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+ remoteInfo = Runner;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 106CCA9724C28540621C08F8 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 433212155117BA6740700A57 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 48330C29DB2F5923A554A6AB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 72A03884C56EACB9C66F5928 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 7EC9D2E0CF45E19ADCEE43F5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 7FC3B518A963F4CCBA13E086 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 98E6B23C4D892897248AC6BC /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
+ BFC40FC68F08E3AB823C9CB9 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 0651ACE149C37E6991AFB142 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 2086AB28BF8EB8A3C17393B6 /* Pods_RunnerTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ CEDDA4F55408030FE7E4AB64 /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 331C8082294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C807B294A618700263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 4EA73D261B856C7E551366CE /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 433212155117BA6740700A57 /* Pods_Runner.framework */,
+ BFC40FC68F08E3AB823C9CB9 /* Pods_RunnerTests.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 331C8082294A63A400263BE5 /* RunnerTests */,
+ AE153A222F1425F0D51F2A14 /* Pods */,
+ 4EA73D261B856C7E551366CE /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ AE153A222F1425F0D51F2A14 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 7EC9D2E0CF45E19ADCEE43F5 /* Pods-Runner.debug.xcconfig */,
+ 48330C29DB2F5923A554A6AB /* Pods-Runner.release.xcconfig */,
+ 106CCA9724C28540621C08F8 /* Pods-Runner.profile.xcconfig */,
+ 7FC3B518A963F4CCBA13E086 /* Pods-RunnerTests.debug.xcconfig */,
+ 98E6B23C4D892897248AC6BC /* Pods-RunnerTests.release.xcconfig */,
+ 72A03884C56EACB9C66F5928 /* Pods-RunnerTests.profile.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C8080294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 30378B0D995F263478402709 /* [CP] Check Pods Manifest.lock */,
+ 331C807D294A63A400263BE5 /* Sources */,
+ 331C807F294A63A400263BE5 /* Resources */,
+ 0651ACE149C37E6991AFB142 /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ D9E739C5051E13DF485E9744 /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ 89C1BFC95742AC679A7F380B /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastUpgradeCheck = 1510;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 331C8080294A63A400263BE5 = {
+ CreatedOnToolsVersion = 14.0;
+ TestTargetID = 97C146ED1CF9000F007C117D;
+ };
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ 331C8080294A63A400263BE5 /* RunnerTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 331C807F294A63A400263BE5 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 30378B0D995F263478402709 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 89C1BFC95742AC679A7F380B /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+ D9E739C5051E13DF485E9744 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 331C807D294A63A400263BE5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 97C146ED1CF9000F007C117D /* Runner */;
+ targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = X4QZG2T5PZ;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.frontend.gowithme;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 331C8088294A63A400263BE5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7FC3B518A963F4CCBA13E086 /* Pods-RunnerTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.frontend.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Debug;
+ };
+ 331C8089294A63A400263BE5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 98E6B23C4D892897248AC6BC /* Pods-RunnerTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.frontend.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Release;
+ };
+ 331C808A294A63A400263BE5 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 72A03884C56EACB9C66F5928 /* Pods-RunnerTests.profile.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.frontend.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = X4QZG2T5PZ;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.frontend.gowithme;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = X4QZG2T5PZ;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.frontend.gowithme;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 331C8088294A63A400263BE5 /* Debug */,
+ 331C8089294A63A400263BE5 /* Release */,
+ 331C808A294A63A400263BE5 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/frontend/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/frontend/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/frontend/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/frontend/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..e3773d4
--- /dev/null
+++ b/frontend/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/ios/Runner.xcworkspace/contents.xcworkspacedata b/frontend/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/frontend/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/frontend/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/frontend/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/frontend/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/frontend/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/frontend/ios/Runner/AppDelegate.swift b/frontend/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..6266644
--- /dev/null
+++ b/frontend/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import Flutter
+import UIKit
+
+@main
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..7353c41
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..797d452
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..6ed2d93
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cd7b00
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..fe73094
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..321773c
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..797d452
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..502f463
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..0ec3034
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..0ec3034
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..e9f5fea
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..84ac32a
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..8953cba
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..0467bf1
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/frontend/ios/Runner/Base.lproj/LaunchScreen.storyboard b/frontend/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/frontend/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/ios/Runner/Base.lproj/Main.storyboard b/frontend/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/frontend/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/ios/Runner/Info.plist b/frontend/ios/Runner/Info.plist
new file mode 100644
index 0000000..d33b63b
--- /dev/null
+++ b/frontend/ios/Runner/Info.plist
@@ -0,0 +1,49 @@
+
+
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Frontend
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ frontend
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UIApplicationSupportsIndirectInputEvents
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/frontend/ios/Runner/Runner-Bridging-Header.h b/frontend/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/frontend/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/frontend/ios/RunnerTests/RunnerTests.swift b/frontend/ios/RunnerTests/RunnerTests.swift
new file mode 100644
index 0000000..86a7c3b
--- /dev/null
+++ b/frontend/ios/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Flutter
+import UIKit
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+ func testExample() {
+ // If you add code to the Runner application, consider adding tests here.
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+ }
+
+}
diff --git a/frontend/lib/data/api/api_client.dart b/frontend/lib/data/api/api_client.dart
new file mode 100644
index 0000000..c853193
--- /dev/null
+++ b/frontend/lib/data/api/api_client.dart
@@ -0,0 +1,82 @@
+import 'package:dio/dio.dart';
+import 'package:get_storage/get_storage.dart';
+import 'package:frontend/data/enums/get_storage_key.dart';
+import 'package:frontend/data/functions/show_api_error.dart';
+
+final baseUrl = "http://mhdserver.ru:8081";
+// final baseUrl = "http://127.0.0.1:8081/"
+
+class ApiClient {
+ final GetStorage storage = GetStorage();
+ final Dio dio = Dio(
+ BaseOptions(
+ baseUrl: baseUrl,
+ connectTimeout: const Duration(seconds: 60),
+ receiveTimeout: const Duration(seconds: 60),
+ contentType: 'application/json',
+ ),
+ );
+
+ ApiClient() {
+ dio.interceptors.add(
+ InterceptorsWrapper(
+ onRequest: (options, handler) async {
+ options.headers['Accept'] = "application/json";
+ options.followRedirects = true;
+ String? token = storage.read(GetStorageKey.accessToken.value);
+ options.headers['Authorization'] = "Bearer $token";
+ return handler.next(options);
+ },
+
+ onError: (e, handler) async {
+ final suppressNotification =
+ e.requestOptions.extra['suppressErrorNotification'] == true;
+
+ if (e.response?.statusCode == 401) {
+ final newAccessToken = await refreshToken();
+ if (newAccessToken != null) {
+ await storage.write(
+ GetStorageKey.accessToken.value,
+ newAccessToken,
+ );
+ dio.options.headers['Authorization'] = "Bearer $newAccessToken";
+ return handler.resolve(await dio.fetch(e.requestOptions));
+ } else {
+ storage.erase();
+ }
+ }
+ if (!suppressNotification) {
+ showApiError(e);
+ }
+ return handler.next(e);
+ },
+ ),
+ );
+ }
+
+ Future refreshToken() async {
+ try {
+ final refreshToken = await storage.read(GetStorageKey.refreshToken.value);
+ final response = await dio.post(
+ '/auth/refresh',
+ data: {'refresh_token': refreshToken},
+ );
+
+ final newAccessToken = response.data['access_token'];
+ await storage.write(GetStorageKey.accessToken.value, newAccessToken);
+ return newAccessToken;
+ } catch (e) {
+ await clearTokens();
+ return null;
+ }
+ }
+
+ Future saveTokens(String accessToken, String refreshToken) async {
+ await storage.write(GetStorageKey.accessToken.value, accessToken);
+ await storage.write(GetStorageKey.refreshToken.value, refreshToken);
+ }
+
+ Future clearTokens() async {
+ await storage.erase();
+ }
+}
diff --git a/frontend/lib/data/enums/get_storage_key.dart b/frontend/lib/data/enums/get_storage_key.dart
new file mode 100644
index 0000000..0d86821
--- /dev/null
+++ b/frontend/lib/data/enums/get_storage_key.dart
@@ -0,0 +1,8 @@
+enum GetStorageKey {
+ accessToken('accessToken'),
+ refreshToken('refreshToken');
+
+ final String value;
+
+ const GetStorageKey(this.value);
+}
diff --git a/frontend/lib/data/functions/open_url.dart b/frontend/lib/data/functions/open_url.dart
new file mode 100644
index 0000000..30c16bd
--- /dev/null
+++ b/frontend/lib/data/functions/open_url.dart
@@ -0,0 +1,8 @@
+import 'package:url_launcher/url_launcher.dart';
+
+Future openUrl(String urlString) async {
+ final Uri url = Uri.parse(urlString);
+ if (!await launchUrl(url)) {
+ throw Exception('Could not launch $url');
+ }
+}
diff --git a/frontend/lib/data/functions/show_api_error.dart b/frontend/lib/data/functions/show_api_error.dart
new file mode 100644
index 0000000..385fc97
--- /dev/null
+++ b/frontend/lib/data/functions/show_api_error.dart
@@ -0,0 +1,20 @@
+import 'package:dio/dio.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:frontend/main.dart';
+
+void showApiError(DioException error) {
+ final statusCode = error.response?.statusCode;
+
+ final message = 'errors.${statusCode ?? 'default'}'.tr();
+
+ final messenger = rootScaffoldMessengerKey.currentState;
+ messenger?.clearSnackBars();
+ messenger?.showSnackBar(
+ SnackBar(
+ content: Text(message),
+ backgroundColor: Colors.red,
+ duration: Duration(seconds: 4),
+ ),
+ );
+}
diff --git a/frontend/lib/data/functions/text_to_string.dart b/frontend/lib/data/functions/text_to_string.dart
new file mode 100644
index 0000000..cae5404
--- /dev/null
+++ b/frontend/lib/data/functions/text_to_string.dart
@@ -0,0 +1,9 @@
+import 'package:flutter/material.dart';
+
+String themeToString(ThemeMode mode) {
+ return switch (mode) {
+ ThemeMode.light => 'light',
+ ThemeMode.dark => 'dark',
+ ThemeMode.system => 'system',
+ };
+}
diff --git a/frontend/lib/data/functions/validations.dart b/frontend/lib/data/functions/validations.dart
new file mode 100644
index 0000000..18ce168
--- /dev/null
+++ b/frontend/lib/data/functions/validations.dart
@@ -0,0 +1,29 @@
+String? validateLogin(String login) {
+ if (login.isEmpty) {
+ return 'Please enter a login';
+ }
+ return null;
+}
+
+String? validatePassword(String password) {
+ if (password.isEmpty) {
+ return 'Please enter a password';
+ }
+ if (password.length < 6) {
+ return 'Password must be at least 6 characters';
+ }
+ return null;
+}
+
+String? validatePasswordConfirmation(
+ String password,
+ String passwordConfirmation,
+) {
+ if (password != passwordConfirmation) {
+ return 'Passwords do not match';
+ }
+ if (password.isEmpty) {
+ return 'Please enter a password';
+ }
+ return null;
+}
diff --git a/frontend/lib/data/models/user.dart b/frontend/lib/data/models/user.dart
new file mode 100644
index 0000000..0c2f235
--- /dev/null
+++ b/frontend/lib/data/models/user.dart
@@ -0,0 +1,40 @@
+import 'dart:typed_data';
+
+class User {
+ final String name;
+ final String surname;
+ final int age;
+ final List interests;
+ final int id;
+ final String? gender;
+ final String? description;
+ final String? telegram;
+ Uint8List? avatarBytes;
+
+ User({
+ required this.name,
+ required this.surname,
+ required this.age,
+ required this.interests,
+ required this.id,
+ required this.gender,
+ required this.description,
+ required this.telegram,
+ this.avatarBytes,
+ });
+
+ factory User.fromJson(Map json) {
+ final profile = json['profile'];
+ final interests = List.from(json['interests'] ?? []);
+ return User(
+ name: profile['name'],
+ surname: profile['surname'],
+ age: profile['age'],
+ interests: interests,
+ id: profile['user_id'],
+ gender: profile['gender'],
+ description: profile['description'],
+ telegram: profile['telegram'],
+ );
+ }
+}
diff --git a/frontend/lib/data/repositories/auth_repository.dart b/frontend/lib/data/repositories/auth_repository.dart
new file mode 100644
index 0000000..57309ca
--- /dev/null
+++ b/frontend/lib/data/repositories/auth_repository.dart
@@ -0,0 +1,36 @@
+import 'package:frontend/domain/services/app_services.dart';
+
+import '../api/api_client.dart';
+
+class AuthRepository {
+ final ApiClient apiClient;
+
+ AuthRepository(this.apiClient);
+
+ Future login(String username, String password) async {
+ final response = await apiClient.dio.post(
+ '/auth/login',
+ queryParameters: {'username': username, 'password': password},
+ );
+
+ final data = response.data;
+ final accessToken = data['access_token'];
+ final refreshToken = data['refresh_token'];
+
+ await apiClient.saveTokens(accessToken, refreshToken);
+ }
+
+ Future register(String username, String password) async {
+ final response = await apiClient.dio.post(
+ '/auth/register',
+ queryParameters: {'username': username, 'password': password},
+ );
+
+ final data = response.data;
+ final accessToken = data['access_token'];
+ final refreshToken = data['refresh_token'];
+
+ await apiClient.saveTokens(accessToken, refreshToken);
+ await profileRepository.createProfile();
+ }
+}
diff --git a/frontend/lib/data/repositories/profile_repository.dart b/frontend/lib/data/repositories/profile_repository.dart
new file mode 100644
index 0000000..49d798f
--- /dev/null
+++ b/frontend/lib/data/repositories/profile_repository.dart
@@ -0,0 +1,133 @@
+import 'dart:typed_data';
+
+import 'package:dio/dio.dart';
+import 'package:frontend/data/api/api_client.dart';
+import 'package:frontend/domain/services/shared_preferences_service.dart';
+
+class ProfileRepository {
+ final SharedPreferencesService prefs;
+ final ApiClient apiClient;
+
+ ProfileRepository(this.apiClient, this.prefs);
+
+ Future