672 lines
14 KiB
Markdown
672 lines
14 KiB
Markdown
# 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](https://flutter.dev/docs/get-started/install) |
|
|
| Docker | 24+ | [docker.com](https://docker.com) |
|
|
| PostgreSQL | 15+ | `brew install postgresql@15` or Docker |
|
|
| Git | 2.x | `brew install git` |
|
|
|
|
### Recommended Tools
|
|
|
|
- **VS Code** with extensions:
|
|
- Go
|
|
- Flutter
|
|
- Dart
|
|
- Docker
|
|
- GitLens
|
|
- **TablePlus** or **DBeaver** for database management
|
|
- **Postman** or **Bruno** for API testing
|
|
|
|
## Security Guidelines
|
|
|
|
### Code Security
|
|
|
|
- **Never log secrets**: Passwords, tokens, keys must never appear in logs
|
|
- **Validate all inputs**: Use `sanitizePath()` for file paths, validate UUIDs
|
|
- **Use structured errors**: Return safe error messages that don't leak internal details
|
|
- **HTTPS only**: All API calls must use HTTPS in production
|
|
- **Input sanitization**: All user inputs must be validated and sanitized
|
|
|
|
### Authentication
|
|
|
|
- **JWT tokens**: Use secure, short-lived tokens
|
|
- **Session validation**: Always validate sessions against database
|
|
- **Passkey security**: Follow WebAuthn best practices
|
|
|
|
### File Operations
|
|
|
|
- **Path validation**: Prevent directory traversal with proper path sanitization
|
|
- **Permission checks**: Verify user permissions before file operations
|
|
- **Scoped access**: Users can only access authorized files/orgs
|
|
|
|
### Development Security
|
|
|
|
- **Local secrets**: Use `.env` files, never commit secrets
|
|
- **Test with security**: Include security tests in development
|
|
- **Review code**: Security review for all changes
|
|
|
|
## Project Setup
|
|
|
|
### 1. Clone the Repository
|
|
|
|
```bash
|
|
git clone https://lab.b0esche.cloud/b0esche/b0esche_cloud.git
|
|
cd b0esche_cloud
|
|
```
|
|
|
|
### 2. Backend Setup
|
|
|
|
```bash
|
|
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)**
|
|
```bash
|
|
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**
|
|
```bash
|
|
createdb b0esche_dev
|
|
```
|
|
|
|
#### Run Migrations
|
|
|
|
```bash
|
|
# Install goose
|
|
go install github.com/pressly/goose/v3/cmd/goose@latest
|
|
|
|
# Run migrations
|
|
goose -dir migrations postgres "$DATABASE_URL" up
|
|
```
|
|
|
|
#### Start Backend
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
./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](https://golang.org/doc/effective_go)
|
|
- Use `gofmt` for formatting
|
|
- Use `golint` and `go vet` for linting
|
|
|
|
```bash
|
|
# 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
|
|
```go
|
|
// 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
|
|
```go
|
|
// 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](https://dart.dev/guides/language/effective-dart)
|
|
- Use `dart format` for formatting
|
|
- Use `dart analyze` for linting
|
|
|
|
```bash
|
|
# 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
|
|
```dart
|
|
// 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
|
|
```dart
|
|
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
|
|
|
|
```bash
|
|
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:
|
|
```go
|
|
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
|
|
|
|
```bash
|
|
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:
|
|
```dart
|
|
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
|
|
|
|
```bash
|
|
cd go_cloud
|
|
|
|
# Create new migration
|
|
goose -dir migrations create add_new_table sql
|
|
```
|
|
|
|
### Migration Best Practices
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```bash
|
|
# 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:**
|
|
```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:**
|
|
```go
|
|
import "log"
|
|
|
|
log.Printf("User login attempt: %s", username)
|
|
```
|
|
|
|
### Frontend Debugging
|
|
|
|
**Chrome DevTools:**
|
|
- Press F12 in Chrome
|
|
- Use the Flutter DevTools extension
|
|
|
|
**Debug print:**
|
|
```dart
|
|
debugPrint('Current state: $state');
|
|
```
|
|
|
|
**VS Code launch.json:**
|
|
```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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# Clean and rebuild
|
|
flutter clean
|
|
flutter pub get
|
|
flutter run -d chrome
|
|
|
|
# Check for dependency issues
|
|
flutter pub deps
|
|
```
|
|
|
|
#### Database migration fails
|
|
```bash
|
|
# 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)
|
|
|
|
```bash
|
|
# 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`:
|
|
```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
|
|
|
|
- [Go Documentation](https://golang.org/doc/)
|
|
- [Flutter Documentation](https://flutter.dev/docs)
|
|
- [WebAuthn Guide](https://webauthn.guide/)
|
|
- [BLoC Library](https://bloclibrary.dev/)
|
|
- [Chi Router](https://github.com/go-chi/chi)
|