Files
b0esche_cloud/docs/DEVELOPMENT.md

13 KiB

b0esche.cloud Development Guide

This guide covers local development setup, coding conventions, and contribution guidelines.

Prerequisites

Required Software

Software Version Installation
Go 1.21+ brew install go
Flutter 3.10+ flutter.dev
Docker 24+ docker.com
PostgreSQL 15+ brew install postgresql@15 or Docker
Git 2.x brew install git
  • VS Code with extensions:
    • Go
    • Flutter
    • Dart
    • Docker
    • GitLens
  • TablePlus or DBeaver for database management
  • Postman or Bruno for API testing

Project Setup

1. Clone the Repository

git clone https://lab.b0esche.cloud/b0esche/b0esche_cloud.git
cd b0esche_cloud

2. Backend Setup

cd go_cloud

# Copy environment file
cp .env.example .env

# Edit .env with your local settings
# Key variables to set:
# - DATABASE_URL=postgres://user:pass@localhost:5432/b0esche_dev?sslmode=disable
# - JWT_SECRET=your-dev-secret
# - WEBAUTHN_RP_ID=localhost
# - WEBAUTHN_RP_ORIGIN=http://localhost:8080

Start PostgreSQL

Option A: Using Docker (Recommended)

docker run -d \
  --name b0esche-postgres \
  -e POSTGRES_USER=b0esche \
  -e POSTGRES_PASSWORD=devpassword \
  -e POSTGRES_DB=b0esche_dev \
  -p 5432:5432 \
  postgres:15-alpine

Option B: Using local PostgreSQL

createdb b0esche_dev

Run Migrations

# Install goose
go install github.com/pressly/goose/v3/cmd/goose@latest

# Run migrations
goose -dir migrations postgres "$DATABASE_URL" up

Start Backend

# Development mode with hot reload
go run ./cmd/api

# Or build and run
go build -o bin/api ./cmd/api
./bin/api

The backend will be available at http://localhost:8080.

3. Frontend Setup

cd b0esche_cloud

# Get dependencies
flutter pub get

# Run in Chrome (recommended for web development)
flutter run -d chrome

# Or run with specific port
flutter run -d chrome --web-port=3000

The frontend will be available at http://localhost:3000 (or the port shown).

4. Quick Start Script

Use the provided development script:

./scripts/dev-all.sh

This starts all services in the correct order.

Project Structure

Backend (go_cloud/)

go_cloud/
├── cmd/
│   └── api/
│       └── main.go           # Application entry point
├── internal/
│   ├── auth/
│   │   ├── auth.go           # Authentication service
│   │   ├── passkey.go        # WebAuthn implementation
│   │   └── auth_test.go      # Tests
│   ├── config/
│   │   └── config.go         # Configuration loading
│   ├── database/
│   │   └── database.go       # Database connection
│   ├── files/
│   │   └── files.go          # File operations
│   ├── http/
│   │   ├── routes.go         # Route definitions
│   │   ├── server.go         # HTTP server setup
│   │   └── wopi_handlers.go  # WOPI protocol handlers
│   ├── middleware/
│   │   └── middleware.go     # HTTP middleware
│   ├── models/
│   │   └── *.go              # Data models
│   ├── org/
│   │   └── org.go            # Organization logic
│   ├── storage/
│   │   ├── nextcloud.go      # Nextcloud integration
│   │   └── webdav.go         # WebDAV client
│   └── ...
├── migrations/
│   ├── 0001_initial.sql
│   ├── 0002_passkeys.sql
│   └── ...
├── pkg/
│   └── jwt/
│       └── jwt.go            # JWT utilities
├── .env.example
├── Dockerfile
├── go.mod
└── Makefile

Frontend (b0esche_cloud/)

b0esche_cloud/
├── lib/
│   ├── main.dart             # App entry point
│   ├── injection.dart        # Dependency injection
│   ├── blocs/
│   │   ├── auth/
│   │   │   ├── auth_bloc.dart
│   │   │   ├── auth_event.dart
│   │   │   └── auth_state.dart
│   │   ├── files/
│   │   └── org/
│   ├── models/
│   │   ├── user.dart
│   │   ├── file.dart
│   │   └── organization.dart
│   ├── pages/
│   │   ├── home_page.dart
│   │   ├── files_page.dart
│   │   ├── settings_page.dart
│   │   └── admin/
│   ├── repositories/
│   │   ├── auth_repository.dart
│   │   └── file_repository.dart
│   ├── services/
│   │   ├── api_client.dart
│   │   └── webauthn_service.dart
│   ├── theme/
│   │   └── app_theme.dart
│   └── widgets/
│       ├── file_list.dart
│       └── ...
├── web/
│   └── index.html
├── pubspec.yaml
└── analysis_options.yaml

Coding Conventions

Go Backend

Code Style

  • Follow Effective Go
  • Use gofmt for formatting
  • Use golint and go vet for linting
# Format code
gofmt -w .

# Lint
golint ./...
go vet ./...

Naming Conventions

  • Packages: lowercase, single word (auth, files)
  • Exported functions: PascalCase (CreateUser)
  • Private functions: camelCase (validateToken)
  • Constants: PascalCase (DefaultTimeout)

Error Handling

// Always handle errors explicitly
user, err := s.GetUser(ctx, id)
if err != nil {
    return nil, fmt.Errorf("failed to get user: %w", err)
}

// Use custom error types for API errors
type APIError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
}

Project Patterns

// Service pattern
type AuthService struct {
    db     *sqlx.DB
    config *config.Config
}

func NewAuthService(db *sqlx.DB, config *config.Config) *AuthService {
    return &AuthService{db: db, config: config}
}

// Handler pattern
func (h *Handler) HandleLogin(w http.ResponseWriter, r *http.Request) {
    // Parse request
    // Call service
    // Return response
}

Flutter Frontend

Code Style

  • Follow Effective Dart
  • Use dart format for formatting
  • Use dart analyze for linting
# Format code
dart format .

# Analyze
dart analyze
flutter analyze

Naming Conventions

  • Classes: PascalCase (AuthBloc)
  • Files: snake_case (auth_bloc.dart)
  • Variables/Functions: camelCase (getUserName)
  • Constants: camelCase or SCREAMING_CAPS

BLoC Pattern

// Events
abstract class AuthEvent {}
class LoginRequested extends AuthEvent {
  final String username;
  LoginRequested(this.username);
}

// States
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
  final User user;
  AuthAuthenticated(this.user);
}
class AuthError extends AuthState {
  final String message;
  AuthError(this.message);
}

// BLoC
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  AuthBloc() : super(AuthInitial()) {
    on<LoginRequested>(_onLoginRequested);
  }

  Future<void> _onLoginRequested(
    LoginRequested event,
    Emitter<AuthState> emit,
  ) async {
    emit(AuthLoading());
    try {
      final user = await _authRepository.login(event.username);
      emit(AuthAuthenticated(user));
    } catch (e) {
      emit(AuthError(e.toString()));
    }
  }
}

Widget Structure

class MyWidget extends StatelessWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<MyBloc, MyState>(
      builder: (context, state) {
        return switch (state) {
          MyLoading() => const CircularProgressIndicator(),
          MyLoaded(:final data) => _buildContent(data),
          MyError(:final message) => Text('Error: $message'),
          _ => const SizedBox.shrink(),
        };
      },
    );
  }
}

Testing

Backend Tests

cd go_cloud

# Run all tests
go test ./...

# Run with coverage
go test -cover ./...

# Run specific package
go test ./internal/auth/...

# Verbose output
go test -v ./...

Example test:

func TestAuthService_Login(t *testing.T) {
    // Setup
    db := setupTestDB(t)
    service := NewAuthService(db, testConfig)

    // Test
    user, err := service.Login(context.Background(), "testuser")

    // Assert
    assert.NoError(t, err)
    assert.Equal(t, "testuser", user.Username)
}

Frontend Tests

cd b0esche_cloud

# Run all tests
flutter test

# Run with coverage
flutter test --coverage

# Run specific test file
flutter test test/auth_bloc_test.dart

# Run integration tests
flutter test integration_test/

Example test:

void main() {
  group('AuthBloc', () {
    late AuthBloc authBloc;
    late MockAuthRepository mockRepository;

    setUp(() {
      mockRepository = MockAuthRepository();
      authBloc = AuthBloc(authRepository: mockRepository);
    });

    blocTest<AuthBloc, AuthState>(
      'emits [AuthLoading, AuthAuthenticated] on successful login',
      build: () => authBloc,
      act: (bloc) => bloc.add(LoginRequested('testuser')),
      expect: () => [
        AuthLoading(),
        isA<AuthAuthenticated>(),
      ],
    );
  });
}

Database Migrations

Creating a Migration

cd go_cloud

# Create new migration
goose -dir migrations create add_new_table sql

Migration Best Practices

-- migrations/0005_add_feature.sql

-- +goose Up
-- Add new column with default
ALTER TABLE users ADD COLUMN new_field TEXT DEFAULT '';

-- Create index for performance
CREATE INDEX idx_users_new_field ON users(new_field);

-- +goose Down
DROP INDEX IF EXISTS idx_users_new_field;
ALTER TABLE users DROP COLUMN new_field;

Running Migrations

# Apply all pending migrations
goose -dir migrations postgres "$DATABASE_URL" up

# Rollback last migration
goose -dir migrations postgres "$DATABASE_URL" down

# Check migration status
goose -dir migrations postgres "$DATABASE_URL" status

Debugging

Backend Debugging

VS Code launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Go Backend",
      "type": "go",
      "request": "launch",
      "mode": "debug",
      "program": "${workspaceFolder}/go_cloud/cmd/api",
      "envFile": "${workspaceFolder}/go_cloud/.env"
    }
  ]
}

Logging:

import "log"

log.Printf("User login attempt: %s", username)

Frontend Debugging

Chrome DevTools:

  • Press F12 in Chrome
  • Use the Flutter DevTools extension

Debug print:

debugPrint('Current state: $state');

VS Code launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Flutter Web",
      "type": "dart",
      "request": "launch",
      "program": "lib/main.dart",
      "deviceId": "chrome"
    }
  ]
}

Git Workflow

Branch Naming

  • feature/description - New features
  • fix/description - Bug fixes
  • refactor/description - Code refactoring
  • docs/description - Documentation updates

Commit Messages

Follow conventional commits:

type(scope): description

feat(auth): add passkey registration flow
fix(files): correct upload progress display
docs(readme): update deployment instructions
refactor(api): extract common error handling

Pull Request Process

  1. Create feature branch from main
  2. Make changes with atomic commits
  3. Run tests locally
  4. Push and create PR
  5. Wait for review
  6. Squash and merge

Troubleshooting

Common Issues

Backend won't start

# Check if port is in use
lsof -i :8080

# Check database connection
psql $DATABASE_URL -c "SELECT 1"

# Check logs
go run ./cmd/api 2>&1 | head -50

Flutter build fails

# Clean and rebuild
flutter clean
flutter pub get
flutter run -d chrome

# Check for dependency issues
flutter pub deps

Database migration fails

# Check current status
goose -dir migrations postgres "$DATABASE_URL" status

# Force specific version
goose -dir migrations postgres "$DATABASE_URL" fix

WebAuthn not working locally

  • WebAuthn requires HTTPS in production
  • For localhost, use WEBAUTHN_RP_ID=localhost
  • Chrome allows WebAuthn on localhost without HTTPS

Environment Variables Reference

Backend (.env)

# Server
SERVER_ADDR=:8080
DEV_MODE=true

# Database
DATABASE_URL=postgres://user:pass@localhost:5432/dbname?sslmode=disable

# Authentication
JWT_SECRET=your-secret-key
WEBAUTHN_RP_ID=localhost
WEBAUTHN_RP_NAME=b0esche.cloud
WEBAUTHN_RP_ORIGIN=http://localhost:8080

# External Services (optional for local dev)
NEXTCLOUD_BASE_URL=https://storage.b0esche.cloud
NEXTCLOUD_USERNAME=admin
NEXTCLOUD_PASSWORD=password
COLLABORA_BASE_URL=https://of.b0esche.cloud

Frontend

API base URL is configured in lib/services/api_client.dart:

class ApiClient {
  // For development
  static const baseUrl = 'http://localhost:8080';
  
  // For production (set via build args)
  // static const baseUrl = String.fromEnvironment('API_URL');
}

Resources