diff --git a/labs/lab04/README.md b/labs/lab04/README.md deleted file mode 100644 index e34e6ba0b..000000000 --- a/labs/lab04/README.md +++ /dev/null @@ -1,399 +0,0 @@ -# Lab 04: Database & Persistence - -Welcome to Lab 04! In this lab, you'll learn about database fundamentals and implement persistence solutions in both Go and Flutter applications. - -## ๐ŸŽฏ Lab Overview - -This lab introduces **Database & Persistence** concepts by implementing data storage solutions in both Go (backend) and Flutter (frontend). - -### ๐Ÿ”ง Database Approaches Covered - -This lab demonstrates **4 different approaches** to database interaction in Go, allowing you to compare and contrast different patterns: - -#### 1. **Manual SQL** (`user_repository.go`) -- **Approach**: Raw SQL queries with `database/sql` package -- **Pros**: Maximum control, best performance, clear SQL queries -- **Cons**: More boilerplate, manual row scanning, SQL injection risk -- **Use Case**: When you need precise control over queries and performance - -#### 2. **Scany Mapping** (`post_repository.go`) -- **Approach**: Raw SQL queries + automatic struct mapping -- **Library**: `github.com/georgysavva/scany/v2/sqlscan` -- **Pros**: Eliminates manual scanning, type-safe, good performance -- **Cons**: Still requires SQL knowledge, limited query building -- **Use Case**: When you want SQL control but easier result mapping - -#### 3. **Squirrel Query Builder** (`search_service.go`) -- **Approach**: Dynamic query building with fluent API -- **Library**: `github.com/Masterminds/squirrel` -- **Pros**: Type-safe query building, dynamic conditions, readable code -- **Cons**: Learning curve, abstraction overhead -- **Use Case**: When you need dynamic queries with many conditional filters - -#### 4. **GORM ORM** (`category_repository.go`) -- **Approach**: Full Object-Relational Mapping -- **Library**: `gorm.io/gorm` -- **Pros**: Rapid development, automatic migrations, associations -- **Cons**: Less control, potential N+1 queries, steeper learning curve -- **Use Case**: When you want rapid development and don't mind abstraction - -### ๐ŸŽฏ What You'll Learn - -Compare these approaches by implementing similar functionality: -- **Performance**: Benchmark different approaches -- **Code Complexity**: See boilerplate vs. abstraction trade-offs -- **Type Safety**: Experience compile-time vs. runtime error detection -- **Maintainability**: Understand long-term code maintenance implications - -### ๐Ÿ—๏ธ Project Structure - -``` -lab04/ -โ”œโ”€โ”€ backend/ # Go backend with database operations -โ”‚ โ”œโ”€โ”€ models/ # User and Post data models -โ”‚ โ”œโ”€โ”€ database/ # Database connection and migrations -โ”‚ โ”œโ”€โ”€ repository/ # CRUD operations and data access -โ”‚ โ”œโ”€โ”€ main.go # Application entry point -โ”‚ โ””โ”€โ”€ go.mod # Go dependencies -โ”œโ”€โ”€ frontend/ # Flutter frontend with local storage -โ”‚ โ”œโ”€โ”€ lib/ -โ”‚ โ”‚ โ”œโ”€โ”€ models/ # Dart data models -โ”‚ โ”‚ โ”œโ”€โ”€ services/ # Storage services -โ”‚ โ”‚ โ””โ”€โ”€ screens/ # UI screens -โ”‚ โ”œโ”€โ”€ test/ # Unit tests -โ”‚ โ””โ”€โ”€ pubspec.yaml # Flutter dependencies -โ””โ”€โ”€ README.md # This file -``` - -## ๐Ÿ”ง Setup Instructions - -### Backend Setup (Go) - -1. Navigate to the backend directory: -```bash -cd labs/lab04/backend -``` - -2. Install dependencies: -```bash -go mod tidy -``` - -3. Run the application: -```bash -go run main.go -``` - -4. Run tests: -```bash -go test ./... -``` - -### Frontend Setup (Flutter) - -1. Navigate to the frontend directory: -```bash -cd labs/lab04/frontend -``` - -2. Install dependencies: -```bash -flutter pub get -``` - -3. Generate JSON serialization code: -```bash -flutter packages pub run build_runner build -``` - -4. Run the app: -```bash -flutter run -``` - -5. Run tests: -```bash -flutter test -``` - -## ๐Ÿ“ Tasks Overview - -### Go Backend Tasks - Multiple Database Approaches - -Lab 4 covers **multiple database approaches** with **3 NECESSARY tasks** for evaluation plus **additional OPTIONAL tasks** for deeper learning: - ---- - -#### โœ… **NECESSARY TASKS** (Required for Lab Completion) - -#### Task 1: Manual SQL Repository (`user_repository.go`) ๐Ÿ”ด **REQUIRED** -**Approach:** Raw SQL with `database/sql` package โšก - -- โœ… **Maximum Control**: Write your own SQL queries -- โœ… **Best Performance**: No ORM overhead -- โœ… **Manual Scanning**: Explicit row-to-struct mapping - -**TODO Items:** -- `UserRepository.Create()` - Raw SQL INSERT with parameter binding -- `UserRepository.GetByID()` - Manual SELECT with row.Scan() -- `UserRepository.Update()` - Dynamic UPDATE with prepared statements -- `User.ScanRow()` - Manual database row scanning - -#### Task 2: Database Infrastructure (`database/connection.go`, `database/migrations.go`) ๐Ÿ”ด **REQUIRED** -**Approach:** Standard database setup with goose migrations ๐Ÿ—๏ธ - -- โœ… **Connection Management**: Proper database connection pooling -- โœ… **Migration System**: Goose-based schema management -- โœ… **Production Ready**: Configurable and maintainable setup - -**TODO Items:** -- `InitDB()` - Standard database/sql connection setup -- `RunMigrations()` - Execute goose migrations programmatically -- Connection pooling configuration and management -- **Makefile Commands**: `make migrate-up`, `make migrate-down`, `make migrate-status` - -#### Task 3: Data Models (`models/user.go`, `models/post.go`) ๐Ÿ”ด **REQUIRED** -**Approach:** Go structs with manual validation and mapping ๐Ÿ“ฆ - -- โœ… **Struct Design**: Proper JSON tags and database mapping -- โœ… **Validation**: Input validation for data integrity -- โœ… **Manual Mapping**: Row scanning and data conversion - -**TODO Items:** -- `User.Validate()` - Data validation logic -- `CreateUserRequest.ToUser()` - Request to model conversion -- `User.ScanRow()` - Database row to struct mapping -- Similar patterns for Post model - ---- - -#### ๐ŸŽฏ **OPTIONAL TASKS** (For Advanced Learning) - -#### Task 4: Scany Mapping Repository (`post_repository.go`) ๐ŸŸก **OPTIONAL** -**Approach:** Raw SQL + automatic struct mapping ๐Ÿ”„ - -- โœ… **SQL Control**: Write SQL, skip manual scanning -- โœ… **Type Safety**: Automatic struct mapping with validation -- โœ… **Good Performance**: Minimal reflection overhead - -**TODO Items:** -- `PostRepository.Create()` - Use `sqlscan.Get()` for RETURNING queries -- `PostRepository.GetAll()` - Use `sqlscan.Select()` for slice mapping -- `PostRepository.Search()` - Complex WHERE with automatic scanning - -#### Task 5: Squirrel Query Builder (`search_service.go`) ๐ŸŸก **OPTIONAL** -**Approach:** Dynamic query building with fluent API ๐Ÿ—๏ธ - -- โœ… **Dynamic Queries**: Build complex queries programmatically -- โœ… **Type Safety**: Compile-time query validation -- โœ… **Readable Code**: Fluent API instead of string concatenation - -**TODO Items:** -- `SearchService.SearchPosts()` - Dynamic filters with Squirrel -- `SearchService.GetPostStats()` - Complex JOINs and aggregation -- `SearchService.BuildDynamicQuery()` - Modular query building - -#### Task 6: GORM ORM Repository (`category_repository.go`) ๐ŸŸก **OPTIONAL** -**Approach:** Full Object-Relational Mapping ๐Ÿš€ - -- โœ… **Rapid Development**: High-level database operations -- โœ… **Auto Relationships**: Preload associations automatically -- โœ… **Model Hooks**: Lifecycle management with hooks - -**TODO Items:** -- `CategoryRepository.Create()` - GORM Create with auto-timestamps -- `CategoryRepository.GetCategoriesWithPosts()` - GORM Preload -- `Category.BeforeCreate()` - GORM lifecycle hooks -- `CategoryRepository.CreateWithTransaction()` - GORM transactions - ---- - -#### ๐Ÿ“š **Why Multiple Approaches?** - -| Approach | Performance | Development Speed | Learning Value | Production Use | -|----------|-------------|-------------------|----------------|----------------| -| **Manual SQL** โšก | โญโญโญโญโญ | โญโญ | โญโญโญโญโญ | High-performance apps | -| **Scany Mapping** ๐Ÿ”„ | โญโญโญโญ | โญโญโญโญ | โญโญโญโญ | Balanced control+convenience | -| **Squirrel Builder** ๐Ÿ—๏ธ | โญโญโญ | โญโญโญ | โญโญโญ | Dynamic query scenarios | -| **GORM ORM** ๐Ÿš€ | โญโญ | โญโญโญโญโญ | โญโญโญ | Rapid prototyping | - -**Focus on the 3 NECESSARY tasks first, then explore OPTIONAL ones based on interest!** - -### Flutter Frontend Tasks - -#### Task 4: SharedPreferences Service ๐Ÿ”ด **REQUIRED** -**File:** `frontend/lib/services/preferences_service.dart` - -Implement simple key-value storage: - -- โœ… **Basic Operations**: String, int, bool, list storage -- โœ… **Object Storage**: JSON serialization for complex objects -- โœ… **Key Management**: Check existence, get all keys, clear data -- โœ… **Type Safety**: Proper null handling and type conversion - -**TODO Items to Complete:** -- `init()` - Initialize SharedPreferences instance -- `setString()`, `getString()` - String value operations -- `setInt()`, `getInt()` - Integer value operations -- `setBool()`, `getBool()` - Boolean value operations -- `setStringList()`, `getStringList()` - String list operations -- `setObject()`, `getObject()` - JSON object operations -- `remove()`, `clear()` - Data cleanup operations -- `containsKey()`, `getAllKeys()` - Key management - -#### Task 5: SQLite Database Service ๐Ÿ”ด **REQUIRED** -**File:** `frontend/lib/services/database_service.dart` - -Implement local SQLite database: - -- โœ… **Database Setup**: Initialize SQLite with proper schema -- โœ… **User CRUD**: Complete user operations -- โœ… **Search Functionality**: Query users by name/email -- โœ… **Database Management**: Path management, cleanup, migrations - -**TODO Items to Complete:** -- `database` getter - Return database instance or initialize -- `_initDatabase()` - Initialize SQLite database -- `_onCreate()` - Create database tables and schema -- `createUser()` - Insert user into database -- `getUser()` - Get user by ID -- `getAllUsers()` - Get all users ordered by creation -- `updateUser()` - Update user with dynamic fields -- `deleteUser()` - Delete user from database -- `getUserCount()` - Count total users -- `searchUsers()` - Search users by name/email -- `closeDatabase()`, `clearAllData()` - Database management -- `getDatabasePath()` - Get database file path - -#### Task 6: Secure Storage Service ๐Ÿ”ด **REQUIRED** -**File:** `frontend/lib/services/secure_storage_service.dart` - -Implement encrypted storage for sensitive data: - -- โœ… **Authentication**: Token and credential storage -- โœ… **User Preferences**: Biometric and security settings -- โœ… **Generic Storage**: Custom key-value secure storage -- โœ… **Object Storage**: Encrypted JSON object storage -- โœ… **Key Management**: List keys, check existence, export data - -**TODO Items to Complete:** -- `saveAuthToken()`, `getAuthToken()` - Authentication token management -- `deleteAuthToken()` - Remove authentication token -- `saveUserCredentials()`, `getUserCredentials()` - Login credentials -- `deleteUserCredentials()` - Remove stored credentials -- `saveBiometricEnabled()`, `isBiometricEnabled()` - Biometric settings -- `saveSecureData()`, `getSecureData()` - Generic secure storage -- `deleteSecureData()` - Remove secure data by key -- `saveObject()`, `getObject()` - Encrypted object storage -- `containsKey()`, `getAllKeys()` - Key management -- `clearAll()` - Remove all secure data -- `exportData()` - Export all data (for backup) - -## ๐Ÿงช Testing - -### Go Tests - -```bash -# Run all tests -go test ./... - -# Run specific package tests -go test ./models -go test ./database -go test ./repository - -# Run tests with verbose output -go test -v ./... -``` - -### Flutter Tests - -```bash -# Run all tests -flutter test - -# Run specific test file -flutter test test/preferences_service_test.dart -flutter test test/database_service_test.dart -flutter test test/secure_storage_service_test.dart - -# Run tests with coverage -flutter test --coverage -``` - -## ๐Ÿ’ก Implementation Tips - -### Go Backend - -1. **Database Connection**: Use connection pooling for production -2. **SQL Injection**: Always use prepared statements -3. **Error Handling**: Return appropriate error types -4. **Validation**: Validate data before database operations -5. **Transactions**: Use transactions for related operations - -### Flutter Frontend - -1. **Initialization**: Initialize services in main() before runApp() -2. **Error Handling**: Handle storage exceptions gracefully -3. **Type Safety**: Use proper null safety patterns -4. **Performance**: Use batch operations for multiple inserts -5. **Security**: Never store sensitive data in SharedPreferences - -## ๐ŸŽจ Storage Patterns - -### When to Use Each Storage Type - -| Storage Type | Use Cases | Examples | -|--------------|-----------|----------| -| **SharedPreferences** | Simple settings, flags | Theme, language, user preferences | -| **SQLite** | Structured data, relationships | Users, posts, complex queries | -| **Secure Storage** | Sensitive information | Tokens, passwords, API keys | - -### Data Flow Patterns - -1. **Cache-First**: Check local storage before API calls -2. **API-First**: Always fetch from server, cache locally -3. **Offline-First**: Work offline by default, sync when online - -## ๐Ÿ”’ Security Considerations - -1. **Never store sensitive data in SharedPreferences** -2. **Use Secure Storage for authentication tokens** -3. **Validate all user input before storage** -4. **Use proper encryption for sensitive local data** -5. **Implement proper session management** - -## ๐Ÿš€ Bonus Challenges - -1. **Advanced Queries**: Implement complex SQL queries with joins -2. **Migration System**: Add database schema versioning -3. **Sync Mechanism**: Implement offline-first with sync -4. **Caching Strategy**: Add intelligent caching layer -5. **Performance Monitoring**: Add query performance tracking - -## ๐Ÿ“– Resources - -- [Go database/sql Documentation](https://pkg.go.dev/database/sql) -- [SQLite Documentation](https://www.sqlite.org/docs.html) -- [Flutter SharedPreferences](https://pub.dev/packages/shared_preferences) -- [Flutter sqflite](https://pub.dev/packages/sqflite) -- [Flutter Secure Storage](https://pub.dev/packages/flutter_secure_storage) - -## ๐ŸŽฏ Success Criteria - -Your lab is complete when: -- โœ… All Go tests pass (models, database, repository) -- โœ… All Flutter tests pass (preferences, database, secure storage) -- โœ… You can create, read, update, and delete data in both backends -- โœ… You understand the trade-offs between different storage types -- โœ… You can implement basic data synchronization patterns - -## ๐Ÿ†˜ Getting Help - -If you're stuck: -1. Check the TODO comments in the code for guidance -2. Review the test files to understand expected behavior -3. Consult the documentation links above -4. Ask questions in the course discussion forum - -Good luck, and enjoy learning about database and persistence patterns! ๐ŸŽ‰ \ No newline at end of file diff --git a/labs/lab04/backend/Makefile b/labs/lab04/backend/Makefile deleted file mode 100644 index 0952d3593..000000000 --- a/labs/lab04/backend/Makefile +++ /dev/null @@ -1,131 +0,0 @@ -# Go database migration management with goose -# Usage: make migrate-up, make migrate-down, make migrate-status, etc. - -# Database configuration -DATABASE_URL ?= ./lab04.db -MIGRATIONS_DIR = ./migrations - -# Default target -.PHONY: help -help: - @echo "Available commands:" - @echo " make migrate-up - Run all pending migrations" - @echo " make migrate-down - Rollback last migration" - @echo " make migrate-status - Show migration status" - @echo " make migrate-reset - Reset database (DROP ALL TABLES)" - @echo " make migrate-create - Create new migration (usage: make migrate-create NAME=add_new_table)" - @echo " make install-goose - Install goose migration tool" - @echo " make clean-db - Remove database file" - @echo " make setup-db - Clean and setup fresh database" - -# Install goose if not present -.PHONY: install-goose -install-goose: - @which goose > /dev/null || go install github.com/pressly/goose/v3/cmd/goose@latest - @echo "โœ… Goose migration tool ready" - -# Run all pending migrations -.PHONY: migrate-up -migrate-up: install-goose - @echo "๐Ÿš€ Running migrations..." - @goose -dir $(MIGRATIONS_DIR) sqlite3 $(DATABASE_URL) up - @echo "โœ… Migrations completed" - -# Rollback last migration -.PHONY: migrate-down -migrate-down: install-goose - @echo "โช Rolling back last migration..." - @goose -dir $(MIGRATIONS_DIR) sqlite3 $(DATABASE_URL) down - @echo "โœ… Rollback completed" - -# Show migration status -.PHONY: migrate-status -migrate-status: install-goose - @echo "๐Ÿ“Š Migration status:" - @goose -dir $(MIGRATIONS_DIR) sqlite3 $(DATABASE_URL) status - -# Reset database (WARNING: removes all data) -.PHONY: migrate-reset -migrate-reset: install-goose - @echo "โš ๏ธ WARNING: This will remove ALL data!" - @read -p "Are you sure? (y/N): " confirm && [ "$$confirm" = "y" ] - @goose -dir $(MIGRATIONS_DIR) sqlite3 $(DATABASE_URL) reset - @echo "๐Ÿ—‘๏ธ Database reset completed" - -# Create new migration -.PHONY: migrate-create -migrate-create: install-goose - @if [ -z "$(NAME)" ]; then \ - echo "โŒ Error: NAME is required. Usage: make migrate-create NAME=add_new_table"; \ - exit 1; \ - fi - @echo "๐Ÿ“ Creating migration: $(NAME)" - @goose -dir $(MIGRATIONS_DIR) create $(NAME) sql - @echo "โœ… Migration created in $(MIGRATIONS_DIR)/" - -# Remove database file -.PHONY: clean-db -clean-db: - @echo "๐Ÿ—‘๏ธ Removing database file..." - @rm -f $(DATABASE_URL) - @echo "โœ… Database file removed" - -# Setup fresh database -.PHONY: setup-db -setup-db: clean-db migrate-up - @echo "๐ŸŽ‰ Fresh database setup completed!" - -# Run tests with fresh database -.PHONY: test-with-fresh-db -test-with-fresh-db: setup-db - @echo "๐Ÿงช Running tests with fresh database..." - @go test ./... - -# Show database schema (requires sqlite3 command) -.PHONY: show-schema -show-schema: - @echo "๐Ÿ“‹ Database schema:" - @sqlite3 $(DATABASE_URL) ".schema" - -# Show all tables -.PHONY: show-tables -show-tables: - @echo "๐Ÿ“Š Database tables:" - @sqlite3 $(DATABASE_URL) ".tables" - -# Backup database -.PHONY: backup-db -backup-db: - @echo "๐Ÿ’พ Creating database backup..." - @cp $(DATABASE_URL) "./lab04_backup_$(shell date +%Y%m%d_%H%M%S).db" - @echo "โœ… Backup created" - -# Development helpers -.PHONY: dev-setup -dev-setup: install-goose setup-db - @echo "๐Ÿ‘จโ€๐Ÿ’ป Development environment setup completed!" - @echo "๐Ÿ“š Next steps:" - @echo " - Run 'make test-with-fresh-db' to verify setup" - @echo " - Use 'make migrate-create NAME=your_migration' to add migrations" - @echo " - Use 'make migrate-status' to check migration state" - -# Run go mod tidy -.PHONY: tidy -tidy: - @echo "๐Ÿงน Running go mod tidy..." - @go mod tidy - @echo "โœ… Dependencies updated" - -# Run all tests -.PHONY: test -test: - @echo "๐Ÿงช Running all tests..." - @go test ./... -v - -# Run tests with coverage -.PHONY: test-coverage -test-coverage: - @echo "๐Ÿ“Š Running tests with coverage..." - @go test ./... -cover -coverprofile=coverage.out - @go tool cover -html=coverage.out -o coverage.html - @echo "โœ… Coverage report generated: coverage.html" \ No newline at end of file diff --git a/labs/lab04/backend/README.md b/labs/lab04/backend/README.md deleted file mode 100644 index ced0500e2..000000000 --- a/labs/lab04/backend/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# Lab 04 Backend - Database & Persistence - -Go backend demonstrating multiple database approaches and migration management. - -## ๐Ÿ› ๏ธ Migration Commands - -### Quick Start -```bash -# Setup fresh database -make setup-db - -# Install dependencies -make tidy -``` - -### Migration Management -```bash -# Run all pending migrations -make migrate-up - -# Rollback last migration -make migrate-down - -# Check migration status -make migrate-status - -# Create new migration -make migrate-create NAME=add_new_feature - -# Reset database (โš ๏ธ removes all data) -make migrate-reset -``` - -### Development Commands -```bash -# Show all available commands -make help - -# Run tests -make test - -# Run tests with coverage -make test-coverage - -# Database inspection -make show-schema # Show full schema -make show-tables # List all tables - -# Database management -make clean-db # Remove database file -make backup-db # Create timestamped backup -``` - -## ๐Ÿ“ Migration Files - -Migrations are stored in `../migrations/` directory: -- `20250708090008_create_users_table.sql` -- `20250708090034_create_posts_table.sql` -- `20250708090055_create_categories_table.sql` - -## ๐ŸŽฏ Task Structure - -### โœ… NECESSARY Tasks (Required) -1. **Data Models** (`models/user.go`, `models/post.go`) -2. **Database Infrastructure** (`database/connection.go`, `database/migrations.go`) -3. **Manual SQL Repository** (`repository/user_repository.go`) - -### ๐ŸŸก OPTIONAL Tasks (Advanced Learning) -4. **Scany Mapping** (`repository/post_repository.go`) -5. **Squirrel Builder** (`services/search_service.go`) -6. **GORM ORM** (`repository/category_repository.go`) - -## ๐Ÿ—„๏ธ Database Schema - -The migrations create these tables: -- **users**: User accounts with soft delete support -- **posts**: Blog posts with user relationships -- **categories**: Category system for GORM examples -- **post_categories**: Many-to-many junction table - -All tables include proper indexes for performance and foreign key constraints for data integrity. - -## ๐Ÿš€ Next Steps - -1. Complete the 3 necessary tasks first -2. Run tests: `make test-with-fresh-db` -3. Explore optional approaches based on interest -4. Study different database patterns and trade-offs \ No newline at end of file diff --git a/labs/lab04/backend/database/connection.go b/labs/lab04/backend/database/connection.go deleted file mode 100644 index 49db9deeb..000000000 --- a/labs/lab04/backend/database/connection.go +++ /dev/null @@ -1,58 +0,0 @@ -package database - -import ( - "database/sql" - "fmt" - "time" - - _ "github.com/mattn/go-sqlite3" -) - -// Config holds database configuration -type Config struct { - DatabasePath string - MaxOpenConns int - MaxIdleConns int - ConnMaxLifetime time.Duration - ConnMaxIdleTime time.Duration -} - -// DefaultConfig returns a default database configuration -func DefaultConfig() *Config { - return &Config{ - DatabasePath: "./lab04.db", - MaxOpenConns: 25, - MaxIdleConns: 5, - ConnMaxLifetime: 5 * time.Minute, - ConnMaxIdleTime: 2 * time.Minute, - } -} - -// TODO: Implement InitDB function -func InitDB() (*sql.DB, error) { - // TODO: Initialize database connection with SQLite - // - Open database connection using sqlite3 driver - // - Apply connection pool configuration from DefaultConfig() - // - Test connection with Ping() - // - Return the database connection or error - return nil, fmt.Errorf("TODO: implement InitDB function") -} - -// TODO: Implement InitDBWithConfig function -func InitDBWithConfig(config *Config) (*sql.DB, error) { - // TODO: Initialize database connection with custom configuration - // - Open database connection using the provided config - // - Apply all connection pool settings - // - Test connection with Ping() - // - Return the database connection or error - return nil, fmt.Errorf("TODO: implement InitDBWithConfig function") -} - -// TODO: Implement CloseDB function -func CloseDB(db *sql.DB) error { - // TODO: Properly close database connection - // - Check if db is not nil - // - Close the database connection - // - Return any error that occurs - return fmt.Errorf("TODO: implement CloseDB function") -} diff --git a/labs/lab04/backend/database/connection_test.go b/labs/lab04/backend/database/connection_test.go deleted file mode 100644 index 915353fff..000000000 --- a/labs/lab04/backend/database/connection_test.go +++ /dev/null @@ -1,196 +0,0 @@ -package database - -import ( - "os" - "testing" - "time" -) - -func TestDefaultConfig(t *testing.T) { - config := DefaultConfig() - - if config == nil { - t.Fatal("DefaultConfig() returned nil") - } - - if config.DatabasePath == "" { - t.Error("DefaultConfig() DatabasePath should not be empty") - } - - if config.MaxOpenConns <= 0 { - t.Error("DefaultConfig() MaxOpenConns should be positive") - } - - if config.MaxIdleConns <= 0 { - t.Error("DefaultConfig() MaxIdleConns should be positive") - } - - if config.ConnMaxLifetime <= 0 { - t.Error("DefaultConfig() ConnMaxLifetime should be positive") - } - - if config.ConnMaxIdleTime <= 0 { - t.Error("DefaultConfig() ConnMaxIdleTime should be positive") - } -} - -func TestInitDB(t *testing.T) { - // Clean up test database before and after - testDB := "./test_init.db" - defer os.Remove(testDB) - os.Remove(testDB) - - // Test with default config - db, err := InitDB() - if err != nil { - t.Fatalf("InitDB() failed: %v", err) - } - - if db == nil { - t.Fatal("InitDB() returned nil database") - } - - // Test that we can ping the database - if err := db.Ping(); err != nil { - t.Errorf("Database ping failed: %v", err) - } - - // Clean up - if err := CloseDB(db); err != nil { - t.Errorf("CloseDB() failed: %v", err) - } -} - -func TestInitDBWithConfig(t *testing.T) { - // Clean up test database before and after - testDB := "./test_config.db" - defer os.Remove(testDB) - os.Remove(testDB) - - // Create custom config - config := &Config{ - DatabasePath: testDB, - MaxOpenConns: 10, - MaxIdleConns: 2, - ConnMaxLifetime: 2 * time.Minute, - ConnMaxIdleTime: 1 * time.Minute, - } - - db, err := InitDBWithConfig(config) - if err != nil { - t.Fatalf("InitDBWithConfig() failed: %v", err) - } - - if db == nil { - t.Fatal("InitDBWithConfig() returned nil database") - } - - // Test that we can ping the database - if err := db.Ping(); err != nil { - t.Errorf("Database ping failed: %v", err) - } - - // Clean up - if err := CloseDB(db); err != nil { - t.Errorf("CloseDB() failed: %v", err) - } -} - -func TestMigrate(t *testing.T) { - // Clean up test database before and after - testDB := "./test_migrate.db" - defer os.Remove(testDB) - os.Remove(testDB) - - config := &Config{ - DatabasePath: testDB, - MaxOpenConns: 5, - MaxIdleConns: 1, - ConnMaxLifetime: 1 * time.Minute, - ConnMaxIdleTime: 30 * time.Second, - } - - db, err := InitDBWithConfig(config) - if err != nil { - t.Fatalf("InitDBWithConfig() failed: %v", err) - } - defer CloseDB(db) - - // Test migration - err = RunMigrations(db) - if err != nil { - t.Fatalf("RunMigrations() failed: %v", err) - } - - // Test that tables were created by checking if we can query them - _, err = db.Exec("SELECT COUNT(*) FROM users") - if err != nil { - t.Errorf("Users table not created properly: %v", err) - } - - _, err = db.Exec("SELECT COUNT(*) FROM posts") - if err != nil { - t.Errorf("Posts table not created properly: %v", err) - } - - // Test that we can insert data (basic schema validation) - _, err = db.Exec(` - INSERT INTO users (name, email, created_at, updated_at) - VALUES ('Test User', 'test@example.com', datetime('now'), datetime('now')) - `) - if err != nil { - t.Errorf("Cannot insert into users table: %v", err) - } - - // Get the user ID for the post test - var userID int - err = db.QueryRow("SELECT id FROM users WHERE email = 'test@example.com'").Scan(&userID) - if err != nil { - t.Errorf("Cannot query user: %v", err) - } - - _, err = db.Exec(` - INSERT INTO posts (user_id, title, content, published, created_at, updated_at) - VALUES (?, 'Test Post', 'Test content', 1, datetime('now'), datetime('now')) - `, userID) - if err != nil { - t.Errorf("Cannot insert into posts table: %v", err) - } -} - -func TestCloseDB(t *testing.T) { - // Test closing nil database - err := CloseDB(nil) - if err == nil { - t.Error("CloseDB(nil) should return an error") - } - - // Test closing valid database - testDB := "./test_close.db" - defer os.Remove(testDB) - os.Remove(testDB) - - config := &Config{ - DatabasePath: testDB, - MaxOpenConns: 5, - MaxIdleConns: 1, - ConnMaxLifetime: 1 * time.Minute, - ConnMaxIdleTime: 30 * time.Second, - } - - db, err := InitDBWithConfig(config) - if err != nil { - t.Fatalf("InitDBWithConfig() failed: %v", err) - } - - err = CloseDB(db) - if err != nil { - t.Errorf("CloseDB() failed: %v", err) - } - - // Test that database is actually closed - err = db.Ping() - if err == nil { - t.Error("Database should be closed and ping should fail") - } -} diff --git a/labs/lab04/backend/database/migrations.go b/labs/lab04/backend/database/migrations.go deleted file mode 100644 index 08f61adf0..000000000 --- a/labs/lab04/backend/database/migrations.go +++ /dev/null @@ -1,48 +0,0 @@ -package database - -import ( - "database/sql" - "fmt" - - "github.com/pressly/goose/v3" -) - -// RunMigrations runs database migrations using goose -func RunMigrations(db *sql.DB) error { - if db == nil { - return fmt.Errorf("database connection cannot be nil") - } - - // Set goose dialect for SQLite - if err := goose.SetDialect("sqlite3"); err != nil { - return fmt.Errorf("failed to set goose dialect: %v", err) - } - - // Get path to migrations directory (relative to backend directory) - migrationsDir := "../migrations" - - // Run migrations from the migrations directory - if err := goose.Up(db, migrationsDir); err != nil { - return fmt.Errorf("failed to run migrations: %v", err) - } - - return nil -} - -// TODO: Implement this function -// RollbackMigration rolls back the last migration using goose -func RollbackMigration(db *sql.DB) error { - return nil -} - -// TODO: Implement this function -// GetMigrationStatus checks migration status using goose -func GetMigrationStatus(db *sql.DB) error { - return nil -} - -// TODO: Implement this function -// CreateMigration creates a new migration file -func CreateMigration(name string) error { - return nil -} diff --git a/labs/lab04/backend/go.mod b/labs/lab04/backend/go.mod deleted file mode 100644 index 51ffc7e54..000000000 --- a/labs/lab04/backend/go.mod +++ /dev/null @@ -1,22 +0,0 @@ -module lab04-backend - -go 1.24 - -require ( - github.com/Masterminds/squirrel v1.5.4 - github.com/mattn/go-sqlite3 v1.14.22 - github.com/pressly/goose/v3 v3.24.3 - gorm.io/gorm v1.25.12 -) - -require ( - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect - github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect - github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/mfridman/interpolate v0.0.2 // indirect - github.com/sethvargo/go-retry v0.3.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sync v0.14.0 // indirect - golang.org/x/text v0.25.0 // indirect -) diff --git a/labs/lab04/backend/go.sum b/labs/lab04/backend/go.sum deleted file mode 100644 index a4aff6f60..000000000 --- a/labs/lab04/backend/go.sum +++ /dev/null @@ -1,57 +0,0 @@ -github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= -github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -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/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -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/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= -github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -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/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwWlM= -github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= -github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -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/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= -gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y= -modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= -modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= -modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= diff --git a/labs/lab04/backend/main.go b/labs/lab04/backend/main.go deleted file mode 100644 index 501ef29ba..000000000 --- a/labs/lab04/backend/main.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "lab04-backend/database" - "lab04-backend/repository" - - _ "github.com/mattn/go-sqlite3" -) - -func main() { - // TODO: Initialize database connection - db, err := database.InitDB() - if err != nil { - log.Fatal("Failed to initialize database:", err) - } - defer db.Close() - - // TODO: Run migrations (using goose-based approach) - if err := database.RunMigrations(db); err != nil { - log.Fatal("Failed to run migrations:", err) - } - - // TODO: Create repository instances - userRepo := repository.NewUserRepository(db) - postRepo := repository.NewPostRepository(db) - - // Demo operations - fmt.Println("Database initialized successfully!") - fmt.Printf("User repository: %T\n", userRepo) - fmt.Printf("Post repository: %T\n", postRepo) - - // TODO: Add some demo data operations here - // You can test your CRUD operations -} diff --git a/labs/lab04/backend/migrations/20250708090008_create_users_table.sql b/labs/lab04/backend/migrations/20250708090008_create_users_table.sql deleted file mode 100644 index cf82e01c3..000000000 --- a/labs/lab04/backend/migrations/20250708090008_create_users_table.sql +++ /dev/null @@ -1,27 +0,0 @@ --- +goose Up --- +goose StatementBegin --- Create users table with proper schema -CREATE TABLE users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(100) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL, - password_hash VARCHAR(255), - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - deleted_at DATETIME NULL -); - --- Create index for efficient email lookups -CREATE INDEX idx_users_email ON users(email); - --- Create index for soft delete queries -CREATE INDEX idx_users_deleted_at ON users(deleted_at); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin --- Drop the users table and its indexes -DROP INDEX IF EXISTS idx_users_deleted_at; -DROP INDEX IF EXISTS idx_users_email; -DROP TABLE users; --- +goose StatementEnd diff --git a/labs/lab04/backend/migrations/20250708090034_create_posts_table.sql b/labs/lab04/backend/migrations/20250708090034_create_posts_table.sql deleted file mode 100644 index 2f34b6d5d..000000000 --- a/labs/lab04/backend/migrations/20250708090034_create_posts_table.sql +++ /dev/null @@ -1,37 +0,0 @@ --- +goose Up --- +goose StatementBegin --- Create posts table with foreign key relationship to users -CREATE TABLE posts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - title VARCHAR(200) NOT NULL, - content TEXT, - published BOOLEAN DEFAULT FALSE, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - deleted_at DATETIME NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - --- Create index for user posts lookup -CREATE INDEX idx_posts_user_id ON posts(user_id); - --- Create index for published posts -CREATE INDEX idx_posts_published ON posts(published); - --- Create index for soft delete queries -CREATE INDEX idx_posts_deleted_at ON posts(deleted_at); - --- Create composite index for common queries -CREATE INDEX idx_posts_user_published ON posts(user_id, published); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin --- Drop the posts table and its indexes -DROP INDEX IF EXISTS idx_posts_user_published; -DROP INDEX IF EXISTS idx_posts_deleted_at; -DROP INDEX IF EXISTS idx_posts_published; -DROP INDEX IF EXISTS idx_posts_user_id; -DROP TABLE posts; --- +goose StatementEnd diff --git a/labs/lab04/backend/migrations/20250708090055_create_categories_table.sql b/labs/lab04/backend/migrations/20250708090055_create_categories_table.sql deleted file mode 100644 index ca5db60bd..000000000 --- a/labs/lab04/backend/migrations/20250708090055_create_categories_table.sql +++ /dev/null @@ -1,43 +0,0 @@ --- +goose Up --- +goose StatementBegin --- Create categories table for GORM example -CREATE TABLE categories ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(100) NOT NULL UNIQUE, - description VARCHAR(500), - color VARCHAR(7), -- Hex color code - active BOOLEAN DEFAULT TRUE, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - deleted_at DATETIME NULL -- For GORM soft delete -); - --- Create many-to-many junction table for posts and categories -CREATE TABLE post_categories ( - post_id INTEGER NOT NULL, - category_id INTEGER NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (post_id, category_id), - FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, - FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE -); - --- Create indexes for efficient lookups -CREATE INDEX idx_categories_name ON categories(name); -CREATE INDEX idx_categories_active ON categories(active); -CREATE INDEX idx_categories_deleted_at ON categories(deleted_at); -CREATE INDEX idx_post_categories_post_id ON post_categories(post_id); -CREATE INDEX idx_post_categories_category_id ON post_categories(category_id); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin --- Drop the categories tables and indexes -DROP INDEX IF EXISTS idx_post_categories_category_id; -DROP INDEX IF EXISTS idx_post_categories_post_id; -DROP INDEX IF EXISTS idx_categories_deleted_at; -DROP INDEX IF EXISTS idx_categories_active; -DROP INDEX IF EXISTS idx_categories_name; -DROP TABLE post_categories; -DROP TABLE categories; --- +goose StatementEnd diff --git a/labs/lab04/backend/models/category.go b/labs/lab04/backend/models/category.go deleted file mode 100644 index 058435605..000000000 --- a/labs/lab04/backend/models/category.go +++ /dev/null @@ -1,127 +0,0 @@ -package models - -import ( - "time" - - "gorm.io/gorm" -) - -// Category represents a blog post category using GORM model conventions -// This model demonstrates GORM ORM patterns and relationships -type Category struct { - ID uint `json:"id" gorm:"primaryKey"` - Name string `json:"name" gorm:"size:100;not null;uniqueIndex"` - Description string `json:"description" gorm:"size:500"` - Color string `json:"color" gorm:"size:7"` // Hex color code - Active bool `json:"active" gorm:"default:true"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // Soft delete support - - // GORM Associations (demonstrates ORM relationships) - Posts []Post `json:"posts,omitempty" gorm:"many2many:post_categories;"` -} - -// CreateCategoryRequest represents the payload for creating a category -type CreateCategoryRequest struct { - Name string `json:"name" validate:"required,min=2,max=100"` - Description string `json:"description" validate:"max=500"` - Color string `json:"color" validate:"omitempty,hexcolor"` -} - -// UpdateCategoryRequest represents the payload for updating a category -type UpdateCategoryRequest struct { - Name *string `json:"name,omitempty" validate:"omitempty,min=2,max=100"` - Description *string `json:"description,omitempty" validate:"omitempty,max=500"` - Color *string `json:"color,omitempty" validate:"omitempty,hexcolor"` - Active *bool `json:"active,omitempty"` -} - -// TODO: Implement GORM model methods and hooks - -// TableName specifies the table name for GORM (optional - GORM auto-infers) -func (Category) TableName() string { - return "categories" -} - -// TODO: Implement BeforeCreate hook -func (c *Category) BeforeCreate(tx *gorm.DB) error { - // TODO: GORM BeforeCreate hook - // - Validate data before creation - // - Set default values - // - Perform any pre-creation logic - // Example: if c.Color == "" { c.Color = "#007bff" } - return nil -} - -// TODO: Implement AfterCreate hook -func (c *Category) AfterCreate(tx *gorm.DB) error { - // TODO: GORM AfterCreate hook - // - Log creation - // - Send notifications - // - Update cache - // Example: log.Printf("Category created: %s", c.Name) - return nil -} - -// TODO: Implement BeforeUpdate hook -func (c *Category) BeforeUpdate(tx *gorm.DB) error { - // TODO: GORM BeforeUpdate hook - // - Validate changes - // - Prevent certain updates - // - Clean up related data - return nil -} - -// TODO: Implement Validate method for CreateCategoryRequest -func (req *CreateCategoryRequest) Validate() error { - // TODO: Add validation logic for GORM model - // - Name should be unique (checked at database level via GORM) - // - Color should be valid hex color - // - Description should not exceed limits - // Example using validator package: - // return validator.New().Struct(req) - return nil -} - -// TODO: Implement ToCategory method -func (req *CreateCategoryRequest) ToCategory() *Category { - // TODO: Convert request to GORM model - // - Map fields from request to model - // - Set default values - // Example: - // return &Category{ - // Name: req.Name, - // Description: req.Description, - // Color: req.Color, - // Active: true, - // } - return nil -} - -// TODO: Implement GORM scopes (reusable query logic) -func ActiveCategories(db *gorm.DB) *gorm.DB { - // TODO: GORM scope for active categories - // return db.Where("active = ?", true) - return db -} - -func CategoriesWithPosts(db *gorm.DB) *gorm.DB { - // TODO: GORM scope for categories with posts - // return db.Joins("Posts").Where("posts.id IS NOT NULL") - return db -} - -// TODO: Implement model validation methods -func (c *Category) IsActive() bool { - // TODO: Check if category is active - return c.Active -} - -func (c *Category) PostCount(db *gorm.DB) (int64, error) { - // TODO: Get post count for this category using GORM association - // var count int64 - // err := db.Model(c).Association("Posts").Count(&count) - // return count, err - return 0, nil -} diff --git a/labs/lab04/backend/models/post.go b/labs/lab04/backend/models/post.go deleted file mode 100644 index a0fb29b92..000000000 --- a/labs/lab04/backend/models/post.go +++ /dev/null @@ -1,73 +0,0 @@ -package models - -import ( - "database/sql" - "time" -) - -// Post represents a blog post in the system -type Post struct { - ID int `json:"id" db:"id"` - UserID int `json:"user_id" db:"user_id"` - Title string `json:"title" db:"title"` - Content string `json:"content" db:"content"` - Published bool `json:"published" db:"published"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` -} - -// CreatePostRequest represents the payload for creating a post -type CreatePostRequest struct { - UserID int `json:"user_id"` - Title string `json:"title"` - Content string `json:"content"` - Published bool `json:"published"` -} - -// UpdatePostRequest represents the payload for updating a post -type UpdatePostRequest struct { - Title *string `json:"title,omitempty"` - Content *string `json:"content,omitempty"` - Published *bool `json:"published,omitempty"` -} - -// TODO: Implement Validate method for Post -func (p *Post) Validate() error { - // TODO: Add validation logic - // - Title should not be empty and should be at least 5 characters - // - Content should not be empty if published is true - // - UserID should be greater than 0 - // Return appropriate errors if validation fails - return nil -} - -// TODO: Implement Validate method for CreatePostRequest -func (req *CreatePostRequest) Validate() error { - // TODO: Add validation logic - // - Title should not be empty and should be at least 5 characters - // - UserID should be greater than 0 - // - Content should not be empty if published is true - // Return appropriate errors if validation fails - return nil -} - -// TODO: Implement ToPost method for CreatePostRequest -func (req *CreatePostRequest) ToPost() *Post { - // TODO: Convert CreatePostRequest to Post - // Set timestamps to current time - return nil -} - -// TODO: Implement ScanRow method for Post -func (p *Post) ScanRow(row *sql.Row) error { - // TODO: Scan database row into Post struct - // Handle the case where row might be nil - return nil -} - -// TODO: Implement ScanRows method for Post slice -func ScanPosts(rows *sql.Rows) ([]Post, error) { - // TODO: Scan multiple database rows into Post slice - // Make sure to close rows and handle errors properly - return nil, nil -} diff --git a/labs/lab04/backend/models/user.go b/labs/lab04/backend/models/user.go deleted file mode 100644 index 30b4c17ec..000000000 --- a/labs/lab04/backend/models/user.go +++ /dev/null @@ -1,66 +0,0 @@ -package models - -import ( - "database/sql" - "time" -) - -// User represents a user in the system -type User struct { - ID int `json:"id" db:"id"` - Name string `json:"name" db:"name"` - Email string `json:"email" db:"email"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` -} - -// CreateUserRequest represents the payload for creating a user -type CreateUserRequest struct { - Name string `json:"name"` - Email string `json:"email"` -} - -// UpdateUserRequest represents the payload for updating a user -type UpdateUserRequest struct { - Name *string `json:"name,omitempty"` - Email *string `json:"email,omitempty"` -} - -// TODO: Implement Validate method for User -func (u *User) Validate() error { - // TODO: Add validation logic - // - Name should not be empty and should be at least 2 characters - // - Email should be valid format - // Return appropriate errors if validation fails - return nil -} - -// TODO: Implement Validate method for CreateUserRequest -func (req *CreateUserRequest) Validate() error { - // TODO: Add validation logic - // - Name should not be empty and should be at least 2 characters - // - Email should be valid format and not empty - // Return appropriate errors if validation fails - return nil -} - -// TODO: Implement ToUser method for CreateUserRequest -func (req *CreateUserRequest) ToUser() *User { - // TODO: Convert CreateUserRequest to User - // Set timestamps to current time - return nil -} - -// TODO: Implement ScanRow method for User -func (u *User) ScanRow(row *sql.Row) error { - // TODO: Scan database row into User struct - // Handle the case where row might be nil - return nil -} - -// TODO: Implement ScanRows method for User slice -func ScanUsers(rows *sql.Rows) ([]User, error) { - // TODO: Scan multiple database rows into User slice - // Make sure to close rows and handle errors properly - return nil, nil -} diff --git a/labs/lab04/backend/models/user_test.go b/labs/lab04/backend/models/user_test.go deleted file mode 100644 index 726d2ad2b..000000000 --- a/labs/lab04/backend/models/user_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package models - -import ( - "testing" - "time" -) - -func TestUser_Validate(t *testing.T) { - tests := []struct { - name string - user User - wantErr bool - }{ - { - name: "valid user", - user: User{ - Name: "John Doe", - Email: "john@example.com", - }, - wantErr: false, - }, - { - name: "empty name", - user: User{ - Name: "", - Email: "john@example.com", - }, - wantErr: true, - }, - { - name: "short name", - user: User{ - Name: "J", - Email: "john@example.com", - }, - wantErr: true, - }, - { - name: "invalid email", - user: User{ - Name: "John Doe", - Email: "not-an-email", - }, - wantErr: true, - }, - { - name: "empty email", - user: User{ - Name: "John Doe", - Email: "", - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.user.Validate() - if (err != nil) != tt.wantErr { - t.Errorf("User.Validate() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestCreateUserRequest_Validate(t *testing.T) { - tests := []struct { - name string - req CreateUserRequest - wantErr bool - }{ - { - name: "valid request", - req: CreateUserRequest{ - Name: "John Doe", - Email: "john@example.com", - }, - wantErr: false, - }, - { - name: "empty name", - req: CreateUserRequest{ - Name: "", - Email: "john@example.com", - }, - wantErr: true, - }, - { - name: "invalid email", - req: CreateUserRequest{ - Name: "John Doe", - Email: "invalid-email", - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.req.Validate() - if (err != nil) != tt.wantErr { - t.Errorf("CreateUserRequest.Validate() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestCreateUserRequest_ToUser(t *testing.T) { - req := CreateUserRequest{ - Name: "John Doe", - Email: "john@example.com", - } - - user := req.ToUser() - if user == nil { - t.Fatal("ToUser() returned nil") - } - - if user.Name != req.Name { - t.Errorf("ToUser() name = %v, want %v", user.Name, req.Name) - } - - if user.Email != req.Email { - t.Errorf("ToUser() email = %v, want %v", user.Email, req.Email) - } - - // Check that timestamps are set - if user.CreatedAt.IsZero() { - t.Error("ToUser() CreatedAt should not be zero") - } - - if user.UpdatedAt.IsZero() { - t.Error("ToUser() UpdatedAt should not be zero") - } - - // Check that timestamps are recent (within last minute) - now := time.Now() - if now.Sub(user.CreatedAt) > time.Minute { - t.Error("ToUser() CreatedAt should be recent") - } - - if now.Sub(user.UpdatedAt) > time.Minute { - t.Error("ToUser() UpdatedAt should be recent") - } -} diff --git a/labs/lab04/backend/repository/category_repository.go b/labs/lab04/backend/repository/category_repository.go deleted file mode 100644 index 11d91bbe8..000000000 --- a/labs/lab04/backend/repository/category_repository.go +++ /dev/null @@ -1,167 +0,0 @@ -package repository - -import ( - "fmt" - - "lab04-backend/models" - - "gorm.io/gorm" -) - -// CategoryRepository handles database operations for categories using GORM -// This repository demonstrates GORM ORM approach for database operations -type CategoryRepository struct { - db *gorm.DB -} - -// NewCategoryRepository creates a new CategoryRepository with GORM -func NewCategoryRepository(gormDB *gorm.DB) *CategoryRepository { - return &CategoryRepository{db: gormDB} -} - -// TODO: Implement Create method using GORM -func (r *CategoryRepository) Create(category *models.Category) error { - // TODO: Create a new category using GORM - // - Use GORM's Create method: r.db.Create(category) - // - GORM automatically handles ID generation and timestamps - // - No need for manual SQL or RETURNING clauses - // Example: result := r.db.Create(category) - // return result.Error - // - // Notice how much simpler this is compared to manual SQL! - return fmt.Errorf("TODO: implement Create method with GORM") -} - -// TODO: Implement GetByID method using GORM -func (r *CategoryRepository) GetByID(id uint) (*models.Category, error) { - // TODO: Get category by ID using GORM - // - Use GORM's First method: r.db.First(&category, id) - // - GORM automatically generates the WHERE clause - // - Returns gorm.ErrRecordNotFound if not found - // Example: - // var category models.Category - // result := r.db.First(&category, id) - // return &category, result.Error - // - // Much cleaner than manual row scanning! - return nil, fmt.Errorf("TODO: implement GetByID method with GORM") -} - -// TODO: Implement GetAll method using GORM -func (r *CategoryRepository) GetAll() ([]models.Category, error) { - // TODO: Get all categories using GORM - // - Use GORM's Find method: r.db.Find(&categories) - // - GORM automatically handles the slice allocation - // - Can add Order() for sorting: r.db.Order("name").Find(&categories) - // Example: - // var categories []models.Category - // result := r.db.Order("name").Find(&categories) - // return categories, result.Error - return nil, fmt.Errorf("TODO: implement GetAll method with GORM") -} - -// TODO: Implement Update method using GORM -func (r *CategoryRepository) Update(category *models.Category) error { - // TODO: Update category using GORM - // - Use GORM's Save method: r.db.Save(category) - // - GORM automatically updates only changed fields - // - Handles updated_at timestamp automatically - // - Alternative: r.db.Model(category).Updates(updates) - // - // Example: - // result := r.db.Save(category) - // return result.Error - return fmt.Errorf("TODO: implement Update method with GORM") -} - -// TODO: Implement Delete method using GORM -func (r *CategoryRepository) Delete(id uint) error { - // TODO: Delete category using GORM - // - Use GORM's Delete method: r.db.Delete(&models.Category{}, id) - // - GORM can do soft delete if model has DeletedAt field - // - For hard delete: r.db.Unscoped().Delete(&models.Category{}, id) - // - // Example: - // result := r.db.Delete(&models.Category{}, id) - // return result.Error - return fmt.Errorf("TODO: implement Delete method with GORM") -} - -// TODO: Implement FindByName method using GORM -func (r *CategoryRepository) FindByName(name string) (*models.Category, error) { - // TODO: Find category by name using GORM - // - Use GORM's Where method: r.db.Where("name = ?", name).First(&category) - // - GORM handles SQL injection protection automatically - // - Can use struct for conditions: r.db.Where(&models.Category{Name: name}).First(&category) - // - // Example: - // var category models.Category - // result := r.db.Where("name = ?", name).First(&category) - // return &category, result.Error - return nil, fmt.Errorf("TODO: implement FindByName method with GORM") -} - -// TODO: Implement SearchCategories method using GORM -func (r *CategoryRepository) SearchCategories(query string, limit int) ([]models.Category, error) { - // TODO: Search categories using GORM - // - Use GORM's Where with LIKE: r.db.Where("name LIKE ?", "%"+query+"%") - // - Add Limit: .Limit(limit) - // - Add Order: .Order("name") - // - // Example: - // var categories []models.Category - // result := r.db.Where("name LIKE ?", "%"+query+"%"). - // Order("name"). - // Limit(limit). - // Find(&categories) - // return categories, result.Error - return nil, fmt.Errorf("TODO: implement SearchCategories method with GORM") -} - -// TODO: Implement GetCategoriesWithPosts method using GORM associations -func (r *CategoryRepository) GetCategoriesWithPosts() ([]models.Category, error) { - // TODO: Get categories with associated posts using GORM - // - Use GORM's Preload to load associations: r.db.Preload("Posts").Find(&categories) - // - GORM automatically handles the JOINs and relationships - // - Much simpler than manual JOIN queries! - // - // Example: - // var categories []models.Category - // result := r.db.Preload("Posts").Find(&categories) - // return categories, result.Error - // - // This assumes Category model has Posts relationship defined - return nil, fmt.Errorf("TODO: implement GetCategoriesWithPosts method with GORM Preload") -} - -// TODO: Implement Count method using GORM -func (r *CategoryRepository) Count() (int64, error) { - // TODO: Count categories using GORM - // - Use GORM's Count method: r.db.Model(&models.Category{}).Count(&count) - // - GORM returns int64 for count operations - // - // Example: - // var count int64 - // result := r.db.Model(&models.Category{}).Count(&count) - // return count, result.Error - return 0, fmt.Errorf("TODO: implement Count method with GORM") -} - -// TODO: Implement Transaction example using GORM -func (r *CategoryRepository) CreateWithTransaction(categories []models.Category) error { - // TODO: Create multiple categories in a transaction using GORM - // - Use GORM's Transaction method: r.db.Transaction(func(tx *gorm.DB) error {...}) - // - GORM automatically handles rollback on error - // - Much simpler than manual transaction management! - // - // Example: - // return r.db.Transaction(func(tx *gorm.DB) error { - // for _, category := range categories { - // if err := tx.Create(&category).Error; err != nil { - // return err // GORM will rollback automatically - // } - // } - // return nil // GORM will commit automatically - // }) - return fmt.Errorf("TODO: implement CreateWithTransaction method with GORM") -} diff --git a/labs/lab04/backend/repository/category_repository_test.go b/labs/lab04/backend/repository/category_repository_test.go deleted file mode 100644 index d83a9d4a3..000000000 --- a/labs/lab04/backend/repository/category_repository_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package repository - -import ( - "testing" -) - -// TestCategoryRepository tests the GORM ORM approach -func TestCategoryRepository(t *testing.T) { - // TODO: Setup GORM database for testing - // db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - // if err != nil { - // t.Fatalf("Failed to connect to database: %v", err) - // } - - // TODO: Auto-migrate models - // err = db.AutoMigrate(&models.Category{}, &models.Post{}) - // if err != nil { - // t.Fatalf("Failed to migrate database: %v", err) - // } - - // Create repository instance - // categoryRepo := NewCategoryRepository(db) - - // TODO: Test Create method with GORM - t.Run("Create category with GORM", func(t *testing.T) { - // TODO: Test GORM Create functionality - // - Create a category using GORM - // - Verify ID is auto-generated - // - Verify timestamps are set automatically - // - Test validation via GORM hooks - // - // Example: - // category := &models.Category{ - // Name: "Technology", - // Description: "Tech-related posts", - // Color: "#007bff", - // } - // err := categoryRepo.Create(category) - // assert.NoError(t, err) - // assert.NotZero(t, category.ID) - // assert.NotZero(t, category.CreatedAt) - - t.Skip("TODO: implement GORM Create test") - }) - - // TODO: Test GetByID method with GORM - t.Run("GetByID with GORM", func(t *testing.T) { - // TODO: Test GORM First functionality - // - Create test category - // - Retrieve by ID using GORM - // - Test record not found handling - // - // Example: - // category, err := categoryRepo.GetByID(1) - // assert.NoError(t, err) - // assert.Equal(t, "Technology", category.Name) - // - // // Test not found - // _, err = categoryRepo.GetByID(999) - // assert.Error(t, err) - // assert.Equal(t, gorm.ErrRecordNotFound, err) - - t.Skip("TODO: implement GORM GetByID test") - }) - - // TODO: Test GetAll method with GORM - t.Run("GetAll with GORM", func(t *testing.T) { - // TODO: Test GORM Find functionality - // - Create multiple categories - // - Retrieve all using GORM - // - Test ordering - // - // Example: - // categories, err := categoryRepo.GetAll() - // assert.NoError(t, err) - // assert.Len(t, categories, 3) - // // Verify ordering by name - // assert.Equal(t, "Category A", categories[0].Name) - - t.Skip("TODO: implement GORM GetAll test") - }) - - // TODO: Test Update method with GORM - t.Run("Update with GORM", func(t *testing.T) { - // TODO: Test GORM Save/Updates functionality - // - Create category - // - Update using GORM - // - Verify updated_at is automatically set - // - Test partial updates - // - // Example: - // category.Name = "Updated Technology" - // err := categoryRepo.Update(category) - // assert.NoError(t, err) - // assert.Equal(t, "Updated Technology", category.Name) - // assert.True(t, category.UpdatedAt.After(originalUpdatedAt)) - - t.Skip("TODO: implement GORM Update test") - }) - - // TODO: Test Delete method with GORM - t.Run("Delete with GORM", func(t *testing.T) { - // TODO: Test GORM Delete functionality - // - Test soft delete (if DeletedAt field exists) - // - Test hard delete (with Unscoped) - // - Verify cascade behavior - // - // Example: - // err := categoryRepo.Delete(category.ID) - // assert.NoError(t, err) - // - // // Verify soft delete - // _, err = categoryRepo.GetByID(category.ID) - // assert.Error(t, err) - // assert.Equal(t, gorm.ErrRecordNotFound, err) - - t.Skip("TODO: implement GORM Delete test") - }) - - // TODO: Test FindByName method with GORM - t.Run("FindByName with GORM", func(t *testing.T) { - // TODO: Test GORM Where functionality - // - Test exact matches - // - Test case sensitivity - // - Test not found scenarios - // - // Example: - // category, err := categoryRepo.FindByName("Technology") - // assert.NoError(t, err) - // assert.Equal(t, "Technology", category.Name) - - t.Skip("TODO: implement GORM FindByName test") - }) - - // TODO: Test SearchCategories method with GORM - t.Run("SearchCategories with GORM", func(t *testing.T) { - // TODO: Test GORM LIKE functionality - // - Test partial name matches - // - Test limit functionality - // - Test ordering - // - // Example: - // categories, err := categoryRepo.SearchCategories("Tech", 10) - // assert.NoError(t, err) - // assert.True(t, len(categories) <= 10) - - t.Skip("TODO: implement GORM SearchCategories test") - }) - - // TODO: Test GetCategoriesWithPosts method with GORM Preload - t.Run("GetCategoriesWithPosts with GORM Preload", func(t *testing.T) { - // TODO: Test GORM Preload functionality - // - Create categories and posts - // - Test eager loading with Preload - // - Verify associations are loaded - // - // Example: - // categories, err := categoryRepo.GetCategoriesWithPosts() - // assert.NoError(t, err) - // for _, category := range categories { - // assert.NotNil(t, category.Posts) // Posts should be loaded - // } - - t.Skip("TODO: implement GORM Preload test") - }) - - // TODO: Test Count method with GORM - t.Run("Count with GORM", func(t *testing.T) { - // TODO: Test GORM Count functionality - // - Test count with no records - // - Test count with multiple records - // - Test count with soft deleted records - // - // Example: - // count, err := categoryRepo.Count() - // assert.NoError(t, err) - // assert.Equal(t, int64(3), count) - - t.Skip("TODO: implement GORM Count test") - }) - - // TODO: Test Transaction method with GORM - t.Run("Transaction with GORM", func(t *testing.T) { - // TODO: Test GORM Transaction functionality - // - Test successful transaction - // - Test transaction rollback on error - // - Verify atomicity - // - // Example: - // categories := []models.Category{ - // {Name: "Cat1"}, {Name: "Cat2"}, {Name: "Cat3"}, - // } - // err := categoryRepo.CreateWithTransaction(categories) - // assert.NoError(t, err) - - t.Skip("TODO: implement GORM Transaction test") - }) -} - -// TestGORMModelHooks tests GORM model hooks and lifecycle -func TestGORMModelHooks(t *testing.T) { - // TODO: Test GORM hooks - t.Run("BeforeCreate hook", func(t *testing.T) { - // TODO: Test BeforeCreate hook functionality - // - Verify hook is called - // - Test data validation in hook - // - Test default value setting - - t.Skip("TODO: implement GORM BeforeCreate hook test") - }) - - t.Run("AfterCreate hook", func(t *testing.T) { - // TODO: Test AfterCreate hook functionality - // - Verify hook is called after creation - // - Test side effects (logging, notifications) - - t.Skip("TODO: implement GORM AfterCreate hook test") - }) - - t.Run("Validation methods", func(t *testing.T) { - // TODO: Test model validation methods - // - Test IsActive method - // - Test validation constraints - - t.Skip("TODO: implement GORM validation tests") - }) -} - -// TestGORMScopes tests GORM scopes functionality -func TestGORMScopes(t *testing.T) { - // TODO: Test GORM scopes - t.Run("ActiveCategories scope", func(t *testing.T) { - // TODO: Test ActiveCategories scope - // - Create active and inactive categories - // - Test scope filters correctly - - t.Skip("TODO: implement GORM ActiveCategories scope test") - }) - - t.Run("CategoriesWithPosts scope", func(t *testing.T) { - // TODO: Test CategoriesWithPosts scope - // - Create categories with and without posts - // - Test scope filters correctly - - t.Skip("TODO: implement GORM CategoriesWithPosts scope test") - }) -} - -// BenchmarkGORMVsSQL benchmarks GORM vs raw SQL performance -func BenchmarkGORMVsSQL(b *testing.B) { - // TODO: Compare GORM vs raw SQL performance - b.Run("GORM Create", func(b *testing.B) { - // TODO: Benchmark GORM Create operations - b.Skip("TODO: implement GORM Create benchmark") - }) - - b.Run("Raw SQL Create", func(b *testing.B) { - // TODO: Benchmark raw SQL Create operations - b.Skip("TODO: implement raw SQL Create benchmark") - }) - - b.Run("GORM Query", func(b *testing.B) { - // TODO: Benchmark GORM Query operations - b.Skip("TODO: implement GORM Query benchmark") - }) - - b.Run("Raw SQL Query", func(b *testing.B) { - // TODO: Benchmark raw SQL Query operations - b.Skip("TODO: implement raw SQL Query benchmark") - }) -} diff --git a/labs/lab04/backend/repository/post_repository.go b/labs/lab04/backend/repository/post_repository.go deleted file mode 100644 index d75fdf678..000000000 --- a/labs/lab04/backend/repository/post_repository.go +++ /dev/null @@ -1,101 +0,0 @@ -package repository - -import ( - "database/sql" - "fmt" - - "lab04-backend/models" -) - -// PostRepository handles database operations for posts -// This repository demonstrates SCANY MAPPING approach for result scanning -type PostRepository struct { - db *sql.DB -} - -// NewPostRepository creates a new PostRepository -func NewPostRepository(db *sql.DB) *PostRepository { - return &PostRepository{db: db} -} - -// TODO: Implement Create method using scany for result mapping -func (r *PostRepository) Create(req *models.CreatePostRequest) (*models.Post, error) { - // TODO: Create a new post in the database using scany for result mapping - // - Validate the request using req.Validate() - // - Insert into posts table with RETURNING clause - // - Use sqlscan.Get() to scan the RETURNING result into a Post struct - // Example: sqlscan.Get(context.Background(), r.db, &post, query, args...) - // This eliminates manual row scanning compared to user repository - return nil, fmt.Errorf("TODO: implement Create method with scany mapping") -} - -// TODO: Implement GetByID method using scany -func (r *PostRepository) GetByID(id int) (*models.Post, error) { - // TODO: Get post by ID from database using scany - // - Use sqlscan.Get() instead of manual row.Scan() - // Example: sqlscan.Get(context.Background(), r.db, &post, "SELECT * FROM posts WHERE id = $1", id) - // Notice how this eliminates the need for manual field scanning - return nil, fmt.Errorf("TODO: implement GetByID method with scany") -} - -// TODO: Implement GetByUserID method using scany -func (r *PostRepository) GetByUserID(userID int) ([]models.Post, error) { - // TODO: Get all posts by user ID using scany - // - Use sqlscan.Select() for multiple rows instead of manual rows.Next() loop - // Example: sqlscan.Select(context.Background(), r.db, &posts, query, userID) - // This eliminates manual iteration and scanning - return nil, fmt.Errorf("TODO: implement GetByUserID method with scany") -} - -// TODO: Implement GetPublished method using scany -func (r *PostRepository) GetPublished() ([]models.Post, error) { - // TODO: Get all published posts using scany - // - Use sqlscan.Select() for multiple rows - // - Query posts where published = true - // - Order by created_at DESC - return nil, fmt.Errorf("TODO: implement GetPublished method with scany") -} - -// TODO: Implement GetAll method using scany -func (r *PostRepository) GetAll() ([]models.Post, error) { - // TODO: Get all posts from database using scany - // - Use sqlscan.Select() instead of manual rows iteration - // Example: sqlscan.Select(context.Background(), r.db, &posts, "SELECT * FROM posts ORDER BY created_at DESC") - // Compare this simplicity with manual scanning in user repository - return nil, fmt.Errorf("TODO: implement GetAll method with scany") -} - -// TODO: Implement Update method using scany -func (r *PostRepository) Update(id int, req *models.UpdatePostRequest) (*models.Post, error) { - // TODO: Update post in database using scany - // - Build dynamic UPDATE query based on non-nil fields in req - // - Update updated_at timestamp - // - Use sqlscan.Get() with RETURNING clause to get updated post - // This avoids a separate SELECT query after UPDATE - return nil, fmt.Errorf("TODO: implement Update method with scany") -} - -// TODO: Implement Delete method (standard SQL) -func (r *PostRepository) Delete(id int) error { - // TODO: Delete post from database - // - Delete from posts table by ID - // - Return error if post doesn't exist - // Note: Delete operations typically don't need scany since no data is returned - return fmt.Errorf("TODO: implement Delete method") -} - -// TODO: Implement Count method (standard SQL) -func (r *PostRepository) Count() (int, error) { - // TODO: Count total number of posts - // - Return count of posts in database - // - Can use standard QueryRow.Scan() for single values like count - return 0, fmt.Errorf("TODO: implement Count method") -} - -// TODO: Implement CountByUserID method (standard SQL) -func (r *PostRepository) CountByUserID(userID int) (int, error) { - // TODO: Count posts by user ID - // - Return count of posts for specific user - // - Use standard QueryRow.Scan() for single integer result - return 0, fmt.Errorf("TODO: implement CountByUserID method") -} diff --git a/labs/lab04/backend/repository/search_service.go b/labs/lab04/backend/repository/search_service.go deleted file mode 100644 index e8827aa90..000000000 --- a/labs/lab04/backend/repository/search_service.go +++ /dev/null @@ -1,169 +0,0 @@ -package repository - -import ( - "context" - "database/sql" - "fmt" - - "lab04-backend/models" - - "github.com/Masterminds/squirrel" -) - -// SearchService handles dynamic search operations using Squirrel query builder -// This service demonstrates SQUIRREL QUERY BUILDER approach for dynamic SQL -type SearchService struct { - db *sql.DB - psql squirrel.StatementBuilderType -} - -// SearchFilters represents search parameters -type SearchFilters struct { - Query string // Search in title and content - UserID *int // Filter by user ID - Published *bool // Filter by published status - MinWordCount *int // Minimum word count in content - Limit int // Results limit (default 50) - Offset int // Results offset (for pagination) - OrderBy string // Order by field (title, created_at, updated_at) - OrderDir string // Order direction (ASC, DESC) -} - -// NewSearchService creates a new SearchService -func NewSearchService(db *sql.DB) *SearchService { - return &SearchService{ - db: db, - psql: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), - } -} - -// TODO: Implement SearchPosts method using Squirrel query builder -func (s *SearchService) SearchPosts(ctx context.Context, filters SearchFilters) ([]models.Post, error) { - // TODO: Build dynamic query using Squirrel instead of string concatenation - // - // Start with base query: - // query := s.psql.Select("id", "user_id", "title", "content", "published", "created_at", "updated_at"). - // From("posts") - // - // Add WHERE conditions dynamically: - // - If filters.Query: add ILIKE conditions for title and content - // - If filters.UserID: add user_id = ? - // - If filters.Published: add published = ? - // - If filters.MinWordCount: add word count condition - // - // Add ORDER BY dynamically: - // - Use OrderBy() and validate sort fields - // - // Add LIMIT/OFFSET: - // - Use Limit() and Offset() - // - // Build final SQL: - // sql, args, err := query.ToSql() - // - // Execute with scany: - // var posts []models.Post - // err = sqlscan.Select(ctx, s.db, &posts, sql, args...) - // - // This demonstrates the power of combining Squirrel (dynamic queries) - // with scany (automatic result mapping) - - return nil, fmt.Errorf("TODO: implement SearchPosts with Squirrel query builder") -} - -// TODO: Implement SearchUsers method using Squirrel -func (s *SearchService) SearchUsers(ctx context.Context, nameQuery string, limit int) ([]models.User, error) { - // TODO: Build user search query with Squirrel - // query := s.psql.Select("id", "name", "email", "created_at", "updated_at"). - // From("users"). - // Where(squirrel.Like{"name": "%" + nameQuery + "%"}). - // OrderBy("name"). - // Limit(uint64(limit)) - // - // sql, args, err := query.ToSql() - // var users []models.User - // err = sqlscan.Select(ctx, s.db, &users, sql, args...) - - return nil, fmt.Errorf("TODO: implement SearchUsers with Squirrel") -} - -// TODO: Implement GetPostStats method using Squirrel with JOINs -func (s *SearchService) GetPostStats(ctx context.Context) (*PostStats, error) { - // TODO: Build complex query with JOINs using Squirrel - // query := s.psql.Select( - // "COUNT(p.id) as total_posts", - // "COUNT(CASE WHEN p.published = true THEN 1 END) as published_posts", - // "COUNT(DISTINCT p.user_id) as active_users", - // "AVG(LENGTH(p.content)) as avg_content_length", - // ).From("posts p"). - // Join("users u ON p.user_id = u.id") - // - // This shows how Squirrel handles complex queries better than string building - - return nil, fmt.Errorf("TODO: implement GetPostStats with Squirrel JOINs") -} - -// PostStats represents aggregated post statistics -type PostStats struct { - TotalPosts int `db:"total_posts"` - PublishedPosts int `db:"published_posts"` - ActiveUsers int `db:"active_users"` - AvgContentLength float64 `db:"avg_content_length"` -} - -// TODO: Implement BuildDynamicQuery helper method -func (s *SearchService) BuildDynamicQuery(baseQuery squirrel.SelectBuilder, filters SearchFilters) squirrel.SelectBuilder { - // TODO: Demonstrate how to build queries step by step with Squirrel - // - // query := baseQuery - // - // if filters.Query != "" { - // searchTerm := "%" + filters.Query + "%" - // query = query.Where(squirrel.Or{ - // squirrel.ILike{"title": searchTerm}, - // squirrel.ILike{"content": searchTerm}, - // }) - // } - // - // if filters.UserID != nil { - // query = query.Where(squirrel.Eq{"user_id": *filters.UserID}) - // } - // - // if filters.Published != nil { - // query = query.Where(squirrel.Eq{"published": *filters.Published}) - // } - // - // This modular approach makes dynamic queries much cleaner - // than string concatenation used in manual SQL approaches - - return baseQuery -} - -// TODO: Implement GetTopUsers method using Squirrel with complex aggregation -func (s *SearchService) GetTopUsers(ctx context.Context, limit int) ([]UserWithStats, error) { - // TODO: Build complex aggregation query with Squirrel - // query := s.psql.Select( - // "u.id", - // "u.name", - // "u.email", - // "COUNT(p.id) as post_count", - // "COUNT(CASE WHEN p.published = true THEN 1 END) as published_count", - // "MAX(p.created_at) as last_post_date", - // ).From("users u"). - // LeftJoin("posts p ON u.id = p.user_id"). - // GroupBy("u.id", "u.name", "u.email"). - // OrderBy("post_count DESC"). - // Limit(uint64(limit)) - // - // Notice how Squirrel makes complex queries more readable - // compared to building SQL strings manually - - return nil, fmt.Errorf("TODO: implement GetTopUsers with Squirrel aggregation") -} - -// UserWithStats represents a user with post statistics -type UserWithStats struct { - models.User - PostCount int `db:"post_count"` - PublishedCount int `db:"published_count"` - LastPostDate string `db:"last_post_date"` -} diff --git a/labs/lab04/backend/repository/search_service_test.go b/labs/lab04/backend/repository/search_service_test.go deleted file mode 100644 index fd0d0e054..000000000 --- a/labs/lab04/backend/repository/search_service_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package repository - -import ( - "testing" - - "lab04-backend/database" -) - -// TestSearchService tests the Squirrel query builder approach -func TestSearchService(t *testing.T) { - // Initialize database for testing - db, err := database.InitDB() - if err != nil { - t.Fatalf("Failed to initialize database: %v", err) - } - defer database.CloseDB(db) - - // Run migrations - if err := database.RunMigrations(db); err != nil { - t.Fatalf("Failed to run migrations: %v", err) - } - - // Create service instance - searchService := NewSearchService(db) - - // TODO: Test SearchPosts with various filters - t.Run("SearchPosts with filters", func(t *testing.T) { - // TODO: Test dynamic query building with Squirrel - // - Test empty filters (should return all posts) - // - Test search by query string - // - Test filter by user ID - // - Test filter by published status - // - Test pagination (limit/offset) - // - Test sorting (order by different fields) - // - // Example test structure: - // filters := SearchFilters{ - // Query: "golang", - // Published: &[]bool{true}[0], - // Limit: 10, - // OrderBy: "created_at", - // OrderDir: "DESC", - // } - // posts, err := searchService.SearchPosts(context.Background(), filters) - // assert.NoError(t, err) - // assert.LessOrEqual(t, len(posts), 10) - - // Use searchService to avoid "declared and not used" error - _ = searchService - t.Skip("TODO: implement SearchPosts test with Squirrel filters") - }) - - // TODO: Test SearchUsers functionality - t.Run("SearchUsers", func(t *testing.T) { - // TODO: Test user search with Squirrel - // - Test exact name matches - // - Test partial name matches with LIKE - // - Test case insensitive search - // - Test limit functionality - - _ = searchService - t.Skip("TODO: implement SearchUsers test with Squirrel") - }) - - // TODO: Test GetPostStats with complex aggregation - t.Run("GetPostStats", func(t *testing.T) { - // TODO: Test complex aggregation query - // - Insert test data (users and posts) - // - Test aggregation calculations - // - Verify JOIN functionality - // - Test with no data (empty tables) - - _ = searchService - t.Skip("TODO: implement GetPostStats test with Squirrel JOINs") - }) - - // TODO: Test GetTopUsers with aggregation and sorting - t.Run("GetTopUsers", func(t *testing.T) { - // TODO: Test user ranking with post statistics - // - Insert users with different post counts - // - Test ordering by post count - // - Test LEFT JOIN behavior (users with no posts) - // - Test limit functionality - - _ = searchService - t.Skip("TODO: implement GetTopUsers test with Squirrel aggregation") - }) - - // TODO: Test BuildDynamicQuery helper - t.Run("BuildDynamicQuery", func(t *testing.T) { - // TODO: Test query building step by step - // - Test with different filter combinations - // - Verify generated SQL syntax - // - Test parameter binding - // - // Example: - // baseQuery := searchService.psql.Select("*").From("posts") - // filters := SearchFilters{Query: "test", Published: &[]bool{true}[0]} - // query := searchService.BuildDynamicQuery(baseQuery, filters) - // sql, args, err := query.ToSql() - // assert.NoError(t, err) - // assert.Contains(t, sql, "WHERE") - // assert.Contains(t, sql, "published") - - _ = searchService - t.Skip("TODO: implement BuildDynamicQuery test") - }) -} - -// TestSquirrelQueryBuilder tests Squirrel query building functionality -func TestSquirrelQueryBuilder(t *testing.T) { - // TODO: Test Squirrel query builder patterns - t.Run("Basic Query Building", func(t *testing.T) { - // TODO: Test basic Squirrel functionality - // - Test SELECT with WHERE conditions - // - Test dynamic WHERE building - // - Test ORDER BY, LIMIT, OFFSET - // - Test parameter placeholder generation - // - // Example: - // psql := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) - // query := psql.Select("id", "name").From("users").Where(squirrel.Eq{"active": true}) - // sql, args, err := query.ToSql() - // assert.NoError(t, err) - // assert.Equal(t, "SELECT id, name FROM users WHERE active = $1", sql) - // assert.Equal(t, []interface{}{true}, args) - - t.Skip("TODO: implement basic Squirrel query building tests") - }) - - t.Run("Complex Query Building", func(t *testing.T) { - // TODO: Test complex Squirrel features - // - Test JOINs - // - Test subqueries - // - Test complex WHERE conditions (OR, AND, IN) - // - Test aggregation functions - // - Test GROUP BY and HAVING - - t.Skip("TODO: implement complex Squirrel query tests") - }) -} - -// BenchmarkSquirrelVsManualSQL benchmarks Squirrel vs manual SQL building -func BenchmarkSquirrelVsManualSQL(b *testing.B) { - // TODO: Compare performance of Squirrel vs manual string building - b.Run("Squirrel", func(b *testing.B) { - // TODO: Benchmark Squirrel query building - b.Skip("TODO: implement Squirrel benchmark") - }) - - b.Run("Manual SQL", func(b *testing.B) { - // TODO: Benchmark manual string building - b.Skip("TODO: implement manual SQL benchmark") - }) -} diff --git a/labs/lab04/backend/repository/user_repository.go b/labs/lab04/backend/repository/user_repository.go deleted file mode 100644 index 28103fee6..000000000 --- a/labs/lab04/backend/repository/user_repository.go +++ /dev/null @@ -1,82 +0,0 @@ -package repository - -import ( - "database/sql" - "fmt" - - "lab04-backend/models" -) - -// UserRepository handles database operations for users -// This repository demonstrates MANUAL SQL approach with database/sql package -type UserRepository struct { - db *sql.DB -} - -// NewUserRepository creates a new UserRepository -func NewUserRepository(db *sql.DB) *UserRepository { - return &UserRepository{db: db} -} - -// TODO: Implement Create method -func (r *UserRepository) Create(req *models.CreateUserRequest) (*models.User, error) { - // TODO: Create a new user in the database - // - Validate the request - // - Insert into users table - // - Return the created user with ID and timestamps - // Use RETURNING clause to get the generated ID and timestamps - return nil, fmt.Errorf("TODO: implement Create method") -} - -// TODO: Implement GetByID method -func (r *UserRepository) GetByID(id int) (*models.User, error) { - // TODO: Get user by ID from database - // - Query users table by ID - // - Return user or sql.ErrNoRows if not found - // - Handle scanning properly - return nil, fmt.Errorf("TODO: implement GetByID method") -} - -// TODO: Implement GetByEmail method -func (r *UserRepository) GetByEmail(email string) (*models.User, error) { - // TODO: Get user by email from database - // - Query users table by email - // - Return user or sql.ErrNoRows if not found - // - Handle scanning properly - return nil, fmt.Errorf("TODO: implement GetByEmail method") -} - -// TODO: Implement GetAll method -func (r *UserRepository) GetAll() ([]models.User, error) { - // TODO: Get all users from database - // - Query all users ordered by created_at - // - Return slice of users - // - Handle empty result properly - return nil, fmt.Errorf("TODO: implement GetAll method") -} - -// TODO: Implement Update method -func (r *UserRepository) Update(id int, req *models.UpdateUserRequest) (*models.User, error) { - // TODO: Update user in database - // - Build dynamic UPDATE query based on non-nil fields in req - // - Update updated_at timestamp - // - Return updated user - // - Handle case where user doesn't exist - return nil, fmt.Errorf("TODO: implement Update method") -} - -// TODO: Implement Delete method -func (r *UserRepository) Delete(id int) error { - // TODO: Delete user from database - // - Delete from users table by ID - // - Return error if user doesn't exist - // - Consider cascading deletes for posts - return fmt.Errorf("TODO: implement Delete method") -} - -// TODO: Implement Count method -func (r *UserRepository) Count() (int, error) { - // TODO: Count total number of users - // - Return count of users in database - return 0, fmt.Errorf("TODO: implement Count method") -} diff --git a/labs/lab04/backend/repository/user_repository_test.go b/labs/lab04/backend/repository/user_repository_test.go deleted file mode 100644 index 2b84beb07..000000000 --- a/labs/lab04/backend/repository/user_repository_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package repository - -import ( - "os" - "testing" - - "lab04-backend/database" - "lab04-backend/models" -) - -func setupTestDB(t *testing.T) (*UserRepository, func()) { - // Create test database - testDB := "./test_user_repo.db" - config := &database.Config{ - DatabasePath: testDB, - MaxOpenConns: 5, - MaxIdleConns: 1, - ConnMaxLifetime: 0, - ConnMaxIdleTime: 0, - } - - db, err := database.InitDBWithConfig(config) - if err != nil { - t.Fatalf("Failed to initialize test database: %v", err) - } - - err = database.RunMigrations(db) - if err != nil { - t.Fatalf("Failed to migrate test database: %v", err) - } - - repo := NewUserRepository(db) - - // Return cleanup function - cleanup := func() { - database.CloseDB(db) - os.Remove(testDB) - } - - return repo, cleanup -} - -func TestUserRepository_Create(t *testing.T) { - repo, cleanup := setupTestDB(t) - defer cleanup() - - req := &models.CreateUserRequest{ - Name: "John Doe", - Email: "john@example.com", - } - - user, err := repo.Create(req) - if err != nil { - t.Fatalf("Create() failed: %v", err) - } - - if user == nil { - t.Fatal("Create() returned nil user") - } - - if user.ID == 0 { - t.Error("Create() should set user ID") - } - - if user.Name != req.Name { - t.Errorf("Create() name = %v, want %v", user.Name, req.Name) - } - - if user.Email != req.Email { - t.Errorf("Create() email = %v, want %v", user.Email, req.Email) - } - - if user.CreatedAt.IsZero() { - t.Error("Create() should set CreatedAt") - } - - if user.UpdatedAt.IsZero() { - t.Error("Create() should set UpdatedAt") - } -} - -func TestUserRepository_GetByID(t *testing.T) { - repo, cleanup := setupTestDB(t) - defer cleanup() - - // Create a user first - req := &models.CreateUserRequest{ - Name: "Jane Doe", - Email: "jane@example.com", - } - - createdUser, err := repo.Create(req) - if err != nil { - t.Fatalf("Failed to create user: %v", err) - } - - // Test GetByID - foundUser, err := repo.GetByID(createdUser.ID) - if err != nil { - t.Fatalf("GetByID() failed: %v", err) - } - - if foundUser == nil { - t.Fatal("GetByID() returned nil user") - } - - if foundUser.ID != createdUser.ID { - t.Errorf("GetByID() ID = %v, want %v", foundUser.ID, createdUser.ID) - } - - if foundUser.Name != createdUser.Name { - t.Errorf("GetByID() name = %v, want %v", foundUser.Name, createdUser.Name) - } - - if foundUser.Email != createdUser.Email { - t.Errorf("GetByID() email = %v, want %v", foundUser.Email, createdUser.Email) - } - - // Test GetByID with non-existent ID - _, err = repo.GetByID(99999) - if err == nil { - t.Error("GetByID() should return error for non-existent user") - } -} - -func TestUserRepository_GetByEmail(t *testing.T) { - repo, cleanup := setupTestDB(t) - defer cleanup() - - // Create a user first - req := &models.CreateUserRequest{ - Name: "Bob Smith", - Email: "bob@example.com", - } - - createdUser, err := repo.Create(req) - if err != nil { - t.Fatalf("Failed to create user: %v", err) - } - - // Test GetByEmail - foundUser, err := repo.GetByEmail(createdUser.Email) - if err != nil { - t.Fatalf("GetByEmail() failed: %v", err) - } - - if foundUser == nil { - t.Fatal("GetByEmail() returned nil user") - } - - if foundUser.Email != createdUser.Email { - t.Errorf("GetByEmail() email = %v, want %v", foundUser.Email, createdUser.Email) - } - - // Test GetByEmail with non-existent email - _, err = repo.GetByEmail("nonexistent@example.com") - if err == nil { - t.Error("GetByEmail() should return error for non-existent email") - } -} - -func TestUserRepository_GetAll(t *testing.T) { - repo, cleanup := setupTestDB(t) - defer cleanup() - - // Test empty database - users, err := repo.GetAll() - if err != nil { - t.Fatalf("GetAll() failed: %v", err) - } - - if len(users) != 0 { - t.Errorf("GetAll() should return empty slice for empty database, got %d users", len(users)) - } - - // Create multiple users - userRequests := []*models.CreateUserRequest{ - {Name: "User One", Email: "user1@example.com"}, - {Name: "User Two", Email: "user2@example.com"}, - {Name: "User Three", Email: "user3@example.com"}, - } - - for _, req := range userRequests { - _, err := repo.Create(req) - if err != nil { - t.Fatalf("Failed to create user: %v", err) - } - } - - // Test GetAll with users - users, err = repo.GetAll() - if err != nil { - t.Fatalf("GetAll() failed: %v", err) - } - - if len(users) != len(userRequests) { - t.Errorf("GetAll() returned %d users, want %d", len(users), len(userRequests)) - } -} - -func TestUserRepository_Update(t *testing.T) { - repo, cleanup := setupTestDB(t) - defer cleanup() - - // Create a user first - req := &models.CreateUserRequest{ - Name: "Original Name", - Email: "original@example.com", - } - - createdUser, err := repo.Create(req) - if err != nil { - t.Fatalf("Failed to create user: %v", err) - } - - // Test update - newName := "Updated Name" - newEmail := "updated@example.com" - updateReq := &models.UpdateUserRequest{ - Name: &newName, - Email: &newEmail, - } - - updatedUser, err := repo.Update(createdUser.ID, updateReq) - if err != nil { - t.Fatalf("Update() failed: %v", err) - } - - if updatedUser == nil { - t.Fatal("Update() returned nil user") - } - - if updatedUser.Name != newName { - t.Errorf("Update() name = %v, want %v", updatedUser.Name, newName) - } - - if updatedUser.Email != newEmail { - t.Errorf("Update() email = %v, want %v", updatedUser.Email, newEmail) - } - - if !updatedUser.UpdatedAt.After(createdUser.UpdatedAt) { - t.Error("Update() should update UpdatedAt timestamp") - } - - // Test update with non-existent ID - _, err = repo.Update(99999, updateReq) - if err == nil { - t.Error("Update() should return error for non-existent user") - } -} - -func TestUserRepository_Delete(t *testing.T) { - repo, cleanup := setupTestDB(t) - defer cleanup() - - // Create a user first - req := &models.CreateUserRequest{ - Name: "To Be Deleted", - Email: "delete@example.com", - } - - createdUser, err := repo.Create(req) - if err != nil { - t.Fatalf("Failed to create user: %v", err) - } - - // Test delete - err = repo.Delete(createdUser.ID) - if err != nil { - t.Fatalf("Delete() failed: %v", err) - } - - // Verify user is deleted - _, err = repo.GetByID(createdUser.ID) - if err == nil { - t.Error("User should be deleted and GetByID should return error") - } - - // Test delete with non-existent ID - err = repo.Delete(99999) - if err == nil { - t.Error("Delete() should return error for non-existent user") - } -} - -func TestUserRepository_Count(t *testing.T) { - repo, cleanup := setupTestDB(t) - defer cleanup() - - // Test count with empty database - count, err := repo.Count() - if err != nil { - t.Fatalf("Count() failed: %v", err) - } - - if count != 0 { - t.Errorf("Count() should return 0 for empty database, got %d", count) - } - - // Create users - userRequests := []*models.CreateUserRequest{ - {Name: "Count User 1", Email: "count1@example.com"}, - {Name: "Count User 2", Email: "count2@example.com"}, - } - - for _, req := range userRequests { - _, err := repo.Create(req) - if err != nil { - t.Fatalf("Failed to create user: %v", err) - } - } - - // Test count with users - count, err = repo.Count() - if err != nil { - t.Fatalf("Count() failed: %v", err) - } - - if count != len(userRequests) { - t.Errorf("Count() returned %d, want %d", count, len(userRequests)) - } -} diff --git a/labs/lab04/frontend/.gitignore b/labs/lab04/frontend/.gitignore deleted file mode 100644 index 79c113f9b..000000000 --- a/labs/lab04/frontend/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -# 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/labs/lab04/frontend/.metadata b/labs/lab04/frontend/.metadata deleted file mode 100644 index 603c64122..000000000 --- a/labs/lab04/frontend/.metadata +++ /dev/null @@ -1,45 +0,0 @@ -# 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/labs/lab04/frontend/README.md b/labs/lab04/frontend/README.md deleted file mode 100644 index 03d619cfa..000000000 --- a/labs/lab04/frontend/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# frontend - -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/labs/lab04/frontend/analysis_options.yaml b/labs/lab04/frontend/analysis_options.yaml deleted file mode 100644 index 0d2902135..000000000 --- a/labs/lab04/frontend/analysis_options.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/labs/lab04/frontend/lib/main.dart b/labs/lab04/frontend/lib/main.dart deleted file mode 100644 index e107715f1..000000000 --- a/labs/lab04/frontend/lib/main.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'services/preferences_service.dart'; -import 'screens/home_screen.dart'; - -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - - // TODO: Initialize services - try { - // TODO: Initialize PreferencesService - await PreferencesService.init(); - - // TODO: Add any other service initialization here - // For example: await DatabaseService.database; - } catch (e) { - print('Error initializing services: $e'); - } - - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Lab 04 - Database & Persistence', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), - home: const HomeScreen(), - debugShowCheckedModeBanner: false, - ); - } -} diff --git a/labs/lab04/frontend/lib/models/user.dart b/labs/lab04/frontend/lib/models/user.dart deleted file mode 100644 index 1b2dc9a5e..000000000 --- a/labs/lab04/frontend/lib/models/user.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'user.g.dart'; - -@JsonSerializable() -class User { - final int id; - final String name; - final String email; - @JsonKey(name: 'created_at') - final DateTime createdAt; - @JsonKey(name: 'updated_at') - final DateTime updatedAt; - - User({ - required this.id, - required this.name, - required this.email, - required this.createdAt, - required this.updatedAt, - }); - - factory User.fromJson(Map json) => _$UserFromJson(json); - Map toJson() => _$UserToJson(this); - - // TODO: Implement copyWith method - User copyWith({ - int? id, - String? name, - String? email, - DateTime? createdAt, - DateTime? updatedAt, - }) { - // TODO: Create a copy of User with updated fields - // Return new User instance with updated values or original values if null - throw UnimplementedError('TODO: implement copyWith method'); - } - - // TODO: Implement equality operator - @override - bool operator ==(Object other) { - // TODO: Compare User objects for equality - // Check if other is User and all fields are equal - return super == other; - } - - // TODO: Implement hashCode - @override - int get hashCode { - // TODO: Generate hash code based on all fields - return super.hashCode; - } - - // TODO: Implement toString - @override - String toString() { - // TODO: Return string representation of User - return super.toString(); - } -} - -@JsonSerializable() -class CreateUserRequest { - final String name; - final String email; - - CreateUserRequest({ - required this.name, - required this.email, - }); - - factory CreateUserRequest.fromJson(Map json) => - _$CreateUserRequestFromJson(json); - Map toJson() => _$CreateUserRequestToJson(this); - - // TODO: Implement validate method - bool validate() { - // TODO: Validate user creation request - // - Name should not be empty and should be at least 2 characters - // - Email should be valid format - return false; - } -} diff --git a/labs/lab04/frontend/lib/models/user.g.dart b/labs/lab04/frontend/lib/models/user.g.dart deleted file mode 100644 index 2bdcd526d..000000000 --- a/labs/lab04/frontend/lib/models/user.g.dart +++ /dev/null @@ -1,35 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'user.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -User _$UserFromJson(Map json) => User( - id: (json['id'] as num).toInt(), - name: json['name'] as String, - email: json['email'] as String, - createdAt: DateTime.parse(json['created_at'] as String), - updatedAt: DateTime.parse(json['updated_at'] as String), - ); - -Map _$UserToJson(User instance) => { - 'id': instance.id, - 'name': instance.name, - 'email': instance.email, - 'created_at': instance.createdAt.toIso8601String(), - 'updated_at': instance.updatedAt.toIso8601String(), - }; - -CreateUserRequest _$CreateUserRequestFromJson(Map json) => - CreateUserRequest( - name: json['name'] as String, - email: json['email'] as String, - ); - -Map _$CreateUserRequestToJson(CreateUserRequest instance) => - { - 'name': instance.name, - 'email': instance.email, - }; diff --git a/labs/lab04/frontend/lib/screens/home_screen.dart b/labs/lab04/frontend/lib/screens/home_screen.dart deleted file mode 100644 index 92840499a..000000000 --- a/labs/lab04/frontend/lib/screens/home_screen.dart +++ /dev/null @@ -1,209 +0,0 @@ -import 'package:flutter/material.dart'; -import '../services/preferences_service.dart'; -import '../services/database_service.dart'; -import '../services/secure_storage_service.dart'; - -class HomeScreen extends StatefulWidget { - const HomeScreen({super.key}); - - @override - State createState() => _HomeScreenState(); -} - -class _HomeScreenState extends State { - String _statusMessage = 'Welcome to Lab 04 - Database & Persistence'; - bool _isLoading = false; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Lab 04 - Database & Persistence'), - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Status', - style: - TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text(_statusMessage), - if (_isLoading) - const Padding( - padding: EdgeInsets.only(top: 8), - child: LinearProgressIndicator(), - ), - ], - ), - ), - ), - const SizedBox(height: 20), - const Text( - 'Storage Options', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 16), - - // SharedPreferences Section - _buildStorageSection( - 'SharedPreferences', - 'Simple key-value storage for app settings', - [ - ElevatedButton( - onPressed: _testSharedPreferences, - child: const Text('Test SharedPreferences'), - ), - ], - ), - - // SQLite Section - _buildStorageSection( - 'SQLite Database', - 'Local SQL database for structured data', - [ - ElevatedButton( - onPressed: _testSQLite, - child: const Text('Test SQLite'), - ), - ], - ), - - // Secure Storage Section - _buildStorageSection( - 'Secure Storage', - 'Encrypted storage for sensitive data', - [ - ElevatedButton( - onPressed: _testSecureStorage, - child: const Text('Test Secure Storage'), - ), - ], - ), - ], - ), - ), - ); - } - - Widget _buildStorageSection( - String title, String description, List buttons) { - return Card( - margin: const EdgeInsets.only(bottom: 12), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 4), - Text( - description, - style: const TextStyle(color: Colors.grey), - ), - const SizedBox(height: 12), - Wrap( - spacing: 8, - children: buttons, - ), - ], - ), - ), - ); - } - - Future _testSharedPreferences() async { - setState(() { - _isLoading = true; - _statusMessage = 'Testing SharedPreferences...'; - }); - - try { - // TODO: Implement SharedPreferences test - // This will test when students implement the methods - - await PreferencesService.setString( - 'test_key', 'Hello from SharedPreferences!'); - final value = PreferencesService.getString('test_key'); - - setState(() { - _statusMessage = 'SharedPreferences test result: $value'; - }); - } catch (e) { - setState(() { - _statusMessage = 'SharedPreferences test failed: $e'; - }); - } finally { - setState(() { - _isLoading = false; - }); - } - } - - Future _testSQLite() async { - setState(() { - _isLoading = true; - _statusMessage = 'Testing SQLite database...'; - }); - - try { - // TODO: Implement SQLite test - // This will test when students implement the methods - - final userCount = await DatabaseService.getUserCount(); - - setState(() { - _statusMessage = - 'SQLite test result: Found $userCount users in database'; - }); - } catch (e) { - setState(() { - _statusMessage = 'SQLite test failed: $e'; - }); - } finally { - setState(() { - _isLoading = false; - }); - } - } - - Future _testSecureStorage() async { - setState(() { - _isLoading = true; - _statusMessage = 'Testing Secure Storage...'; - }); - - try { - // TODO: Implement Secure Storage test - // This will test when students implement the methods - - await SecureStorageService.saveSecureData('test_secure', 'Secret data'); - final value = await SecureStorageService.getSecureData('test_secure'); - - setState(() { - _statusMessage = 'Secure Storage test result: $value'; - }); - } catch (e) { - setState(() { - _statusMessage = 'Secure Storage test failed: $e'; - }); - } finally { - setState(() { - _isLoading = false; - }); - } - } -} diff --git a/labs/lab04/frontend/lib/services/database_service.dart b/labs/lab04/frontend/lib/services/database_service.dart deleted file mode 100644 index 62cc703e0..000000000 --- a/labs/lab04/frontend/lib/services/database_service.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:sqflite/sqflite.dart'; -import 'package:path/path.dart'; -import '../models/user.dart'; - -class DatabaseService { - static Database? _database; - static const String _dbName = 'lab04_app.db'; - static const int _version = 1; - - // TODO: Implement database getter - static Future get database async { - // TODO: Return existing database or initialize new one - // Use the null-aware operator to check if _database exists - throw UnimplementedError('TODO: implement database getter'); - } - - // TODO: Implement _initDatabase method - static Future _initDatabase() async { - // TODO: Initialize the SQLite database - // - Get the databases path - // - Join with database name - // - Open database with version and callbacks - throw UnimplementedError('TODO: implement _initDatabase method'); - } - - // TODO: Implement _onCreate method - static Future _onCreate(Database db, int version) async { - // TODO: Create tables when database is first created - // Create users table with: id, name, email, created_at, updated_at - // Create posts table with: id, user_id, title, content, published, created_at, updated_at - // Include proper PRIMARY KEY and FOREIGN KEY constraints - throw UnimplementedError('TODO: implement _onCreate method'); - } - - // TODO: Implement _onUpgrade method - static Future _onUpgrade( - Database db, int oldVersion, int newVersion) async { - // TODO: Handle database schema upgrades - // For now, you can leave this empty or add migration logic later - } - - // User CRUD operations - - // TODO: Implement createUser method - static Future createUser(CreateUserRequest request) async { - // TODO: Insert user into database - // - Get database instance - // - Insert user data - // - Return User object with generated ID and timestamps - throw UnimplementedError('TODO: implement createUser method'); - } - - // TODO: Implement getUser method - static Future getUser(int id) async { - // TODO: Get user by ID from database - // - Query users table by ID - // - Return User object or null if not found - throw UnimplementedError('TODO: implement getUser method'); - } - - // TODO: Implement getAllUsers method - static Future> getAllUsers() async { - // TODO: Get all users from database - // - Query all users ordered by created_at - // - Convert query results to User objects - throw UnimplementedError('TODO: implement getAllUsers method'); - } - - // TODO: Implement updateUser method - static Future updateUser(int id, Map updates) async { - // TODO: Update user in database - // - Update user with provided data - // - Update the updated_at timestamp - // - Return updated User object - throw UnimplementedError('TODO: implement updateUser method'); - } - - // TODO: Implement deleteUser method - static Future deleteUser(int id) async { - // TODO: Delete user from database - // - Delete user by ID - // - Consider cascading deletes for related data - throw UnimplementedError('TODO: implement deleteUser method'); - } - - // TODO: Implement getUserCount method - static Future getUserCount() async { - // TODO: Count total number of users - // - Query count from users table - throw UnimplementedError('TODO: implement getUserCount method'); - } - - // TODO: Implement searchUsers method - static Future> searchUsers(String query) async { - // TODO: Search users by name or email - // - Use LIKE operator for pattern matching - // - Search in both name and email fields - throw UnimplementedError('TODO: implement searchUsers method'); - } - - // Database utility methods - - // TODO: Implement closeDatabase method - static Future closeDatabase() async { - // TODO: Close database connection - // - Close the database if it exists - // - Set _database to null - throw UnimplementedError('TODO: implement closeDatabase method'); - } - - // TODO: Implement clearAllData method - static Future clearAllData() async { - // TODO: Clear all data from database (for testing) - // - Delete all records from all tables - // - Reset auto-increment counters if needed - throw UnimplementedError('TODO: implement clearAllData method'); - } - - // TODO: Implement getDatabasePath method - static Future getDatabasePath() async { - // TODO: Get the full path to the database file - // - Return the complete path to the database file - throw UnimplementedError('TODO: implement getDatabasePath method'); - } -} diff --git a/labs/lab04/frontend/lib/services/preferences_service.dart b/labs/lab04/frontend/lib/services/preferences_service.dart deleted file mode 100644 index 616276c27..000000000 --- a/labs/lab04/frontend/lib/services/preferences_service.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:shared_preferences/shared_preferences.dart'; -import 'dart:convert'; - -class PreferencesService { - static SharedPreferences? _prefs; - - // TODO: Implement init method - static Future init() async { - // TODO: Initialize SharedPreferences - // Store the instance in _prefs variable - throw UnimplementedError('TODO: implement init method'); - } - - // TODO: Implement setString method - static Future setString(String key, String value) async { - // TODO: Set string value in SharedPreferences - // Make sure _prefs is not null - throw UnimplementedError('TODO: implement setString method'); - } - - // TODO: Implement getString method - static String? getString(String key) { - // TODO: Get string value from SharedPreferences - // Return null if key doesn't exist - throw UnimplementedError('TODO: implement getString method'); - } - - // TODO: Implement setInt method - static Future setInt(String key, int value) async { - // TODO: Set int value in SharedPreferences - throw UnimplementedError('TODO: implement setInt method'); - } - - // TODO: Implement getInt method - static int? getInt(String key) { - // TODO: Get int value from SharedPreferences - throw UnimplementedError('TODO: implement getInt method'); - } - - // TODO: Implement setBool method - static Future setBool(String key, bool value) async { - // TODO: Set bool value in SharedPreferences - throw UnimplementedError('TODO: implement setBool method'); - } - - // TODO: Implement getBool method - static bool? getBool(String key) { - // TODO: Get bool value from SharedPreferences - throw UnimplementedError('TODO: implement getBool method'); - } - - // TODO: Implement setStringList method - static Future setStringList(String key, List value) async { - // TODO: Set string list in SharedPreferences - throw UnimplementedError('TODO: implement setStringList method'); - } - - // TODO: Implement getStringList method - static List? getStringList(String key) { - // TODO: Get string list from SharedPreferences - throw UnimplementedError('TODO: implement getStringList method'); - } - - // TODO: Implement setObject method - static Future setObject(String key, Map value) async { - // TODO: Set object (as JSON string) in SharedPreferences - // Convert object to JSON string first - throw UnimplementedError('TODO: implement setObject method'); - } - - // TODO: Implement getObject method - static Map? getObject(String key) { - // TODO: Get object from SharedPreferences - // Parse JSON string back to Map - throw UnimplementedError('TODO: implement getObject method'); - } - - // TODO: Implement remove method - static Future remove(String key) async { - // TODO: Remove key from SharedPreferences - throw UnimplementedError('TODO: implement remove method'); - } - - // TODO: Implement clear method - static Future clear() async { - // TODO: Clear all data from SharedPreferences - throw UnimplementedError('TODO: implement clear method'); - } - - // TODO: Implement containsKey method - static bool containsKey(String key) { - // TODO: Check if key exists in SharedPreferences - throw UnimplementedError('TODO: implement containsKey method'); - } - - // TODO: Implement getAllKeys method - static Set getAllKeys() { - // TODO: Get all keys from SharedPreferences - throw UnimplementedError('TODO: implement getAllKeys method'); - } -} diff --git a/labs/lab04/frontend/lib/services/secure_storage_service.dart b/labs/lab04/frontend/lib/services/secure_storage_service.dart deleted file mode 100644 index 5ddb348e5..000000000 --- a/labs/lab04/frontend/lib/services/secure_storage_service.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'dart:convert'; - -class SecureStorageService { - static const FlutterSecureStorage _storage = FlutterSecureStorage( - aOptions: AndroidOptions( - encryptedSharedPreferences: true, - ), - iOptions: IOSOptions( - accessibility: KeychainAccessibility.first_unlock_this_device, - ), - ); - - // TODO: Implement saveAuthToken method - static Future saveAuthToken(String token) async { - // TODO: Save authentication token securely - // Use key 'auth_token' - throw UnimplementedError('TODO: implement saveAuthToken method'); - } - - // TODO: Implement getAuthToken method - static Future getAuthToken() async { - // TODO: Get authentication token from secure storage - // Return null if not found - throw UnimplementedError('TODO: implement getAuthToken method'); - } - - // TODO: Implement deleteAuthToken method - static Future deleteAuthToken() async { - // TODO: Delete authentication token from secure storage - throw UnimplementedError('TODO: implement deleteAuthToken method'); - } - - // TODO: Implement saveUserCredentials method - static Future saveUserCredentials( - String username, String password) async { - // TODO: Save user credentials securely - // Save username with key 'username' and password with key 'password' - throw UnimplementedError('TODO: implement saveUserCredentials method'); - } - - // TODO: Implement getUserCredentials method - static Future> getUserCredentials() async { - // TODO: Get user credentials from secure storage - // Return map with 'username' and 'password' keys - throw UnimplementedError('TODO: implement getUserCredentials method'); - } - - // TODO: Implement deleteUserCredentials method - static Future deleteUserCredentials() async { - // TODO: Delete user credentials from secure storage - // Delete both username and password - throw UnimplementedError('TODO: implement deleteUserCredentials method'); - } - - // TODO: Implement saveBiometricEnabled method - static Future saveBiometricEnabled(bool enabled) async { - // TODO: Save biometric setting securely - // Convert bool to string for storage - throw UnimplementedError('TODO: implement saveBiometricEnabled method'); - } - - // TODO: Implement isBiometricEnabled method - static Future isBiometricEnabled() async { - // TODO: Get biometric setting from secure storage - // Return false as default if not found - throw UnimplementedError('TODO: implement isBiometricEnabled method'); - } - - // TODO: Implement saveSecureData method - static Future saveSecureData(String key, String value) async { - // TODO: Save any secure data with custom key - throw UnimplementedError('TODO: implement saveSecureData method'); - } - - // TODO: Implement getSecureData method - static Future getSecureData(String key) async { - // TODO: Get secure data by key - throw UnimplementedError('TODO: implement getSecureData method'); - } - - // TODO: Implement deleteSecureData method - static Future deleteSecureData(String key) async { - // TODO: Delete secure data by key - throw UnimplementedError('TODO: implement deleteSecureData method'); - } - - // TODO: Implement saveObject method - static Future saveObject( - String key, Map object) async { - // TODO: Save object as JSON string in secure storage - // Convert object to JSON string first - throw UnimplementedError('TODO: implement saveObject method'); - } - - // TODO: Implement getObject method - static Future?> getObject(String key) async { - // TODO: Get object from secure storage - // Parse JSON string back to Map - throw UnimplementedError('TODO: implement getObject method'); - } - - // TODO: Implement containsKey method - static Future containsKey(String key) async { - // TODO: Check if key exists in secure storage - throw UnimplementedError('TODO: implement containsKey method'); - } - - // TODO: Implement getAllKeys method - static Future> getAllKeys() async { - // TODO: Get all keys from secure storage - // Return list of all stored keys - throw UnimplementedError('TODO: implement getAllKeys method'); - } - - // TODO: Implement clearAll method - static Future clearAll() async { - // TODO: Clear all data from secure storage - // Use deleteAll method from FlutterSecureStorage - throw UnimplementedError('TODO: implement clearAll method'); - } - - // TODO: Implement exportData method - static Future> exportData() async { - // TODO: Export all data (for backup purposes) - // Return all key-value pairs - // NOTE: This defeats the purpose of secure storage, use carefully - throw UnimplementedError('TODO: implement exportData method'); - } -} diff --git a/labs/lab04/frontend/pubspec.lock b/labs/lab04/frontend/pubspec.lock deleted file mode 100644 index 7cb1e095f..000000000 --- a/labs/lab04/frontend/pubspec.lock +++ /dev/null @@ -1,834 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f - url: "https://pub.dev" - source: hosted - version: "85.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: d10f9c2bf877706e39e9cc94d89ba28c7084f6e3529b4a2a96bb474fd0323647 - url: "https://pub.dev" - source: hosted - version: "7.5.5" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - build: - dependency: transitive - description: - name: build - sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - build_config: - dependency: transitive - description: - name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - build_daemon: - dependency: transitive - description: - name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" - url: "https://pub.dev" - source: hosted - version: "4.0.4" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 - url: "https://pub.dev" - source: hosted - version: "2.5.4" - build_runner: - dependency: "direct dev" - description: - name: build_runner - sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" - url: "https://pub.dev" - source: hosted - version: "9.1.2" - built_collection: - dependency: transitive - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - built_value: - dependency: transitive - description: - name: built_value - sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" - url: "https://pub.dev" - source: hosted - version: "8.10.1" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" - url: "https://pub.dev" - source: hosted - version: "4.10.1" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - crypto: - dependency: transitive - description: - name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.dev" - source: hosted - version: "3.0.6" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - flutter_secure_storage: - dependency: "direct main" - description: - name: flutter_secure_storage - sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" - url: "https://pub.dev" - source: hosted - version: "9.2.4" - flutter_secure_storage_linux: - dependency: transitive - description: - name: flutter_secure_storage_linux - sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 - url: "https://pub.dev" - source: hosted - version: "1.2.3" - flutter_secure_storage_macos: - dependency: transitive - description: - name: flutter_secure_storage_macos - sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" - url: "https://pub.dev" - source: hosted - version: "3.1.3" - flutter_secure_storage_platform_interface: - dependency: transitive - description: - name: flutter_secure_storage_platform_interface - sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 - url: "https://pub.dev" - source: hosted - version: "1.1.2" - flutter_secure_storage_web: - dependency: transitive - description: - name: flutter_secure_storage_web - sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - flutter_secure_storage_windows: - dependency: transitive - description: - name: flutter_secure_storage_windows - sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" - source: hosted - version: "4.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - graphs: - dependency: transitive - description: - name: graphs - sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - http: - dependency: transitive - description: - name: http - sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - io: - dependency: transitive - description: - name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.dev" - source: hosted - version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" - json_annotation: - dependency: "direct main" - description: - name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" - source: hosted - version: "4.9.0" - json_serializable: - dependency: "direct dev" - description: - name: json_serializable - sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c - url: "https://pub.dev" - source: hosted - version: "6.9.5" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" - url: "https://pub.dev" - source: hosted - version: "10.0.9" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 - url: "https://pub.dev" - source: hosted - version: "3.0.9" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" - lints: - dependency: transitive - description: - name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 - url: "https://pub.dev" - source: hosted - version: "3.0.0" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c - url: "https://pub.dev" - source: hosted - version: "1.16.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" - path: - dependency: "direct main" - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 - url: "https://pub.dev" - source: hosted - version: "2.2.17" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pool: - dependency: transitive - description: - name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.dev" - source: hosted - version: "1.5.0" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" - url: "https://pub.dev" - source: hosted - version: "2.5.3" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" - url: "https://pub.dev" - source: hosted - version: "2.4.10" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shelf: - dependency: transitive - description: - name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.dev" - source: hosted - version: "1.4.2" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_gen: - dependency: transitive - description: - name: source_gen - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - source_helper: - dependency: transitive - description: - name: source_helper - sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" - url: "https://pub.dev" - source: hosted - version: "1.3.5" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - sqflite: - dependency: "direct main" - description: - name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" - url: "https://pub.dev" - source: hosted - version: "2.5.5" - sqflite_common_ffi: - dependency: "direct dev" - description: - name: sqflite_common_ffi - sha256: "9faa2fedc5385ef238ce772589f7718c24cdddd27419b609bb9c6f703ea27988" - url: "https://pub.dev" - source: hosted - version: "2.3.6" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - sqlite3: - dependency: transitive - description: - name: sqlite3 - sha256: c0503c69b44d5714e6abbf4c1f51a3c3cc42b75ce785f44404765e4635481d38 - url: "https://pub.dev" - source: hosted - version: "2.7.6" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" - source: hosted - version: "2.1.1" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 - url: "https://pub.dev" - source: hosted - version: "3.4.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd - url: "https://pub.dev" - source: hosted - version: "0.7.4" - timing: - dependency: transitive - description: - name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" - url: "https://pub.dev" - source: hosted - version: "1.0.2" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 - url: "https://pub.dev" - source: hosted - version: "15.0.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" - source: hosted - version: "3.0.3" - win32: - dependency: transitive - description: - name: win32 - sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" - url: "https://pub.dev" - source: hosted - version: "5.14.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.8.0 <4.0.0" - flutter: ">=3.27.0" diff --git a/labs/lab04/frontend/pubspec.yaml b/labs/lab04/frontend/pubspec.yaml deleted file mode 100644 index 0e804d745..000000000 --- a/labs/lab04/frontend/pubspec.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: lab04_frontend -description: A Flutter lab for database and persistence operations - -publish_to: 'none' - -version: 1.0.0+1 - -environment: - sdk: '>=3.3.0 <4.0.0' - flutter: ">=3.19.0" - -dependencies: - flutter: - sdk: flutter - - # Database and storage - sqflite: ^2.3.0 - shared_preferences: ^2.2.2 - flutter_secure_storage: ^9.0.0 - path_provider: ^2.1.1 - path: ^1.8.3 - - # JSON serialization - json_annotation: ^4.8.1 - -dev_dependencies: - flutter_test: - sdk: flutter - - # JSON code generation - json_serializable: ^6.7.1 - build_runner: ^2.4.7 - - # Testing dependencies - sqflite_common_ffi: ^2.3.0 - - flutter_lints: ^3.0.0 - -flutter: - uses-material-design: true diff --git a/labs/lab04/frontend/test/database_service_test.dart b/labs/lab04/frontend/test/database_service_test.dart deleted file mode 100644 index f4070c9c9..000000000 --- a/labs/lab04/frontend/test/database_service_test.dart +++ /dev/null @@ -1,237 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import 'package:lab04_frontend/services/database_service.dart'; -import 'package:lab04_frontend/models/user.dart'; - -void main() { - group('DatabaseService Tests', () { - setUpAll(() { - // Initialize ffi for testing - sqfliteFfiInit(); - databaseFactory = databaseFactoryFfi; - }); - - setUp(() async { - // Clean up database before each test - try { - await DatabaseService.clearAllData(); - } catch (e) { - // Database might not exist yet, which is fine - } - }); - - tearDown(() async { - // Clean up after each test - try { - await DatabaseService.closeDatabase(); - } catch (e) { - // Ignore cleanup errors - } - }); - - test('should initialize database', () async { - final db = await DatabaseService.database; - expect(db, isNotNull); - }); - - test('should create user successfully', () async { - final request = CreateUserRequest( - name: 'John Doe', - email: 'john@example.com', - ); - - final user = await DatabaseService.createUser(request); - - expect(user.id, greaterThan(0)); - expect(user.name, equals(request.name)); - expect(user.email, equals(request.email)); - expect(user.createdAt, isNotNull); - expect(user.updatedAt, isNotNull); - }); - - test('should get user by ID', () async { - final request = CreateUserRequest( - name: 'Jane Doe', - email: 'jane@example.com', - ); - - final createdUser = await DatabaseService.createUser(request); - final retrievedUser = await DatabaseService.getUser(createdUser.id); - - expect(retrievedUser, isNotNull); - expect(retrievedUser!.id, equals(createdUser.id)); - expect(retrievedUser.name, equals(createdUser.name)); - expect(retrievedUser.email, equals(createdUser.email)); - }); - - test('should return null for non-existent user ID', () async { - final user = await DatabaseService.getUser(99999); - expect(user, isNull); - }); - - test('should get all users', () async { - final requests = [ - CreateUserRequest(name: 'User 1', email: 'user1@example.com'), - CreateUserRequest(name: 'User 2', email: 'user2@example.com'), - CreateUserRequest(name: 'User 3', email: 'user3@example.com'), - ]; - - for (final request in requests) { - await DatabaseService.createUser(request); - } - - final users = await DatabaseService.getAllUsers(); - expect(users.length, equals(requests.length)); - - for (int i = 0; i < requests.length; i++) { - expect(users.any((u) => u.name == requests[i].name), isTrue); - expect(users.any((u) => u.email == requests[i].email), isTrue); - } - }); - - test('should return empty list when no users exist', () async { - final users = await DatabaseService.getAllUsers(); - expect(users, isEmpty); - }); - - test('should update user successfully', () async { - final request = CreateUserRequest( - name: 'Original Name', - email: 'original@example.com', - ); - - final createdUser = await DatabaseService.createUser(request); - - final updates = { - 'name': 'Updated Name', - 'email': 'updated@example.com', - }; - - final updatedUser = - await DatabaseService.updateUser(createdUser.id, updates); - - expect(updatedUser.id, equals(createdUser.id)); - expect(updatedUser.name, equals('Updated Name')); - expect(updatedUser.email, equals('updated@example.com')); - expect(updatedUser.updatedAt.isAfter(createdUser.updatedAt), isTrue); - }); - - test('should update only specified fields', () async { - final request = CreateUserRequest( - name: 'Original Name', - email: 'original@example.com', - ); - - final createdUser = await DatabaseService.createUser(request); - - final updates = { - 'name': 'Updated Name Only', - }; - - final updatedUser = - await DatabaseService.updateUser(createdUser.id, updates); - - expect(updatedUser.name, equals('Updated Name Only')); - expect(updatedUser.email, - equals(createdUser.email)); // Should remain unchanged - }); - - test('should delete user successfully', () async { - final request = CreateUserRequest( - name: 'To Be Deleted', - email: 'delete@example.com', - ); - - final createdUser = await DatabaseService.createUser(request); - - await DatabaseService.deleteUser(createdUser.id); - - final deletedUser = await DatabaseService.getUser(createdUser.id); - expect(deletedUser, isNull); - }); - - test('should count users correctly', () async { - final initialCount = await DatabaseService.getUserCount(); - expect(initialCount, equals(0)); - - final requests = [ - CreateUserRequest(name: 'Counter 1', email: 'counter1@example.com'), - CreateUserRequest(name: 'Counter 2', email: 'counter2@example.com'), - ]; - - for (final request in requests) { - await DatabaseService.createUser(request); - } - - final finalCount = await DatabaseService.getUserCount(); - expect(finalCount, equals(requests.length)); - }); - - test('should search users by name', () async { - final requests = [ - CreateUserRequest(name: 'John Smith', email: 'john@example.com'), - CreateUserRequest(name: 'Jane Doe', email: 'jane@example.com'), - CreateUserRequest(name: 'Bob Johnson', email: 'bob@example.com'), - ]; - - for (final request in requests) { - await DatabaseService.createUser(request); - } - - final searchResults = await DatabaseService.searchUsers('John'); - expect(searchResults.length, equals(2)); // John Smith and Bob Johnson - expect(searchResults.any((u) => u.name.contains('John')), isTrue); - }); - - test('should search users by email', () async { - final requests = [ - CreateUserRequest(name: 'Test User 1', email: 'test1@gmail.com'), - CreateUserRequest(name: 'Test User 2', email: 'test2@yahoo.com'), - CreateUserRequest(name: 'Test User 3', email: 'test3@gmail.com'), - ]; - - for (final request in requests) { - await DatabaseService.createUser(request); - } - - final searchResults = await DatabaseService.searchUsers('gmail'); - expect(searchResults.length, equals(2)); // Users with gmail addresses - expect(searchResults.every((u) => u.email.contains('gmail')), isTrue); - }); - - test('should return empty search results for non-matching query', () async { - final request = CreateUserRequest( - name: 'Test User', - email: 'test@example.com', - ); - - await DatabaseService.createUser(request); - - final searchResults = await DatabaseService.searchUsers('nonexistent'); - expect(searchResults, isEmpty); - }); - - test('should get database path', () async { - final path = await DatabaseService.getDatabasePath(); - expect(path, isNotEmpty); - expect(path.contains('lab04_app.db'), isTrue); - }); - - test('should clear all data', () async { - final request = CreateUserRequest( - name: 'Test User', - email: 'test@example.com', - ); - - await DatabaseService.createUser(request); - - var count = await DatabaseService.getUserCount(); - expect(count, equals(1)); - - await DatabaseService.clearAllData(); - - count = await DatabaseService.getUserCount(); - expect(count, equals(0)); - }); - }); -} diff --git a/labs/lab04/frontend/test/preferences_service_test.dart b/labs/lab04/frontend/test/preferences_service_test.dart deleted file mode 100644 index fc873ceec..000000000 --- a/labs/lab04/frontend/test/preferences_service_test.dart +++ /dev/null @@ -1,156 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:lab04_frontend/services/preferences_service.dart'; - -void main() { - group('PreferencesService Tests', () { - setUp(() async { - // Clear SharedPreferences before each test - SharedPreferences.setMockInitialValues({}); - await PreferencesService.init(); - }); - - test('should initialize SharedPreferences', () async { - expect(() => PreferencesService.init(), returnsNormally); - }); - - test('should set and get string values', () async { - const key = 'test_string'; - const value = 'Hello World'; - - await PreferencesService.setString(key, value); - final result = PreferencesService.getString(key); - - expect(result, equals(value)); - }); - - test('should return null for non-existent string key', () { - const key = 'non_existent_key'; - final result = PreferencesService.getString(key); - expect(result, isNull); - }); - - test('should set and get int values', () async { - const key = 'test_int'; - const value = 42; - - await PreferencesService.setInt(key, value); - final result = PreferencesService.getInt(key); - - expect(result, equals(value)); - }); - - test('should return null for non-existent int key', () { - const key = 'non_existent_int'; - final result = PreferencesService.getInt(key); - expect(result, isNull); - }); - - test('should set and get bool values', () async { - const key = 'test_bool'; - const value = true; - - await PreferencesService.setBool(key, value); - final result = PreferencesService.getBool(key); - - expect(result, equals(value)); - }); - - test('should set and get string list values', () async { - const key = 'test_string_list'; - const value = ['item1', 'item2', 'item3']; - - await PreferencesService.setStringList(key, value); - final result = PreferencesService.getStringList(key); - - expect(result, equals(value)); - }); - - test('should set and get object values', () async { - const key = 'test_object'; - const value = { - 'name': 'John Doe', - 'age': 30, - 'isActive': true, - }; - - await PreferencesService.setObject(key, value); - final result = PreferencesService.getObject(key); - - expect(result, equals(value)); - }); - - test('should return null for non-existent object key', () { - const key = 'non_existent_object'; - final result = PreferencesService.getObject(key); - expect(result, isNull); - }); - - test('should remove specific key', () async { - const key = 'test_remove'; - const value = 'value_to_remove'; - - await PreferencesService.setString(key, value); - expect(PreferencesService.getString(key), equals(value)); - - await PreferencesService.remove(key); - expect(PreferencesService.getString(key), isNull); - }); - - test('should check if key exists', () async { - const key = 'test_contains'; - const value = 'test_value'; - - expect(PreferencesService.containsKey(key), isFalse); - - await PreferencesService.setString(key, value); - expect(PreferencesService.containsKey(key), isTrue); - }); - - test('should get all keys', () async { - const keys = ['key1', 'key2', 'key3']; - const values = ['value1', 'value2', 'value3']; - - for (int i = 0; i < keys.length; i++) { - await PreferencesService.setString(keys[i], values[i]); - } - - final allKeys = PreferencesService.getAllKeys(); - for (final key in keys) { - expect(allKeys.contains(key), isTrue); - } - }); - - test('should clear all preferences', () async { - await PreferencesService.setString('key1', 'value1'); - await PreferencesService.setInt('key2', 42); - await PreferencesService.setBool('key3', true); - - expect(PreferencesService.getAllKeys().length, greaterThan(0)); - - await PreferencesService.clear(); - expect(PreferencesService.getAllKeys().length, equals(0)); - }); - - test('should handle complex object serialization', () async { - const key = 'complex_object'; - final value = { - 'user': { - 'id': 123, - 'name': 'Jane Doe', - 'preferences': { - 'theme': 'dark', - 'notifications': true, - }, - 'tags': ['developer', 'flutter', 'dart'], - }, - 'timestamp': '2025-01-09T12:00:00Z', - }; - - await PreferencesService.setObject(key, value); - final result = PreferencesService.getObject(key); - - expect(result, equals(value)); - }); - }); -} diff --git a/labs/lab04/frontend/test/secure_storage_service_test.dart b/labs/lab04/frontend/test/secure_storage_service_test.dart deleted file mode 100644 index deb45a8a3..000000000 --- a/labs/lab04/frontend/test/secure_storage_service_test.dart +++ /dev/null @@ -1,245 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:lab04_frontend/services/secure_storage_service.dart'; - -void main() { - // Initialize Flutter bindings for platform channels - TestWidgetsFlutterBinding.ensureInitialized(); - - group('SecureStorageService Tests', () { - setUp(() async { - // Clear secure storage before each test - await SecureStorageService.clearAll(); - }); - - tearDown(() async { - // Clean up after each test - await SecureStorageService.clearAll(); - }); - - test('should save and get auth token', () async { - const token = 'test_auth_token_12345'; - - await SecureStorageService.saveAuthToken(token); - final retrievedToken = await SecureStorageService.getAuthToken(); - - expect(retrievedToken, equals(token)); - }); - - test('should return null for non-existent auth token', () async { - final token = await SecureStorageService.getAuthToken(); - expect(token, isNull); - }); - - test('should delete auth token', () async { - const token = 'token_to_delete'; - - await SecureStorageService.saveAuthToken(token); - expect(await SecureStorageService.getAuthToken(), equals(token)); - - await SecureStorageService.deleteAuthToken(); - expect(await SecureStorageService.getAuthToken(), isNull); - }); - - test('should save and get user credentials', () async { - const username = 'test_user'; - const password = 'secure_password_123'; - - await SecureStorageService.saveUserCredentials(username, password); - final credentials = await SecureStorageService.getUserCredentials(); - - expect(credentials['username'], equals(username)); - expect(credentials['password'], equals(password)); - }); - - test('should return null credentials when not set', () async { - final credentials = await SecureStorageService.getUserCredentials(); - expect(credentials['username'], isNull); - expect(credentials['password'], isNull); - }); - - test('should delete user credentials', () async { - const username = 'user_to_delete'; - const password = 'password_to_delete'; - - await SecureStorageService.saveUserCredentials(username, password); - expect((await SecureStorageService.getUserCredentials())['username'], - equals(username)); - - await SecureStorageService.deleteUserCredentials(); - final credentials = await SecureStorageService.getUserCredentials(); - expect(credentials['username'], isNull); - expect(credentials['password'], isNull); - }); - - test('should save and get biometric enabled setting', () async { - await SecureStorageService.saveBiometricEnabled(true); - expect(await SecureStorageService.isBiometricEnabled(), isTrue); - - await SecureStorageService.saveBiometricEnabled(false); - expect(await SecureStorageService.isBiometricEnabled(), isFalse); - }); - - test('should return false for biometric setting when not set', () async { - final isEnabled = await SecureStorageService.isBiometricEnabled(); - expect(isEnabled, isFalse); - }); - - test('should save and get secure data with custom key', () async { - const key = 'custom_secure_key'; - const value = 'very_secret_data'; - - await SecureStorageService.saveSecureData(key, value); - final retrievedValue = await SecureStorageService.getSecureData(key); - - expect(retrievedValue, equals(value)); - }); - - test('should return null for non-existent secure data', () async { - final value = - await SecureStorageService.getSecureData('non_existent_key'); - expect(value, isNull); - }); - - test('should delete secure data by key', () async { - const key = 'key_to_delete'; - const value = 'value_to_delete'; - - await SecureStorageService.saveSecureData(key, value); - expect(await SecureStorageService.getSecureData(key), equals(value)); - - await SecureStorageService.deleteSecureData(key); - expect(await SecureStorageService.getSecureData(key), isNull); - }); - - test('should save and get object data', () async { - const key = 'user_profile'; - final objectData = { - 'id': 123, - 'name': 'John Doe', - 'preferences': { - 'theme': 'dark', - 'notifications': true, - }, - 'roles': ['user', 'admin'], - }; - - await SecureStorageService.saveObject(key, objectData); - final retrievedObject = await SecureStorageService.getObject(key); - - expect(retrievedObject, equals(objectData)); - }); - - test('should return null for non-existent object', () async { - final object = - await SecureStorageService.getObject('non_existent_object'); - expect(object, isNull); - }); - - test('should check if key exists', () async { - const key = 'existence_test_key'; - const value = 'test_value'; - - expect(await SecureStorageService.containsKey(key), isFalse); - - await SecureStorageService.saveSecureData(key, value); - expect(await SecureStorageService.containsKey(key), isTrue); - }); - - test('should get all keys', () async { - final testData = { - 'key1': 'value1', - 'key2': 'value2', - 'key3': 'value3', - }; - - for (final entry in testData.entries) { - await SecureStorageService.saveSecureData(entry.key, entry.value); - } - - final allKeys = await SecureStorageService.getAllKeys(); - for (final key in testData.keys) { - expect(allKeys.contains(key), isTrue); - } - }); - - test('should clear all data', () async { - await SecureStorageService.saveAuthToken('test_token'); - await SecureStorageService.saveUserCredentials('user', 'pass'); - await SecureStorageService.saveSecureData('key', 'value'); - - final keysBeforeClear = await SecureStorageService.getAllKeys(); - expect(keysBeforeClear.length, greaterThan(0)); - - await SecureStorageService.clearAll(); - - final keysAfterClear = await SecureStorageService.getAllKeys(); - expect(keysAfterClear, isEmpty); - }); - - test('should export all data', () async { - final testData = { - 'auth_token': 'token123', - 'username': 'testuser', - 'password': 'testpass', - 'custom_key': 'custom_value', - }; - - for (final entry in testData.entries) { - await SecureStorageService.saveSecureData(entry.key, entry.value); - } - - final exportedData = await SecureStorageService.exportData(); - - for (final entry in testData.entries) { - expect(exportedData[entry.key], equals(entry.value)); - } - }); - - test('should handle complex object serialization', () async { - const key = 'complex_settings'; - final complexObject = { - 'user': { - 'id': 456, - 'profile': { - 'firstName': 'Jane', - 'lastName': 'Smith', - 'avatar': null, - }, - 'settings': { - 'privacy': { - 'showEmail': false, - 'showPhone': true, - }, - 'notifications': { - 'email': true, - 'push': false, - 'sms': true, - }, - }, - 'tags': ['premium', 'verified'], - }, - 'app': { - 'version': '1.0.0', - 'lastLogin': '2025-01-09T12:00:00Z', - 'sessionTimeout': 3600, - }, - }; - - await SecureStorageService.saveObject(key, complexObject); - final retrievedObject = await SecureStorageService.getObject(key); - - expect(retrievedObject, equals(complexObject)); - }); - - test('should handle empty and null values', () async { - await SecureStorageService.saveSecureData('empty_string', ''); - final emptyValue = - await SecureStorageService.getSecureData('empty_string'); - expect(emptyValue, equals('')); - - final nullObject = await SecureStorageService.getObject('null_object'); - expect(nullObject, isNull); - }); - }); -} diff --git a/labs/lab04/frontend/web/favicon.png b/labs/lab04/frontend/web/favicon.png deleted file mode 100644 index 8aaa46ac1..000000000 Binary files a/labs/lab04/frontend/web/favicon.png and /dev/null differ diff --git a/labs/lab04/frontend/web/icons/Icon-192.png b/labs/lab04/frontend/web/icons/Icon-192.png deleted file mode 100644 index b749bfef0..000000000 Binary files a/labs/lab04/frontend/web/icons/Icon-192.png and /dev/null differ diff --git a/labs/lab04/frontend/web/icons/Icon-512.png b/labs/lab04/frontend/web/icons/Icon-512.png deleted file mode 100644 index 88cfd48df..000000000 Binary files a/labs/lab04/frontend/web/icons/Icon-512.png and /dev/null differ diff --git a/labs/lab04/frontend/web/icons/Icon-maskable-192.png b/labs/lab04/frontend/web/icons/Icon-maskable-192.png deleted file mode 100644 index eb9b4d76e..000000000 Binary files a/labs/lab04/frontend/web/icons/Icon-maskable-192.png and /dev/null differ diff --git a/labs/lab04/frontend/web/icons/Icon-maskable-512.png b/labs/lab04/frontend/web/icons/Icon-maskable-512.png deleted file mode 100644 index d69c56691..000000000 Binary files a/labs/lab04/frontend/web/icons/Icon-maskable-512.png and /dev/null differ diff --git a/labs/lab04/frontend/web/index.html b/labs/lab04/frontend/web/index.html deleted file mode 100644 index 7c602e6a1..000000000 --- a/labs/lab04/frontend/web/index.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - Lab 04 - Database & Persistence - - - - - - - diff --git a/labs/lab04/frontend/web/manifest.json b/labs/lab04/frontend/web/manifest.json deleted file mode 100644 index 409f4c96f..000000000 --- a/labs/lab04/frontend/web/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "frontend", - "short_name": "frontend", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/labs/lab05/backend/jwtservice/jwt.go b/labs/lab05/backend/jwtservice/jwt.go index 860541df4..6bbd4eba9 100644 --- a/labs/lab05/backend/jwtservice/jwt.go +++ b/labs/lab05/backend/jwtservice/jwt.go @@ -2,7 +2,9 @@ package jwtservice import ( "errors" - _ "github.com/golang-jwt/jwt/v4" + "time" + + "github.com/golang-jwt/jwt/v4" ) // JWTService handles JWT token operations @@ -10,39 +12,65 @@ type JWTService struct { secretKey string } -// TODO: Implement NewJWTService function -// NewJWTService creates a new JWT service -// Requirements: -// - secretKey must not be empty func NewJWTService(secretKey string) (*JWTService, error) { - // TODO: Implement this function - // Validate secretKey and create service instance - return nil, errors.New("not implemented") + if secretKey == "" { + return nil, ErrEmptySecretKey + } + + newJWT := &JWTService{secretKey: secretKey} + return newJWT, nil } -// TODO: Implement GenerateToken method -// GenerateToken creates a new JWT token with user claims -// Requirements: -// - userID must be positive -// - email must not be empty -// - Token expires in 24 hours -// - Use HS256 signing method func (j *JWTService) GenerateToken(userID int, email string) (string, error) { - // TODO: Implement token generation - // Create claims with userID, email, and expiration - // Sign token with secret key - return "", errors.New("not implemented") + if userID <= 0 { + return "", ErrNonPosUserID + } + if email == "" { + return "", ErrEmptyEmail + } + + claims := Claims{ + UserID: userID, + Email: email, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + generatedToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return generatedToken.SignedString([]byte(j.secretKey)) } -// TODO: Implement ValidateToken method -// ValidateToken parses and validates a JWT token -// Requirements: -// - Check token signature with secret key -// - Verify token is not expired -// - Return parsed claims on success func (j *JWTService) ValidateToken(tokenString string) (*Claims, error) { - // TODO: Implement token validation - // Parse token and verify signature - // Return claims if valid - return nil, errors.New("not implemented") + token, err := jwt.Parse(tokenString, + func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, ErrUnexpSignMethod + } + return []byte(j.secretKey), nil + }) + + if err != nil || !token.Valid { + return nil, ErrInvToken + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, ErrInvClaims + } + + return &Claims{ + UserID: int(claims["user_id"].(float64)), + Email: claims["email"].(string), + }, nil } + +var ( + ErrEmptySecretKey = errors.New("secret cannot be empty") + ErrNonPosUserID = errors.New("user ID must be positive") + ErrEmptyEmail = errors.New("email cannot be empty") + ErrUnexpSignMethod = errors.New("unexpected signing method") + ErrInvToken = errors.New("invalid token") + ErrInvClaims = errors.New("invalid claims") +) diff --git a/labs/lab05/backend/security/password.go b/labs/lab05/backend/security/password.go index 484f80ab8..9533c2562 100644 --- a/labs/lab05/backend/security/password.go +++ b/labs/lab05/backend/security/password.go @@ -2,54 +2,50 @@ package security import ( "errors" - _ "regexp" + "strings" - _ "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/bcrypt" ) // PasswordService handles password operations type PasswordService struct{} -// TODO: Implement NewPasswordService function -// NewPasswordService creates a new password service func NewPasswordService() *PasswordService { - // TODO: Implement this function - // Return a new PasswordService instance - return nil + return &PasswordService{} } -// TODO: Implement HashPassword method -// HashPassword hashes a password using bcrypt -// Requirements: -// - password must not be empty -// - use bcrypt with cost 10 -// - return the hashed password as string func (p *PasswordService) HashPassword(password string) (string, error) { - // TODO: Implement password hashing - // Use golang.org/x/crypto/bcrypt.GenerateFromPassword - return "", errors.New("not implemented") + if err := ValidatePassword(password); err != nil { + return "", err + } + + hashedPw, err := bcrypt.GenerateFromPassword([]byte(password), 10) + if err != nil { + return "", nil + } + return string(hashedPw), nil } -// TODO: Implement VerifyPassword method -// VerifyPassword checks if password matches hash -// Requirements: -// - password and hash must not be empty -// - return true if password matches hash -// - return false if password doesn't match func (p *PasswordService) VerifyPassword(password, hash string) bool { - // TODO: Implement password verification - // Use bcrypt.CompareHashAndPassword - // Return true only if passwords match exactly - return false + compareResult := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return compareResult == nil } -// TODO: Implement ValidatePassword function -// ValidatePassword checks if password meets basic requirements -// Requirements: -// - At least 6 characters -// - Contains at least one letter and one number func ValidatePassword(password string) error { - // TODO: Implement password validation - // Check length and basic complexity requirements - return errors.New("not implemented") + if len(password) < 6 { + return ErrInvPw + } + if !strings.ContainsAny(password, letters) || !strings.ContainsAny(password, numbers) { + return ErrInvPw + } + return nil } + +var ( + ErrInvPw = errors.New("password must be at least 6 charecters long and contain at least one letter and one number") +) + +var ( + letters = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm" + numbers = "1234567890" +) diff --git a/labs/lab05/backend/userdomain/user.go b/labs/lab05/backend/userdomain/user.go index 8794c595f..0218ddc05 100644 --- a/labs/lab05/backend/userdomain/user.go +++ b/labs/lab05/backend/userdomain/user.go @@ -2,7 +2,7 @@ package userdomain import ( "errors" - _ "regexp" + "regexp" "strings" "time" ) @@ -17,52 +17,70 @@ type User struct { UpdatedAt time.Time `json:"updated_at"` } -// TODO: Implement NewUser function -// NewUser creates a new user with validation -// Requirements: -// - Email must be valid format -// - Name must be 2-51 characters -// - Password must be at least 8 characters -// - CreatedAt and UpdatedAt should be set to current time func NewUser(email, name, password string) (*User, error) { - // TODO: Implement this function - // Hint: Use ValidateEmail, ValidateName, ValidatePassword helper functions - return nil, errors.New("not implemented") + if err := ValidateEmail(email); err != nil { + return nil, err + } + if err := ValidateName(name); err != nil { + return nil, err + } + if err := ValidatePassword(password); err != nil { + return nil, err + } + + timeNow := time.Now() + + user := &User{ + Email: strings.ToLower(strings.TrimSpace(email)), + Name: strings.TrimSpace(name), + Password: strings.TrimSpace(password), + CreatedAt: timeNow, + UpdatedAt: timeNow, + } + + return user, nil } -// TODO: Implement Validate method -// Validate checks if the user data is valid func (u *User) Validate() error { - // TODO: Implement validation logic - // Check email, name, and password validity - return errors.New("not implemented") + if err := ValidateEmail(u.Email); err != nil { + return err + } + if err := ValidateName(u.Name); err != nil { + return err + } + if err := ValidatePassword(u.Password); err != nil { + return err + } + return nil } -// TODO: Implement ValidateEmail function -// ValidateEmail checks if email format is valid func ValidateEmail(email string) error { - // TODO: Implement email validation - // Use regex pattern to validate email format - // Email should not be empty and should match standard email pattern - return errors.New("not implemented") + normEmail := strings.ToLower(strings.TrimSpace(email)) + if !emailRegex.MatchString(normEmail) { + return ErrInvEmail + } + return nil } -// TODO: Implement ValidateName function -// ValidateName checks if name is valid func ValidateName(name string) error { - // TODO: Implement name validation - // Name should be 2-50 characters, trimmed of whitespace - // Should not be empty after trimming - return errors.New("not implemented") + trimmedName := strings.TrimSpace(name) + + if len(trimmedName) < 2 || len(trimmedName) > 50 { + return ErrInvName + } + return nil } -// TODO: Implement ValidatePassword function -// ValidatePassword checks if password meets security requirements func ValidatePassword(password string) error { - // TODO: Implement password validation - // Password should be at least 8 characters - // Should contain at least one uppercase, lowercase, and number - return errors.New("not implemented") + valid := len(password) >= 8 + valid = valid && strings.ContainsAny(password, uppsercase) + valid = valid && strings.ContainsAny(password, lowercase) + valid = valid && strings.ContainsAny(password, numbers) + + if !valid { + return ErrInvPassword + } + return nil } // UpdateName updates the user's name with validation @@ -84,3 +102,17 @@ func (u *User) UpdateEmail(email string) error { u.UpdatedAt = time.Now() return nil } + +var ( + uppsercase = "QWERTYUIOPASDFGHJKLZXCVBNM" + lowercase = "qwertyuiopasdfghjklzxcvbnm" + numbers = "1234567890" +) + +var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + +var ( + ErrInvPassword = errors.New("password should be at least 8 characters long, contain at least one uppercase letter, lowercase letter and a number") + ErrInvName = errors.New("name should be from 2 to 50 characters long") + ErrInvEmail = errors.New("invalid email format") +) diff --git a/labs/lab05/frontend/.dart_tool/flutter_build/dart_plugin_registrant.dart b/labs/lab05/frontend/.dart_tool/flutter_build/dart_plugin_registrant.dart new file mode 100644 index 000000000..0c4415651 --- /dev/null +++ b/labs/lab05/frontend/.dart_tool/flutter_build/dart_plugin_registrant.dart @@ -0,0 +1,82 @@ +// +// Generated file. Do not edit. +// This file is generated from template in file `flutter_tools/lib/src/flutter_plugins.dart`. +// + +// @dart = 3.5 + +import 'dart:io'; // flutter_ignore: dart_io_import. +import 'package:path_provider_android/path_provider_android.dart'; +import 'package:path_provider_foundation/path_provider_foundation.dart'; +import 'package:path_provider_linux/path_provider_linux.dart'; +import 'package:path_provider_foundation/path_provider_foundation.dart'; +import 'package:flutter_secure_storage_windows/flutter_secure_storage_windows.dart'; +import 'package:path_provider_windows/path_provider_windows.dart'; + +@pragma('vm:entry-point') +class _PluginRegistrant { + + @pragma('vm:entry-point') + static void register() { + if (Platform.isAndroid) { + try { + PathProviderAndroid.registerWith(); + } catch (err) { + print( + '`path_provider_android` threw an error: $err. ' + 'The app may not function as expected until you remove this plugin from pubspec.yaml' + ); + } + + } else if (Platform.isIOS) { + try { + PathProviderFoundation.registerWith(); + } catch (err) { + print( + '`path_provider_foundation` threw an error: $err. ' + 'The app may not function as expected until you remove this plugin from pubspec.yaml' + ); + } + + } else if (Platform.isLinux) { + try { + PathProviderLinux.registerWith(); + } catch (err) { + print( + '`path_provider_linux` threw an error: $err. ' + 'The app may not function as expected until you remove this plugin from pubspec.yaml' + ); + } + + } else if (Platform.isMacOS) { + try { + PathProviderFoundation.registerWith(); + } catch (err) { + print( + '`path_provider_foundation` threw an error: $err. ' + 'The app may not function as expected until you remove this plugin from pubspec.yaml' + ); + } + + } else if (Platform.isWindows) { + try { + FlutterSecureStorageWindows.registerWith(); + } catch (err) { + print( + '`flutter_secure_storage_windows` threw an error: $err. ' + 'The app may not function as expected until you remove this plugin from pubspec.yaml' + ); + } + + try { + PathProviderWindows.registerWith(); + } catch (err) { + print( + '`path_provider_windows` threw an error: $err. ' + 'The app may not function as expected until you remove this plugin from pubspec.yaml' + ); + } + + } + } +} diff --git a/labs/lab05/frontend/.dart_tool/package_config.json b/labs/lab05/frontend/.dart_tool/package_config.json index 17f4ca1e5..ca6bad0ef 100644 --- a/labs/lab05/frontend/.dart_tool/package_config.json +++ b/labs/lab05/frontend/.dart_tool/package_config.json @@ -3,547 +3,547 @@ "packages": [ { "name": "_fe_analyzer_shared", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-85.0.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-85.0.0", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "analyzer", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/analyzer-7.5.6", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/analyzer-7.5.6", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "args", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/args-2.7.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/args-2.7.0", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "async", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/async-2.13.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/async-2.13.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "boolean_selector", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "build", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/build-2.5.4", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/build-2.5.4", "packageUri": "lib/", "languageVersion": "3.7" }, { "name": "build_config", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/build_config-1.1.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/build_config-1.1.2", "packageUri": "lib/", "languageVersion": "3.6" }, { "name": "build_daemon", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/build_daemon-4.0.4", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/build_daemon-4.0.4", "packageUri": "lib/", "languageVersion": "3.6" }, { "name": "build_resolvers", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/build_resolvers-2.5.4", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/build_resolvers-2.5.4", "packageUri": "lib/", "languageVersion": "3.7" }, { "name": "build_runner", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/build_runner-2.5.4", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/build_runner-2.5.4", "packageUri": "lib/", "languageVersion": "3.7" }, { "name": "build_runner_core", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/build_runner_core-9.1.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/build_runner_core-9.1.2", "packageUri": "lib/", "languageVersion": "3.7" }, { "name": "built_collection", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/built_collection-5.1.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/built_collection-5.1.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "built_value", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/built_value-8.10.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/built_value-8.10.1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "characters", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/characters-1.4.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/characters-1.4.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "checked_yaml", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/checked_yaml-2.0.4", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/checked_yaml-2.0.4", "packageUri": "lib/", "languageVersion": "3.8" }, { "name": "clock", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/clock-1.1.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/clock-1.1.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "code_builder", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/code_builder-4.10.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/code_builder-4.10.1", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "collection", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/collection-1.19.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/collection-1.19.1", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "convert", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/convert-3.1.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/convert-3.1.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "crypto", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/crypto-3.0.6", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/crypto-3.0.6", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "dart_style", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/dart_style-3.1.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/dart_style-3.1.0", "packageUri": "lib/", "languageVersion": "3.7" }, { "name": "dartz", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/dartz-0.10.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/dartz-0.10.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "equatable", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/equatable-2.0.7", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/equatable-2.0.7", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "fake_async", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/fake_async-1.3.3", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/fake_async-1.3.3", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "ffi", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/ffi-2.1.4", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/ffi-2.1.4", "packageUri": "lib/", "languageVersion": "3.7" }, { "name": "file", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/file-7.0.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/file-7.0.1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "fixnum", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/fixnum-1.1.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/fixnum-1.1.1", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "flutter", - "rootUri": "file:///opt/homebrew/Caskroom/flutter/3.32.4/flutter/packages/flutter", + "rootUri": "file:///home/pavmash/flutter/packages/flutter", "packageUri": "lib/", "languageVersion": "3.7" }, { "name": "flutter_lints", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_lints-3.0.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_lints-3.0.2", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "flutter_secure_storage", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "flutter_secure_storage_linux", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "flutter_secure_storage_macos", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "flutter_secure_storage_platform_interface", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_platform_interface-1.1.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_platform_interface-1.1.2", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "flutter_secure_storage_web", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "flutter_secure_storage_windows", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "flutter_test", - "rootUri": "file:///opt/homebrew/Caskroom/flutter/3.32.4/flutter/packages/flutter_test", + "rootUri": "file:///home/pavmash/flutter/packages/flutter_test", "packageUri": "lib/", "languageVersion": "3.7" }, { "name": "flutter_web_plugins", - "rootUri": "file:///opt/homebrew/Caskroom/flutter/3.32.4/flutter/packages/flutter_web_plugins", + "rootUri": "file:///home/pavmash/flutter/packages/flutter_web_plugins", "packageUri": "lib/", "languageVersion": "3.7" }, { "name": "frontend_server_client", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "glob", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/glob-2.1.3", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/glob-2.1.3", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "graphs", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/graphs-2.3.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/graphs-2.3.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "http", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/http-1.4.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/http-1.4.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "http_multi_server", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/http_multi_server-3.2.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/http_multi_server-3.2.2", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "http_parser", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/http_parser-4.1.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/http_parser-4.1.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "io", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/io-1.0.5", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/io-1.0.5", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "js", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/js-0.6.7", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/js-0.6.7", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "json_annotation", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/json_annotation-4.9.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/json_annotation-4.9.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "leak_tracker", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/leak_tracker-10.0.9", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/leak_tracker-10.0.9", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "leak_tracker_flutter_testing", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.9", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.9", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "leak_tracker_testing", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.1", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "lints", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/lints-3.0.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/lints-3.0.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "logging", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/logging-1.3.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/logging-1.3.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "matcher", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/matcher-0.12.17", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/matcher-0.12.17", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "material_color_utilities", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "meta", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/meta-1.16.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/meta-1.16.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "mime", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/mime-2.0.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/mime-2.0.0", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "mockito", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/mockito-5.4.6", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/mockito-5.4.6", "packageUri": "lib/", "languageVersion": "3.7" }, { "name": "package_config", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/package_config-2.2.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/package_config-2.2.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "path", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/path-1.9.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/path-1.9.1", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "path_provider", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider-2.1.5", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider-2.1.5", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "path_provider_android", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17", "packageUri": "lib/", "languageVersion": "3.6" }, { "name": "path_provider_foundation", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "path_provider_linux", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "path_provider_platform_interface", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_platform_interface-2.1.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_platform_interface-2.1.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "path_provider_windows", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "platform", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/platform-3.1.6", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/platform-3.1.6", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "plugin_platform_interface", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/plugin_platform_interface-2.1.8", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/plugin_platform_interface-2.1.8", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "pool", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/pool-1.5.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/pool-1.5.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "pub_semver", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/pub_semver-2.2.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/pub_semver-2.2.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "pubspec_parse", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/pubspec_parse-1.5.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/pubspec_parse-1.5.0", "packageUri": "lib/", "languageVersion": "3.6" }, { "name": "shelf", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/shelf-1.4.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/shelf-1.4.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "shelf_web_socket", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/shelf_web_socket-3.0.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/shelf_web_socket-3.0.0", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "sky_engine", - "rootUri": "file:///opt/homebrew/Caskroom/flutter/3.32.4/flutter/bin/cache/pkg/sky_engine", + "rootUri": "file:///home/pavmash/flutter/bin/cache/pkg/sky_engine", "packageUri": "lib/", "languageVersion": "3.7" }, { "name": "source_gen", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/source_gen-2.0.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/source_gen-2.0.0", "packageUri": "lib/", "languageVersion": "3.6" }, { "name": "source_span", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/source_span-1.10.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/source_span-1.10.1", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "stack_trace", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/stack_trace-1.12.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/stack_trace-1.12.1", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "stream_channel", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/stream_channel-2.1.4", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/stream_channel-2.1.4", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "stream_transform", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/stream_transform-2.1.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/stream_transform-2.1.1", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "string_scanner", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/string_scanner-1.4.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/string_scanner-1.4.1", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "term_glyph", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/term_glyph-1.2.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/term_glyph-1.2.2", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "test_api", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/test_api-0.7.4", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/test_api-0.7.4", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "timing", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/timing-1.0.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/timing-1.0.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "typed_data", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/typed_data-1.4.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/typed_data-1.4.0", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "vector_math", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/vector_math-2.1.4", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/vector_math-2.1.4", "packageUri": "lib/", "languageVersion": "2.14" }, { "name": "vm_service", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/vm_service-15.0.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/vm_service-15.0.0", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "watcher", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/watcher-1.1.2", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/watcher-1.1.2", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "web", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/web-1.1.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/web-1.1.1", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "web_socket", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/web_socket-1.0.1", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/web_socket-1.0.1", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "web_socket_channel", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/web_socket_channel-3.0.3", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/web_socket_channel-3.0.3", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "win32", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/win32-5.14.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/win32-5.14.0", "packageUri": "lib/", "languageVersion": "3.8" }, { "name": "xdg_directories", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "yaml", - "rootUri": "file:///Users/timur/.pub-cache/hosted/pub.dev/yaml-3.1.3", + "rootUri": "file:///home/pavmash/.pub-cache/hosted/pub.dev/yaml-3.1.3", "packageUri": "lib/", "languageVersion": "3.4" }, @@ -556,7 +556,7 @@ ], "generator": "pub", "generatorVersion": "3.8.1", - "flutterRoot": "file:///opt/homebrew/Caskroom/flutter/3.32.4/flutter", - "flutterVersion": "3.32.4", - "pubCache": "file:///Users/timur/.pub-cache" + "flutterRoot": "file:///home/pavmash/flutter", + "flutterVersion": "3.32.5", + "pubCache": "file:///home/pavmash/.pub-cache" } diff --git a/labs/lab05/frontend/.dart_tool/package_config_subset b/labs/lab05/frontend/.dart_tool/package_config_subset index 00584b939..2b4ea566e 100644 --- a/labs/lab05/frontend/.dart_tool/package_config_subset +++ b/labs/lab05/frontend/.dart_tool/package_config_subset @@ -1,369 +1,369 @@ _fe_analyzer_shared 3.5 -file:///Users/timur/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-85.0.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-85.0.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-85.0.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-85.0.0/lib/ analyzer 3.5 -file:///Users/timur/.pub-cache/hosted/pub.dev/analyzer-7.5.6/ -file:///Users/timur/.pub-cache/hosted/pub.dev/analyzer-7.5.6/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/analyzer-7.5.6/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/analyzer-7.5.6/lib/ args 3.3 -file:///Users/timur/.pub-cache/hosted/pub.dev/args-2.7.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/args-2.7.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/args-2.7.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/args-2.7.0/lib/ async 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/async-2.13.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/async-2.13.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/async-2.13.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/async-2.13.0/lib/ boolean_selector 3.1 -file:///Users/timur/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/ build 3.7 -file:///Users/timur/.pub-cache/hosted/pub.dev/build-2.5.4/ -file:///Users/timur/.pub-cache/hosted/pub.dev/build-2.5.4/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/build-2.5.4/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/build-2.5.4/lib/ build_config 3.6 -file:///Users/timur/.pub-cache/hosted/pub.dev/build_config-1.1.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/build_config-1.1.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/build_config-1.1.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/build_config-1.1.2/lib/ build_daemon 3.6 -file:///Users/timur/.pub-cache/hosted/pub.dev/build_daemon-4.0.4/ -file:///Users/timur/.pub-cache/hosted/pub.dev/build_daemon-4.0.4/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/build_daemon-4.0.4/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/build_daemon-4.0.4/lib/ build_resolvers 3.7 -file:///Users/timur/.pub-cache/hosted/pub.dev/build_resolvers-2.5.4/ -file:///Users/timur/.pub-cache/hosted/pub.dev/build_resolvers-2.5.4/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/build_resolvers-2.5.4/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/build_resolvers-2.5.4/lib/ build_runner 3.7 -file:///Users/timur/.pub-cache/hosted/pub.dev/build_runner-2.5.4/ -file:///Users/timur/.pub-cache/hosted/pub.dev/build_runner-2.5.4/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/build_runner-2.5.4/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/build_runner-2.5.4/lib/ build_runner_core 3.7 -file:///Users/timur/.pub-cache/hosted/pub.dev/build_runner_core-9.1.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/build_runner_core-9.1.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/build_runner_core-9.1.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/build_runner_core-9.1.2/lib/ built_collection 2.12 -file:///Users/timur/.pub-cache/hosted/pub.dev/built_collection-5.1.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/built_collection-5.1.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/built_collection-5.1.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/built_collection-5.1.1/lib/ built_value 3.0 -file:///Users/timur/.pub-cache/hosted/pub.dev/built_value-8.10.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/built_value-8.10.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/built_value-8.10.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/built_value-8.10.1/lib/ characters 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/characters-1.4.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/characters-1.4.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/characters-1.4.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/characters-1.4.0/lib/ checked_yaml 3.8 -file:///Users/timur/.pub-cache/hosted/pub.dev/checked_yaml-2.0.4/ -file:///Users/timur/.pub-cache/hosted/pub.dev/checked_yaml-2.0.4/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/checked_yaml-2.0.4/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/checked_yaml-2.0.4/lib/ clock 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/clock-1.1.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/clock-1.1.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/clock-1.1.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/clock-1.1.2/lib/ code_builder 3.5 -file:///Users/timur/.pub-cache/hosted/pub.dev/code_builder-4.10.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/code_builder-4.10.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/code_builder-4.10.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/code_builder-4.10.1/lib/ collection 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/collection-1.19.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/collection-1.19.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/ convert 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/convert-3.1.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/convert-3.1.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/convert-3.1.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/convert-3.1.2/lib/ crypto 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/crypto-3.0.6/ -file:///Users/timur/.pub-cache/hosted/pub.dev/crypto-3.0.6/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/crypto-3.0.6/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/crypto-3.0.6/lib/ dart_style 3.7 -file:///Users/timur/.pub-cache/hosted/pub.dev/dart_style-3.1.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/dart_style-3.1.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/dart_style-3.1.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/dart_style-3.1.0/lib/ dartz 2.12 -file:///Users/timur/.pub-cache/hosted/pub.dev/dartz-0.10.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/dartz-0.10.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/dartz-0.10.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/dartz-0.10.1/lib/ equatable 2.12 -file:///Users/timur/.pub-cache/hosted/pub.dev/equatable-2.0.7/ -file:///Users/timur/.pub-cache/hosted/pub.dev/equatable-2.0.7/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/equatable-2.0.7/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/equatable-2.0.7/lib/ fake_async 3.3 -file:///Users/timur/.pub-cache/hosted/pub.dev/fake_async-1.3.3/ -file:///Users/timur/.pub-cache/hosted/pub.dev/fake_async-1.3.3/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/fake_async-1.3.3/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/fake_async-1.3.3/lib/ ffi 3.7 -file:///Users/timur/.pub-cache/hosted/pub.dev/ffi-2.1.4/ -file:///Users/timur/.pub-cache/hosted/pub.dev/ffi-2.1.4/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/ffi-2.1.4/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/ffi-2.1.4/lib/ file 3.0 -file:///Users/timur/.pub-cache/hosted/pub.dev/file-7.0.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/file-7.0.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/file-7.0.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/file-7.0.1/lib/ fixnum 3.1 -file:///Users/timur/.pub-cache/hosted/pub.dev/fixnum-1.1.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/fixnum-1.1.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/fixnum-1.1.1/lib/ flutter_lints 3.1 -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_lints-3.0.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_lints-3.0.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_lints-3.0.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_lints-3.0.2/lib/ flutter_secure_storage 2.12 -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/ -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/lib/ flutter_secure_storage_linux 2.12 -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/ -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/lib/ flutter_secure_storage_macos 2.12 -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/ -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/lib/ flutter_secure_storage_platform_interface 2.12 -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_platform_interface-1.1.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_platform_interface-1.1.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_platform_interface-1.1.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_platform_interface-1.1.2/lib/ flutter_secure_storage_web 2.12 -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/lib/ flutter_secure_storage_windows 2.12 -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/lib/ frontend_server_client 3.0 -file:///Users/timur/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0/lib/ glob 3.3 -file:///Users/timur/.pub-cache/hosted/pub.dev/glob-2.1.3/ -file:///Users/timur/.pub-cache/hosted/pub.dev/glob-2.1.3/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/glob-2.1.3/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/glob-2.1.3/lib/ graphs 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/graphs-2.3.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/graphs-2.3.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/graphs-2.3.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/graphs-2.3.2/lib/ http 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/http-1.4.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/http-1.4.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/http-1.4.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/http-1.4.0/lib/ http_multi_server 3.2 -file:///Users/timur/.pub-cache/hosted/pub.dev/http_multi_server-3.2.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/http_multi_server-3.2.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/http_multi_server-3.2.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/http_multi_server-3.2.2/lib/ http_parser 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/http_parser-4.1.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/http_parser-4.1.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/ io 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/io-1.0.5/ -file:///Users/timur/.pub-cache/hosted/pub.dev/io-1.0.5/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/io-1.0.5/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/io-1.0.5/lib/ js 2.19 -file:///Users/timur/.pub-cache/hosted/pub.dev/js-0.6.7/ -file:///Users/timur/.pub-cache/hosted/pub.dev/js-0.6.7/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/js-0.6.7/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/js-0.6.7/lib/ json_annotation 3.0 -file:///Users/timur/.pub-cache/hosted/pub.dev/json_annotation-4.9.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/json_annotation-4.9.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/json_annotation-4.9.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/json_annotation-4.9.0/lib/ leak_tracker 3.2 -file:///Users/timur/.pub-cache/hosted/pub.dev/leak_tracker-10.0.9/ -file:///Users/timur/.pub-cache/hosted/pub.dev/leak_tracker-10.0.9/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/leak_tracker-10.0.9/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/leak_tracker-10.0.9/lib/ leak_tracker_flutter_testing 3.2 -file:///Users/timur/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.9/ -file:///Users/timur/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.9/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.9/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.9/lib/ leak_tracker_testing 3.2 -file:///Users/timur/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.1/lib/ lints 3.0 -file:///Users/timur/.pub-cache/hosted/pub.dev/lints-3.0.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/lints-3.0.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/lints-3.0.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/lints-3.0.0/lib/ logging 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/logging-1.3.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/logging-1.3.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/logging-1.3.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/logging-1.3.0/lib/ matcher 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/matcher-0.12.17/ -file:///Users/timur/.pub-cache/hosted/pub.dev/matcher-0.12.17/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/matcher-0.12.17/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/matcher-0.12.17/lib/ material_color_utilities 2.17 -file:///Users/timur/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1/lib/ meta 2.12 -file:///Users/timur/.pub-cache/hosted/pub.dev/meta-1.16.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/meta-1.16.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/meta-1.16.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/meta-1.16.0/lib/ mime 3.2 -file:///Users/timur/.pub-cache/hosted/pub.dev/mime-2.0.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/mime-2.0.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/ mockito 3.7 -file:///Users/timur/.pub-cache/hosted/pub.dev/mockito-5.4.6/ -file:///Users/timur/.pub-cache/hosted/pub.dev/mockito-5.4.6/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/mockito-5.4.6/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/mockito-5.4.6/lib/ package_config 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/package_config-2.2.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/package_config-2.2.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/package_config-2.2.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/package_config-2.2.0/lib/ path 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/path-1.9.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/path-1.9.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path-1.9.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path-1.9.1/lib/ path_provider 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider-2.1.5/ -file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider-2.1.5/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider-2.1.5/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider-2.1.5/lib/ path_provider_android 3.6 -file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17/ -file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17/lib/ path_provider_foundation 3.3 -file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/lib/ path_provider_linux 2.19 -file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/lib/ path_provider_platform_interface 3.0 -file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_platform_interface-2.1.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_platform_interface-2.1.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_platform_interface-2.1.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_platform_interface-2.1.2/lib/ path_provider_windows 3.2 -file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/lib/ platform 3.2 -file:///Users/timur/.pub-cache/hosted/pub.dev/platform-3.1.6/ -file:///Users/timur/.pub-cache/hosted/pub.dev/platform-3.1.6/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/platform-3.1.6/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/platform-3.1.6/lib/ plugin_platform_interface 3.0 -file:///Users/timur/.pub-cache/hosted/pub.dev/plugin_platform_interface-2.1.8/ -file:///Users/timur/.pub-cache/hosted/pub.dev/plugin_platform_interface-2.1.8/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/plugin_platform_interface-2.1.8/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/plugin_platform_interface-2.1.8/lib/ pool 2.12 -file:///Users/timur/.pub-cache/hosted/pub.dev/pool-1.5.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/pool-1.5.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/pool-1.5.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/pool-1.5.1/lib/ pub_semver 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/pub_semver-2.2.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/pub_semver-2.2.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/pub_semver-2.2.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/pub_semver-2.2.0/lib/ pubspec_parse 3.6 -file:///Users/timur/.pub-cache/hosted/pub.dev/pubspec_parse-1.5.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/pubspec_parse-1.5.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/pubspec_parse-1.5.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/pubspec_parse-1.5.0/lib/ shelf 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/shelf-1.4.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/shelf-1.4.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/shelf-1.4.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/shelf-1.4.2/lib/ shelf_web_socket 3.5 -file:///Users/timur/.pub-cache/hosted/pub.dev/shelf_web_socket-3.0.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/shelf_web_socket-3.0.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/shelf_web_socket-3.0.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/shelf_web_socket-3.0.0/lib/ source_gen 3.6 -file:///Users/timur/.pub-cache/hosted/pub.dev/source_gen-2.0.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/source_gen-2.0.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/source_gen-2.0.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/source_gen-2.0.0/lib/ source_span 3.1 -file:///Users/timur/.pub-cache/hosted/pub.dev/source_span-1.10.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/source_span-1.10.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/ stack_trace 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/ stream_channel 3.3 -file:///Users/timur/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/ -file:///Users/timur/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/ stream_transform 3.1 -file:///Users/timur/.pub-cache/hosted/pub.dev/stream_transform-2.1.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/stream_transform-2.1.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/stream_transform-2.1.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/stream_transform-2.1.1/lib/ string_scanner 3.1 -file:///Users/timur/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/ term_glyph 3.1 -file:///Users/timur/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/ test_api 3.5 -file:///Users/timur/.pub-cache/hosted/pub.dev/test_api-0.7.4/ -file:///Users/timur/.pub-cache/hosted/pub.dev/test_api-0.7.4/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/test_api-0.7.4/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/test_api-0.7.4/lib/ timing 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/timing-1.0.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/timing-1.0.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/timing-1.0.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/timing-1.0.2/lib/ typed_data 3.5 -file:///Users/timur/.pub-cache/hosted/pub.dev/typed_data-1.4.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/typed_data-1.4.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/typed_data-1.4.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/typed_data-1.4.0/lib/ vector_math 2.14 -file:///Users/timur/.pub-cache/hosted/pub.dev/vector_math-2.1.4/ -file:///Users/timur/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/vector_math-2.1.4/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/ vm_service 3.3 -file:///Users/timur/.pub-cache/hosted/pub.dev/vm_service-15.0.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/vm_service-15.0.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/vm_service-15.0.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/vm_service-15.0.0/lib/ watcher 3.1 -file:///Users/timur/.pub-cache/hosted/pub.dev/watcher-1.1.2/ -file:///Users/timur/.pub-cache/hosted/pub.dev/watcher-1.1.2/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/watcher-1.1.2/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/watcher-1.1.2/lib/ web 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/web-1.1.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/web-1.1.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/web-1.1.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/web-1.1.1/lib/ web_socket 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/web_socket-1.0.1/ -file:///Users/timur/.pub-cache/hosted/pub.dev/web_socket-1.0.1/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/web_socket-1.0.1/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/web_socket-1.0.1/lib/ web_socket_channel 3.3 -file:///Users/timur/.pub-cache/hosted/pub.dev/web_socket_channel-3.0.3/ -file:///Users/timur/.pub-cache/hosted/pub.dev/web_socket_channel-3.0.3/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/web_socket_channel-3.0.3/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/web_socket_channel-3.0.3/lib/ win32 3.8 -file:///Users/timur/.pub-cache/hosted/pub.dev/win32-5.14.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/win32-5.14.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/win32-5.14.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/win32-5.14.0/lib/ xdg_directories 3.3 -file:///Users/timur/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0/ -file:///Users/timur/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0/lib/ yaml 3.4 -file:///Users/timur/.pub-cache/hosted/pub.dev/yaml-3.1.3/ -file:///Users/timur/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/ -lab05_frontend -3.5 -file:///Users/timur/develop/sum25-go-flutter-course/labs/lab05/frontend/ -file:///Users/timur/develop/sum25-go-flutter-course/labs/lab05/frontend/lib/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/yaml-3.1.3/ +file:///home/pavmash/.pub-cache/hosted/pub.dev/yaml-3.1.3/lib/ sky_engine 3.7 -file:///opt/homebrew/Caskroom/flutter/3.32.4/flutter/bin/cache/pkg/sky_engine/ -file:///opt/homebrew/Caskroom/flutter/3.32.4/flutter/bin/cache/pkg/sky_engine/lib/ +file:///home/pavmash/flutter/bin/cache/pkg/sky_engine/ +file:///home/pavmash/flutter/bin/cache/pkg/sky_engine/lib/ flutter 3.7 -file:///opt/homebrew/Caskroom/flutter/3.32.4/flutter/packages/flutter/ -file:///opt/homebrew/Caskroom/flutter/3.32.4/flutter/packages/flutter/lib/ +file:///home/pavmash/flutter/packages/flutter/ +file:///home/pavmash/flutter/packages/flutter/lib/ flutter_test 3.7 -file:///opt/homebrew/Caskroom/flutter/3.32.4/flutter/packages/flutter_test/ -file:///opt/homebrew/Caskroom/flutter/3.32.4/flutter/packages/flutter_test/lib/ +file:///home/pavmash/flutter/packages/flutter_test/ +file:///home/pavmash/flutter/packages/flutter_test/lib/ flutter_web_plugins 3.7 -file:///opt/homebrew/Caskroom/flutter/3.32.4/flutter/packages/flutter_web_plugins/ -file:///opt/homebrew/Caskroom/flutter/3.32.4/flutter/packages/flutter_web_plugins/lib/ +file:///home/pavmash/flutter/packages/flutter_web_plugins/ +file:///home/pavmash/flutter/packages/flutter_web_plugins/lib/ +lab05_frontend +3.5 +file:///home/pavmash/sum25-go-flutter-course/labs/lab05/frontend/ +file:///home/pavmash/sum25-go-flutter-course/labs/lab05/frontend/lib/ 2 diff --git a/labs/lab05/frontend/.dart_tool/package_graph.json b/labs/lab05/frontend/.dart_tool/package_graph.json index 67f13503a..0d10a4d62 100644 --- a/labs/lab05/frontend/.dart_tool/package_graph.json +++ b/labs/lab05/frontend/.dart_tool/package_graph.json @@ -77,6 +77,13 @@ "test_api" ] }, + { + "name": "flutter_lints", + "version": "3.0.2", + "dependencies": [ + "lints" + ] + }, { "name": "flutter_test", "version": "0.0.0", @@ -534,6 +541,11 @@ "test_api" ] }, + { + "name": "lints", + "version": "3.0.0", + "dependencies": [] + }, { "name": "vm_service", "version": "15.0.0", @@ -870,18 +882,6 @@ "meta", "path" ] - }, - { - "name": "flutter_lints", - "version": "3.0.2", - "dependencies": [ - "lints" - ] - }, - { - "name": "lints", - "version": "3.0.0", - "dependencies": [] } ], "configVersion": 1 diff --git a/labs/lab05/frontend/.dart_tool/version b/labs/lab05/frontend/.dart_tool/version index 462914540..28fb4be5e 100644 --- a/labs/lab05/frontend/.dart_tool/version +++ b/labs/lab05/frontend/.dart_tool/version @@ -1 +1 @@ -3.32.4 \ No newline at end of file +3.32.5 \ No newline at end of file diff --git a/labs/lab05/frontend/build/native_assets/linux/native_assets.json b/labs/lab05/frontend/build/native_assets/linux/native_assets.json new file mode 100644 index 000000000..523bfc7c6 --- /dev/null +++ b/labs/lab05/frontend/build/native_assets/linux/native_assets.json @@ -0,0 +1 @@ +{"format-version":[1,0,0],"native-assets":{}} \ No newline at end of file diff --git a/labs/lab05/frontend/build/test_cache/build/5ed1ec56e46357ec5ad2faed02821e03.cache.dill.track.dill b/labs/lab05/frontend/build/test_cache/build/5ed1ec56e46357ec5ad2faed02821e03.cache.dill.track.dill new file mode 100644 index 000000000..c35a28f89 Binary files /dev/null and b/labs/lab05/frontend/build/test_cache/build/5ed1ec56e46357ec5ad2faed02821e03.cache.dill.track.dill differ diff --git a/labs/lab05/frontend/build/unit_test_assets/AssetManifest.bin b/labs/lab05/frontend/build/unit_test_assets/AssetManifest.bin new file mode 100644 index 000000000..86d111f09 Binary files /dev/null and b/labs/lab05/frontend/build/unit_test_assets/AssetManifest.bin differ diff --git a/labs/lab05/frontend/build/unit_test_assets/AssetManifest.json b/labs/lab05/frontend/build/unit_test_assets/AssetManifest.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/labs/lab05/frontend/build/unit_test_assets/AssetManifest.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/labs/lab05/frontend/build/unit_test_assets/FontManifest.json b/labs/lab05/frontend/build/unit_test_assets/FontManifest.json new file mode 100644 index 000000000..3abf18c41 --- /dev/null +++ b/labs/lab05/frontend/build/unit_test_assets/FontManifest.json @@ -0,0 +1 @@ +[{"family":"MaterialIcons","fonts":[{"asset":"fonts/MaterialIcons-Regular.otf"}]}] \ No newline at end of file diff --git a/labs/lab05/frontend/build/unit_test_assets/NOTICES.Z b/labs/lab05/frontend/build/unit_test_assets/NOTICES.Z new file mode 100644 index 000000000..f7c204305 Binary files /dev/null and b/labs/lab05/frontend/build/unit_test_assets/NOTICES.Z differ diff --git a/labs/lab05/frontend/build/unit_test_assets/NativeAssetsManifest.json b/labs/lab05/frontend/build/unit_test_assets/NativeAssetsManifest.json new file mode 100644 index 000000000..523bfc7c6 --- /dev/null +++ b/labs/lab05/frontend/build/unit_test_assets/NativeAssetsManifest.json @@ -0,0 +1 @@ +{"format-version":[1,0,0],"native-assets":{}} \ No newline at end of file diff --git a/labs/lab05/frontend/build/unit_test_assets/fonts/MaterialIcons-Regular.otf b/labs/lab05/frontend/build/unit_test_assets/fonts/MaterialIcons-Regular.otf new file mode 100644 index 000000000..8c9926613 Binary files /dev/null and b/labs/lab05/frontend/build/unit_test_assets/fonts/MaterialIcons-Regular.otf differ diff --git a/labs/lab05/frontend/build/unit_test_assets/shaders/ink_sparkle.frag b/labs/lab05/frontend/build/unit_test_assets/shaders/ink_sparkle.frag new file mode 100644 index 000000000..85fc35759 Binary files /dev/null and b/labs/lab05/frontend/build/unit_test_assets/shaders/ink_sparkle.frag differ diff --git a/labs/lab05/frontend/lib/core/auth/auth_service.dart b/labs/lab05/frontend/lib/core/auth/auth_service.dart index 891100815..61327b820 100644 --- a/labs/lab05/frontend/lib/core/auth/auth_service.dart +++ b/labs/lab05/frontend/lib/core/auth/auth_service.dart @@ -79,76 +79,96 @@ class AuthService { // Get current user User? get currentUser => _currentState.currentUser; - // TODO: Implement login method - // login authenticates a user with email and password - // Requirements: - // - Validate email and password using FormValidator.validateEmail() and FormValidator.validatePassword() - // - Return AuthResult.validationError if either validation fails - // - Sanitize email input using FormValidator.sanitizeText() - // - Use _userRepository.findByEmail() to get user - // - Return AuthResult.invalidCredentials if user not found - // - Use _userRepository.verifyPassword() to check password - // - Return AuthResult.invalidCredentials if password verification fails - // - Generate JWT token using _jwtService.generateToken() with user.id.toString() and user.email - // - Update _currentState with authenticated user, token, and current DateTime for loginTime - // - Return AuthResult.success on successful authentication - // - Return AuthResult.networkError if any exception occurs during the process Future login(String email, String password) async { - // TODO: Implement this method - throw UnimplementedError('AuthService login not implemented'); + try { + final validEmail = FormValidator.validateEmail(email); + final validPassword = FormValidator.validatePassword(password); + if (validEmail != null || validPassword != null) { + return AuthResult.validationError; + } + + final cleanEmail = FormValidator.sanitizeText(email); + + final user = await _userRepository.findByEmail(cleanEmail); + if (user == null) { + return AuthResult.invalidCredentials; + } + + final rightPassword = await _userRepository.verifyPassword(email, password); + if (!rightPassword) { + return AuthResult.invalidCredentials; + } + + final token = _jwtService.generateToken(user.id.toString(), user.email); + + final currentTime = DateTime.now(); + _currentState = _currentState.copyWith( + isAuthenticated: true, + currentUser: user, + token: token, + loginTime: currentTime, + ); + + return AuthResult.success; + } catch (error) { + return AuthResult.networkError; + } } - // TODO: Implement logout method - // logout clears the current authentication state - // Requirements: - // - Reset _currentState to a new empty AuthState() - // - This should clear isAuthenticated, currentUser, token, and loginTime - // - Method should complete without throwing exceptions Future logout() async { - // TODO: Implement this method - throw UnimplementedError('AuthService logout not implemented'); + _currentState = const AuthState(); } - // TODO: Implement isSessionValid method - // isSessionValid checks if the current session is still valid - // Requirements: - // - Return false if not authenticated (!_currentState.isAuthenticated) - // - Return false if loginTime is null - // - Calculate time difference between current DateTime.now() and _currentState.loginTime - // - Return true if session duration is less than 24 hours - // - Return false if session has expired (24+ hours) bool isSessionValid() { - // TODO: Implement this method - throw UnimplementedError('AuthService isSessionValid not implemented'); + if (!_currentState.isAuthenticated) { + return false; + } + + final loginTime = _currentState.loginTime; + if (loginTime == null) { + return false; + } + + final duration = DateTime.now().difference(loginTime); + + return duration.inHours < 24; } - // TODO: Implement refreshAuth method - // refreshAuth validates and refreshes the current authentication status - // Requirements: - // - Call isSessionValid() to check session validity - // - If session is invalid, call logout() and return false - // - If token is present in _currentState.token, validate it using _jwtService.validateToken() - // - If token validation fails, call logout() and return false - // - Return true if session and token are valid - // - Handle any exceptions and return false if errors occur Future refreshAuth() async { - // TODO: Implement this method - throw UnimplementedError('AuthService refreshAuth not implemented'); + try { + if (!isSessionValid()) { + await logout(); + return false; + } + + final token = _currentState.token; + + if (token == null || !_jwtService.validateToken(token)) { + await logout(); + return false; + } + + return true; + } catch (exception) { + await logout(); + return false; + } } - // TODO: Implement getUserInfo method - // getUserInfo returns user information if authenticated - // Requirements: - // - Return null if not authenticated or currentUser is null - // - Return a Map containing: - // - 'id': currentUser!.id - // - 'name': currentUser!.name - // - 'email': currentUser!.email - // - 'loginTime': _currentState.loginTime?.toIso8601String() (convert to string or null) - // - 'sessionValid': result of calling isSessionValid() Map? getUserInfo() { - // TODO: Implement this method - throw UnimplementedError('AuthService getUserInfo not implemented'); + final user = _currentState.currentUser; + + if (!_currentState.isAuthenticated || user == null) { + return null; + } + + return { + 'id': user!.id, + 'name': user!.name, + 'email': user!.email, + 'loginTime': _currentState.loginTime?.toIso8601String(), + 'sessionValid': isSessionValid() + }; } } diff --git a/labs/lab05/frontend/lib/core/validation/form_validator.dart b/labs/lab05/frontend/lib/core/validation/form_validator.dart index 289c4bf8b..bf350dbd0 100644 --- a/labs/lab05/frontend/lib/core/validation/form_validator.dart +++ b/labs/lab05/frontend/lib/core/validation/form_validator.dart @@ -1,53 +1,65 @@ // Simple form validation with basic security checks class FormValidator { - // TODO: Implement validateEmail method - // validateEmail checks if an email is valid - // Requirements: - // - return null for valid emails - // - return error message for invalid emails - // - check basic email format (contains @ and .) - // - check reasonable length (max 100 characters) static String? validateEmail(String? email) { - // TODO: Implement email validation - // Check for null/empty, basic format, and length - throw UnimplementedError('FormValidator validateEmail not implemented'); + final cleanEmail = sanitizeText(email); + + if (cleanEmail == null || cleanEmail == "") { + return "Email required"; + } + + final emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',); + if (!emailRegex.hasMatch(cleanEmail)) { + return "Email format is invalid"; + } + + if (cleanEmail.length > 100) { + return "Email is too long"; + } + + return null; } - // TODO: Implement validatePassword method - // validatePassword checks if a password meets basic requirements - // Requirements: - // - return null for valid passwords - // - return error message for invalid passwords - // - minimum 6 characters - // - contains at least one letter and one number static String? validatePassword(String? password) { - // TODO: Implement password validation - // Check length and basic complexity - throw UnimplementedError('FormValidator validatePassword not implemented'); + if (password == null || password.isEmpty) { + return "Password required"; + } + + if (password.length < 6) { + return "Password must be at least 6 characters long"; + } + + final lettersRegex = RegExp(r'[A-Za-z]'); + final numbersRegex = RegExp(r'[0-9]'); + if (!lettersRegex.hasMatch(password!) || + !numbersRegex.hasMatch(password!)) { + return "Password must contain at least one letter and number"; + } + + return null; } - // TODO: Implement sanitizeText method - // sanitizeText removes basic dangerous characters - // Requirements: - // - remove < and > characters - // - trim whitespace - // - return cleaned text static String sanitizeText(String? text) { - // TODO: Implement text sanitization - // Clean basic dangerous characters - throw UnimplementedError('FormValidator sanitizeText not implemented'); + if (text == null) { + return ''; + } + + final trimmedText = text.trim(); + final cleanText = trimmedText.replaceAll(RegExp(r'<[^>]*>'), ''); + return cleanText; } - // TODO: Implement isValidLength method - // isValidLength checks if text is within length limits - // Requirements: - // - return true if text length is between min and max - // - handle null text gracefully static bool isValidLength(String? text, {int minLength = 1, int maxLength = 100}) { - // TODO: Implement length validation - // Check text length bounds - throw UnimplementedError('FormValidator isValidLength not implemented'); + if (text == null) { + return false; + } + + if (text.length < minLength || text.length > maxLength) { + return false; + } + + return true; } }